ブロックチェーン

PythonによるWeb3とNFT(ERC721)の実践入門

投稿日:2022年2月11日 更新日:

ERC721に準拠したスマートコントラクト(NFT(Non Fragile Token)と呼ばれる)をSolidity言語を使ってプログラミングし、PythonのWeb3から以下を実施します。

①コンパイル
②ブロックチェーンへの配置(デプロイ)
③メソッドの呼び出し


■ NFT=ERC721とは何か

仮想通貨の一つにイーサリアム(ethereum)があります。イーサリアムのブロックチェーンにデジタル貨幣以外に、スマートコントラクトと呼ばれるSolidity言語で書かれたプログラムを流通させることができます。

スマートコントラクトを作る目的は、契約情報などの機密性が高く、改ざんが許されないデータを
ブロックチェーンで管理するためです。

スマートコントラクトは、自由にプログラミングすることができます。ERC(Ethereum Requests for Comments)とはイーサリアムのコミュニティによって作成された規格です。
スマートコントラクトのメソッド(インタフェース)仕様が公開されているので、誰でもERCに準拠したスマートコントラクトを使うことができます。

ERC721は、何等かのモノなどを識別するための情報(トークン)を安全に管理し、他者への移転をするための規格です。

ERC721のメソッドには以下のものがあります。トークンを識別するトークンIDを指定して呼び出します。

 ownerOf(uint256 tokenId)
  トークンIDで特定されるトークンの所有者のアカウントを返す

 balanceOf(address owner)
  アカウントが所有するトークンの数を返す

 approve(address spender, uint256 tokenId)
  トークンIDのトークンを他アカウントへ移転することを事前に承認する

 transferFrom(address from, address to, uint256 tokenId)
  トークンIDのトークンを他アカウントへ移転する

この中で、NFTの主な機能であるとトークンの移転を実行するのが、transferFromメソッドです。
ブロックチェーン内に生成されたスマートコントラクトの内部で管理されたトークンを、ブロックチェーンのトランザクションを発行して、メソッドを呼び出すことで移転します。

トランザクションを発行して、ブロックを生成しないとメソッドを呼び出せないため、ブロックチェーンのブロック改ざんが実施不可能であるメリットを生かすことができます。

逆に言えば、ERC721で規定されているのは、トークンの移転の機能だけです。その他は、自由に定義することが可能です。

■ ERC721準拠スマートコントラクトの開発環境

ERC721準拠のスマートコントラクトを開発するには以下の環境が必要になります。

1)Truffle

Solidity言語で書かれたスマートコントラクトのコンパイルを実施します。
単独のSolidityコンパイラーは、コンパイルだけしかできませんが、
Truffleでは、コンパイルだけでなく、専用のCLI上のコマンドラインからブロックチェーンへのデプロイまで実施でき、実際にメソッドを実行することができます。

Widnows10でのインストール方法は、以下の通りです。

①Truffleのインストールに必要となるNode.jsをインストールする

https://nodejs.org/ja/


インストール後、node、npmが使えるようになっているか、バージョンを確認する。


②Truffleをインストールする

③任意のフォルダ配下(プロジェクトフォルダ)にTruffle環境を作成する

2)OpenZeppelin

ERC20/721など、ERC規格に準拠したベースクラスとなるスマートコントラクトを提供してくれるSDKです。
上記のプロジェクトフォルダ配下にインストールします。
独自のERC721準拠のスマートコントラクトは、これらを継承することで作成します。

上記のtruffleプロジェクトのフォルダ配下に、 OpenZeppelin をインストールします。
node_modulesフォルダが作成されます。

ERC721スマートコントラクトは、以下にインストールされます。

3) Ganache

テスト用のブロックチェーンとして、Ganacheをインストールします。
マイニングをさせることなく、自動でトランザクションをリアルタイムに処理してくれます。
ただし、テンポラリのブロックチェーンなので、再起動するとトランザクションは消えます。

https://trufflesuite.com/ganache/

■ ERC721準拠のスマートコントラクトを作成する

まず、管理したい情報(トークン)を何にするか決め、それを管理するためのオリジナルのスマートコントラクトを作成します。これは、通常のスマートコントラクトとして作成します
必要となる主な機能は、トークンとアドレスとの相互変換、アドレスが保持するトークン数の取得、トークンの移動です。

その後、このスマートコントラクトとERC721のスマートコントラクトを継承させた、ERC721準拠のスマートコントラクトを作成します。

必要となる主な機能は、前述したメソッド(ownerOf、balanceOf、approve、transferFromなど)となります。
これらの機能は、継承しているスマートコントラクトの トークンとアドレスとの相互変換、アドレスが保持するトークン数の取得、トークンの移動 の機能を使うことで実現されます。

例として、書籍をトークン(オリジナルコンテンツ)として販売する書籍販売用スマートコントラクト(BookContract)を考えます。

書籍に書籍番号を採番し、価格を付与します。書籍番号をトークンIDとして、スマートコントラクトで管理するものとします。書籍実体は、書籍番号をファイル名とし、クラウド上で管理するものとします。

このBookContactと ERC721のスマートコントラクトを継承させた、ERC721準拠の書籍販売用スマートコントラクト( BookContact _ERC721)を作成します。


・スマートコントラクト:BookContract

プライベートデータとして、以下の配列を管理します。

①書籍番号(トークンID)の配列

 // book list
 book[] public book_list;

 ※bookは以下の構造体

   struct book {
   uint    book_id; //  書籍番号(トークンID)
   uint    price;  // 価格(単位:wei)
  }


②書籍番号(トークンID)の所有者アドレスを管理するリスト

 ※所有者アドレスは、pyable属性を付与し、
  トークンを移動する場合、価格に相当する費用を送金できるようにします。

 // Mapping from doc id to owner
 mapping (uint => address payable) public BookIdToOwner;

③所有者アドレスが保有する文書番号(トークンID)の数を管理するリスト
 (都度、②のリストを使って、数え上げてもいい)

 // Mapping from owner to number of owned book id
 mapping (address => uint) ownerCount;


④ ①の配列番号から書籍番号(トークンID)を取得するリスト

 // Mapping from book list id to book id
 mapping (uint => uint) BookListIdToBookId;

⑤書籍番号(トークンID)から①の配列番号を取得するリスト 

 // Mapping from book id to book list id
 mapping (uint => uint) BookIdToBookListId;


以下のメソッドを用意します。

①書籍作成

 function CreateBook(
       address payable _sc_owner_addr, //  所有者アドレス ※payable属性付与
       uint _book_id , //  書籍番号(トークンID)
       uint _price //  価格(単位:wei)
      ) public  


②所有者アドレスが所有する書籍番号(複数)を取得

 function getBookByOwner(address _owner) external view returns (uint[] memory)

③所有者アドレスが保有する書籍番号の数を取得

 function getCountByOwner(address _owner) external view returns (uint)

④書籍番号(トークンID)の所有者アドレスを取得

 function getOwnerByBookId(uint _book_id) public view returns (address)

⑤書籍番号(トークンID)の所有者に、書籍番号(トークンID)の価格を支払い

 ※Redirectという名前の意味は、直接、所有者に支払いが行われるわけではなく、
  スマートコントラクト経由で支払いが行われる
  (購入者からスマートコントラクトへのトランザクションのみで、所有者への支払いが行われる)

 function PayPriceRedirect(uint _book_id) public payable


このBookContractとERC721のスマートコントラクトを継承したERC721準拠の書籍販売用スマートコントラクト(BookContract_ERC721)は以下のような定義となります。

 contract BookContract_ERC721 is ERC721,BookContract

pragma solidity ^0.8.0;   // solidityのバージョン(コンパイラのバージョンと合わせる)

//import './contracts/utils/math/SafeMath.sol';
//import "./contracts/access/ownable.sol";
import '../node_modules/openzeppelin-solidity/contracts/utils/math/SafeMath.sol';
import "../node_modules/openzeppelin-solidity/contracts/access/ownable.sol";


/**
 * @title CowBreeding
 * @dev Cow Basic Implementation to breed them and provides basic functions.
 */  

 // コントラクトにも残高(Balance)がある
 // getBalanceでとれる
 // paylableにすると、コントラクト自身にも送金できる →コントラクトを生成した所有者のアカウントではない

contract BookContract is Ownable {

  using SafeMath for uint256;
  event Event_CreateBook(uint book_id,uint price);
  event Event_Transfer(address indexed from, address indexed to, uint256 value);

  struct book {
    uint    book_id;
    uint    price;
  }

  // book list
  book[] public book_list;

  // Mapping from book id to owner
  mapping (uint => address payable) public BookIdToOwner;

  // Mapping from owner to number of owned book
  mapping (address => uint) ownerBookCount;

  // Mapping from book list id to book id
  mapping (uint => uint) BookListIdToBookId;

  // Mapping from  book id to book list id
  mapping (uint => uint) BookIdToBookListId;

  modifier onlyOwnerOf(address _ownerOf) {
    require(msg.sender == _ownerOf);
    _;
  }

  //
  //  書籍生成メソッド(プライベートメソッド)
  // 
  function _CreateBook(address payable _sc_owner_addr,uint _book_id,uint _price) internal {

    book_list.push(book(_book_id, _price));
    uint id = book_list.length - 1;

    BookListIdToBookId[id] = _book_id;
    BookIdToBookListId[_book_id] = id;

    BookIdToOwner[_book_id] = _sc_owner_addr;

    ownerBookCount[_sc_owner_addr] = ownerBookCount[_sc_owner_addr].add(1);
    emit Event_CreateBook(_book_id,_price);
  }

  //
  //  書籍生成メソッド
  // 
  function CreateBook(address payable _sc_owner_addr,uint _book_id , uint _price) public {
    _CreateBook(_sc_owner_addr,_book_id, _price);
  }


  //
  // 所有者の保有する書籍リストを取得するメソッド
  // 
  function getBookByOwner(address _owner) external view returns (uint[] memory) {
    uint[] memory BookByOwner = new uint[](ownerBookCount[_owner]);
    uint counter = 0;
    uint number;

    for (uint i = 0; i < book_list.length; i++) {

      number = BookListIdToBookId[i];
      if (BookIdToOwner[number] == _owner) {
        BookByOwner[counter] = number;
        counter = counter.add(1);
      }
    }
    return BookByOwner;
  }

  //
  // 所有者の保有する書籍数を取得するメソッド
  // 
  function getCountByOwner(address _owner) external view returns (uint) {
    return ownerBookCount[_owner]; 
  }

  //
  // 書籍番号(トークンID)の書籍リスト配列番号を取得するメソッド
  // 
  function getBookListIdByBookId(uint _doc_id) external view returns (uint) {
    return BookIdToBookListId[_doc_id];
  }

  //
  // 書籍番号(トークンID)の所有者アドレスを取得するメソッド
  // 
  function getOwnerByBookId(uint _book_id) public view returns (address) {
    return BookIdToOwner[_book_id];
  } 

  //
  // 書籍番号(トークンID)の価格をチェックするメソッド
  // 
  function _CheckPrice(uint _doc_id,uint256 _price_wei) public view returns (bool) {
    uint id = BookIdToBookListId[_doc_id];

    if(book_list[id].price <= _price_wei)
    {
      return(true);
    }
    return(false);
  }

  //
  // 所有者に送金するメソッド
  // 
  function PayPriceRedirect(uint _book_id) public payable{

    require(_CheckPrice(_book_id, msg.value));

    // オーナーに送金
    address payable dst = BookIdToOwner[_book_id];
    dst.transfer(msg.value);
    emit Event_Transfer(msg.sender,dst , msg.value);
  }

}

・ ERC721準拠スマートコントラクト: BookContract_ERC721

プライベートデータとして、以下の配列を管理します。
 
①承認済みのトークンIDとアドレスを管理するリスト

 // Mapping from token ID to approved address
 mapping (uint256 => address) private _tokenApprovals;

②所有者アドレスによって操作者アドレスが承認済かどうかを管理するリスト

 // Mapping from owner to operator approvals
 mapping (address => mapping (address => bool)) private _operatorApprovals


以下の継承しているERC721のスマートコントラクトの以下のメソッドをオーバライドして、用意します。

①書籍番号(トークンID)の所有者アドレスを取得

 継承するBookConractの getBookByOwner メソッドを呼び出し、
 指定された書籍番号(トークンID)の所有者アドレスを取得する

 function ownerOf(uint256 _tokenId) public override view returns (address _owner)

②所有者アドレスの書籍番号(トークンID)の数を取得

 継承するBookContarctの getBookByOwner メソッドを呼び出し、
 指定された所有者アドレスの 文書番号(トークンID)の数を取得する

 function balanceOf(address _owner) public override view returns (uint256 _balance)

③指定したアドレスに対し、書籍番号(トークンID)の移転を許可

 指定されたアドレス(_to)に対し、指定された書籍番号(トークンID)の移転を許可する

 アドレスは、書籍番号(トークンID)の所有者以外であること、
 そして、このメソッドを呼び出したアカウントのアドレスが所有者でることが条件である。
 
 function approve(address _to, uint256 _tokenId) public override

④書籍番号(トークンID)を移転
 
 指定されたアドレス(_from)からアドレス(_to)へと、書籍番号(トークンID)を移転する。

 指定されたアドレス(_from)と 、このメソッドを呼び出したアカウントのアドレスが 、書籍番号(トークンID)
 の所有者であること、
 あるいは、このメソッドを呼び出したアカウントのアドレスが、 書籍番号(トークンID) の所有者から
 事前に許可されていることが条件である。

 function transferFrom(address _from, address _to, uint256 _tokenId) public override

pragma solidity ^0.8.0;   // solidityのバージョン(コンパイラのバージョンと合わせる)

//import './contracts/utils/math/SafeMath.sol';
//import "./contracts/token/ERC721/ERC721.sol";
import '../node_modules/openzeppelin-solidity/contracts/utils/math/SafeMath.sol';
import "../node_modules/openzeppelin-solidity/contracts/token/ERC721/ERC721.sol";
import "./BookContract.sol";

contract BookContract_ERC721 is ERC721,BookContract{

  using SafeMath for uint256;
  using Address for address;

  constructor()  ERC721("BookContract", "BC"){}

  // Equals to `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`
  // which can be also obtained as `IERC721Receiver(0).onERC721Received.selector`
  bytes4 private constant _ERC721_RECEIVED = 0x150b7a02;

  // Mapping from token ID to approved address
  mapping (uint256 => address) private _tokenApprovals;

  // Mapping from owner to operator approvals
  mapping (address => mapping (address => bool)) private _operatorApprovals;

  /**
   * @dev Returns the number of tokens in ``owner``'s account.
   */
  function balanceOf(address _owner) public override view returns (uint256 _balance) {

    require(_owner != address(0));
    return ownerBookCount[_owner];
  }

  /**
   * @dev Returns the owner of the `tokenId` token.
   *
   * Requirements:
   *
   * - `tokenId` must exist.
   */
  function ownerOf(uint256 _tokenId) public override view returns (address _owner) {
    address owner = BookIdToOwner[_tokenId];
    require(owner != address(0));
    return owner;
  }

  /* @dev Gives permission to `to` to transfer `tokenId` token to another account.
   * The approval is cleared when the token is transferred.
   *
   * Only a single account can be approved at a time, so approving the zero address clears previous approvals.
   *
   * Requirements:
   *
   * - The caller must own the token or be an approved operator.
   * - `tokenId` must exist.
   *
   * Emits an {Approval} event.
   */
  function approve(address _to, uint256 _tokenId) public override{
    address owner = ownerOf(_tokenId);
    require(_to != owner);
    require(msg.sender == owner || isApprovedForAll(owner, msg.sender));

    _tokenApprovals[_tokenId] = _to;
    emit Approval(owner, _to, _tokenId);
  }

  /*
   * @dev Gets the approved address for a token ID, or zero if no address set
   * Reverts if the token ID does not exist.
   * @param tokenId uint256 ID of the token to query the approval of
   * @return address currently approved for the given token ID
   */
  function getApproved(uint256 _tokenId) public override view returns (address operator) {
    require(_exists(_tokenId));
    return _tokenApprovals[_tokenId];
  }

  /**
   * @dev Returns the account approved for `tokenId` token.
   *
   * Requirements:
   *
   * - `tokenId` must exist.
   */
  function setApprovalForAll(address _operator, bool _approved) public override {
    require(_operator != msg.sender);
    _operatorApprovals[msg.sender][_operator] = _approved;
    emit ApprovalForAll(msg.sender, _operator, _approved);
  }

  /**
   * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
   *
   * See {setApprovalForAll}
  */
  function isApprovedForAll(address _owner, address _operator) public override view returns (bool) {
    return _operatorApprovals[_owner][_operator];
  }

  /**
   * @dev Transfers `tokenId` token from `from` to `to`.
   *
   * WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible.
   *
   * Requirements:
   *
   * - `from` cannot be the zero address.
   * - `to` cannot be the zero address.
   * - `tokenId` token must be owned by `from`.
   * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
   *
   * Emits a {Transfer} event.
   */
  // ERC721は、モノの移転だけの機能で、送金の機能ではない  
  function transferFrom(address _from, address _to, uint256 _tokenId) public override {
    require(_isApprovedOrOwner(msg.sender, _tokenId));
    _transferFrom(_from, _to, _tokenId);
  }

  /**
   * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
   * are aware of the ERC721 protocol to prevent tokens from being forever locked.
   *
   * Requirements:
   *
   * - `from` cannot be the zero address.
   * - `to` cannot be the zero address.
   * - `tokenId` token must exist and be owned by `from`.
   * - If the caller is not `from`, it must be have been allowed to move this token by either {approve} or {setApprovalForAll}.
   * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
   *
   * Emits a {Transfer} event.
  */
  function safeTransferFrom(address _from, address _to, uint256 _tokenId) public override {
    safeTransferFrom(_from, _to, _tokenId, "");
  }

  /**
   * @dev Safely transfers `tokenId` token from `from` to `to`.
   *
   * Requirements:
   *
   * - `from` cannot be the zero address.
   * - `to` cannot be the zero address.
   * - `tokenId` token must exist and be owned by `from`.
   * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
   * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
   *
   * Emits a {Transfer} event.
   */
  //function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes calldata _data) public override{
  function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes memory _data) public override{
    transferFrom(_from, _to, _tokenId);
  }

  // private function to return whether the specified token exists
  function _exists(uint256 _tokenId) internal override view returns (bool) {
    address owner = BookIdToOwner[_tokenId];
    return owner != address(0);
  }

  // private function to return whether the given spender can transfer a given token ID
  function _isApprovedOrOwner(address _spender, uint256 _tokenId) internal override view returns (bool) {
    address owner = ownerOf(_tokenId);
    return (_spender == owner || getApproved(_tokenId) == _spender || isApprovedForAll(owner, _spender));
  }

  // private function to transfer ownership of a given token ID to another address.
  function _transferFrom(address _from, address _to, uint256 _tokenId) internal {
    require(ownerOf(_tokenId) ==_from);
    require(_to != address(0));

    _clearApproval(_tokenId);

    ownerBookCount[_from] = ownerBookCount[_from].sub(1);
    ownerBookCount[_to]   = ownerBookCount[_to].add(1);

    // トークンの所有権を移行
    BookIdToOwner[_tokenId] = payable(_to);

    emit Transfer(_from, _to, _tokenId);  
  }

  //  Private function to clear current approval of a given token ID
  function _clearApproval(uint256 _tokenId) private {
    if (_tokenApprovals[_tokenId] != address(0)) {
      _tokenApprovals[_tokenId] = address(0);
    }
  }

}

■  ERC721準拠文書管理用スマートコントラクトのデバック

スマートコントラクトのデバックは、truffleのコンソール(CLI)を使って、
テスト用のブロックチェーンであるGanacheを起動し、以下の手順で実施できます。

① Ganacheを起動する


② truffleプロジェクト配下のtruffle-config.jsファイルを編集し、
 Ganacheとの通信設定(アドレス:127.0.0.1とポート:7545)を記載する

③truffleプロジェクト配下のcontractsフォルダに作成したコントラクトを保存する

④配置用のファイル(2_deploy_book.js)を作成し、migrations配下に保存する


⑤truffleを起動

 truffle console –network development


⑥migrateコマンドにより、スマートコントラクトのコンパイルとGanacheへの配置を実施する

⑦Ganacheへ配置したスマートコントラクトのメソッドを実行する

⑧アカウントの確認と設定

 Ganacheのアカウントを取得し、表示します。
 一つ目のアカウントをaccount0、二つ目のアカウントをaccount1の変数に設定します。

 var accounts = web3.eth.getAccounts()
 var account0; web3.eth.getAccounts().then(function(result){ account0 = result[0]; })
 var account1; web3.eth.getAccounts().then(function(result){ account1 = result[1]; })

⑨スマートコントラクトのトークンを生成 

 var book; BookContract_ERC721.deployed().then((obj) => { book = obj; }) 

⑩生成したトークンを使ってメソッドを呼び出す

 ・account0の所有者で書籍番号(トークンID)=123、価格=1000000000000000 weiの書籍トークンを生成する

  book.CreateBook(account0,123,1000000000000000,{from: account0})

 ・書籍番号(トークンID)=123の代金(価格=1000000000000000 wei)を、account0から支払う

  book.PayPriceRedirect(123,{from: account0,value: 1000000000000000})

 ・書籍番号(トークンID)=123の所有者であるaccount0が、account1に対して移転の許可をする

  book.approve(account1,123,{from: account0})

 ・ 書籍番号(トークンID)=123の移転がaccount1に許可されていることを確認する

  book.getApproved(123)

   
 ・ 書籍番号(トークンID)=123をaccount0からaccount1へ、許可されたaccount1で移転する

  book.transferFrom(account0,account1,123,{from: account1})

 ・account1の書籍番号数が1であることを確認する

  book.balanceOf(account1)


 ・ 書籍番号(トークンID)=123の所有者がaccount1であることを確認する

  book.ownerOf(123)  

                                            

■ Python+Web3によるERC721準拠文書管理用スマートコントラクトの操作

Ganacheを使って、 Python+Web3によるERC721準拠文書管理用スマートコントラクトを操作します。
まず、Python+Web3を動作させる環境を以下のフォルダ配下に作成します

 C:\Users\swata\Desktop\Python\bookcontract\web3


スマートコントラクトファイル(BookContract.sol、BookContract_ERC721.sol)、Web3用Pythonファイル(test_web3_book_manage.py、test_web3_book_owner.py、test_web3_book_buyer.py)を保存します。

contractsフォルダは、”C:\Users\swata\Desktop\Python\bookcontract\node_modules\openzeppelin-solidity\contracts”以下をスマートコントラクトからimportするため、コピーしています。
それに合わせて、スマートコントラクトファイル(BookContract.sol、BookContract_ERC721.sol) のimportのパスを変更しています。

・BookContract.sol

 import ‘./contracts/utils/math/SafeMath.sol’;
 import “./contracts/access/ownable.sol”;
 //import ‘../node_modules/openzeppelin-solidity/contracts/utils/math/SafeMath.sol’;
 //import “../node_modules/openzeppelin-solidity/contracts/access/ownable.sol”;

・BookContract_ERC721.sol

 import ‘./contracts/utils/math/SafeMath.sol’;
 import “./contracts/token/ERC721/ERC721.sol”;
 //import ‘../node_modules/openzeppelin-solidity/contracts/utils/math/SafeMath.sol’;
 //import “../node_modules/openzeppelin-solidity/contracts/token/ERC721/ERC721.sol”;


Python+Web3をERC721準拠書籍販売用スマートコントラクトを扱うため、運営者、所有者、購入者の3者が介在します。アカウントは、所有者:account[0]、購入者:account[1]、運営者:account[2]とします。
流れとしては、以下のようになります。


1)運営者のWeb3用Pyrhonコード
 
以下を実施します。

 ①書籍販売用スマートコントラクト(BookContract.sol、BookContract_ERC721.sol)のコンパイル
 ② 書籍販売用スマートコントラクト(BookContract.sol、BookContract_ERC721.so)のブロックチェーン上へのデプロイ
  →デプロイした書籍販売用スマートコントラクトのabiとaddressを
   test_web3_book_owner.py、test_web3_book_buyer.pyで使用する

# -*- coding: utf-8 -*-
import os
import time

#Web3関連
from web3 import Web3, HTTPProvider,EthereumTesterProvider

#solifityコンパイラ関連
import solcx
from solcx import compile_standard, install_solc


# スマートコントラクトをブロックチェーンに登録する関数
def DeployContract(w3, contract_interface):

    # 登録するアカウントのアドレスとパスワード(ganache用)
    from_address = w3.eth.accounts[2]
    from_address_password = ''

    # 登録するアカウントのアドレスとパスワード(geth用)
    #from_address = w3.eth.accounts[2]
    #from_address_password = 'hoge3'

    ct_address = None

    # スマートコントラクトのabiとバイトコードを指定
    token_ct = w3.eth.contract(abi=contract_interface['abi'], bytecode=contract_interface['bin'])

    # 登録するアカウントをアンロックする(1秒後にアンロック)
    #if w3.geth.personal.unlock_account(from_address, from_address_password): 
    if w3.geth.personal.unlock_account(from_address, from_address_password,0): 

        # ※geth上でマイニングを開始(miner.start())しておく

        # スマートコントラクトを登録するためのトランザクションを生成する
        tx_hash = token_ct.constructor().transact({'from': from_address,'gas': 5000000})

        # トランザクションがブロックチェーンに登録されるのを待つ
        tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)

        # 登録されたスマートコントラクトのチェックサム付きアドレスを取得する
        ct_address = tx_receipt.contractAddress

    return ct_address

path = os.getcwd()
print(path)
os.chdir('C:\\Users\\swata\\Desktop\\Python\\bookcontract\\web3')

# Solidity 0.8.0 install
install_solc("0.8.0")

# スマートコントラクトのソースファイルを指定し、コンパイルを実施
compiled_sol=solcx.compile_files(['BookContract_ERC721.sol'],solc_version="0.8.0")

# GanacheのURL(IPアドレス、ポート)を指定し接続し、web3のインスタンスを生成
w3 = Web3(HTTPProvider('http://localhost:7545'))
# gethのURL(IPアドレス、ポート)を指定し接続し、web3のインスタンスを生成
#w3 = Web3(HTTPProvider('http://localhost:8545'))


contract_interface = None
address = None

# コンパイルしたスマートコントラクトはキー配列なので、そこから該当のスマートコントラクトを取得する
for compiled_contract_key in compiled_sol.keys():

    # 該当のスマートコントラクトのキー名と一致(ソースファイルパス:スマートコントラクト名)
    if compiled_contract_key == 'BookContract_ERC721.sol:BookContract_ERC721':

        # スマートコントラクトの実体を取得
        contract_interface = compiled_sol[compiled_contract_key]

        # コンパイルしたスマートコントラクトをブロックチェーンに登録し、チェックサム付きアドレスを取得
        address = DeployContract(w3, contract_interface)

# スマートコントラクトの実体からabiを取得
abi=contract_interface['abi']

print(abi)
print(address)

2)書籍の作成者(=所有者)のコード

以下を実施します。

 ①BookContarctのCreateBookメソッドで書籍番号(トークンID)を作成
 ②自分のアカウントへの振込(送金)を確認(残高が価格分、増えているのを確認)

  ※BookContarctのPayPriceRedirectメソッドのトランザクションアドレスをメールなどで受け取り、
  トランザクション情報(w3.eth.getTransaction(tx_address))を確認してもいい

  ※ただし、スマートコントラクトを介した送金は、スマートコントラクトのメソッド呼び出しのトランザクション内で実施されるため、送金元(購入者)のアカウントアドレスがgetTransactionではわからない。
  そのため、購入者のアカウントアドレスも メールなどで受け取る必要がある。

 ③所有者は、 BookContract_ERC721のapproveメソッドを使って、自分のアカウントで 購入者への移転を許可する
  (購入者のアカウントアドレスは、 メールなどで受け取る)

# -*- coding: utf-8 -*-
import time
import os

#Web3関連
from web3 import Web3, HTTPProvider,EthereumTesterProvider

path = os.getcwd()
print(path)
os.chdir('C:\\Users\\swata\\Desktop\\Python\\bookcontract\\web3')

# GanacheのURL(IPアドレス、ポート)を指定し接続し、web3のインスタンスを生成
w3 = Web3(HTTPProvider('http://localhost:7545'))
# gethのURL(IPアドレス、ポート)を指定し接続し、web3のインスタンスを生成
#w3 = Web3(HTTPProvider('http://localhost:8545'))


# デプロイされた書籍販売用スマートコントラクト(BookContract.sol、BookContract_ERC721.so)のabiとaddress
abi = [{'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': False, 'internalType': 'uint256', 'name': 'book_id', 'type': 'uint256'}, {'indexed': False, 'internalType': 'uint256', 'name': 'price', 'type': 'uint256'}], 'name': 'Event_CreateBook', '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': 'Event_Transfer', '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': True, 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256'}], 'name': 'Transfer', 'type': 'event'}, {'inputs': [{'internalType': 'uint256', 'name': '', 'type': 'uint256'}], 
'name': 'BookIdToOwner', 'outputs': [{'internalType': 'address payable', 'name': '', 'type': 'address'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'internalType': 'address payable', 'name': '_sc_owner_addr', 'type': 'address'}, {'internalType': 'uint256', 
'name': '_book_id', 'type': 'uint256'}, {'internalType': 'uint256', 'name': '_price', 'type': 'uint256'}], 'name': 'CreateBook', 'outputs': [], 'stateMutability': 'nonpayable', 'type': 'function'}, {'inputs': [{'internalType': 'uint256', 'name': '_book_id', 'type': 'uint256'}], 'name': 'PayPriceRedirect', 'outputs': [], 'stateMutability': 'payable', 'type': 'function'}, {'inputs': [{'internalType': 'uint256', 'name': '_doc_id', 'type': 'uint256'}, {'internalType': 'uint256', 'name': '_price_wei', 'type': 'uint256'}], 'name': '_CheckPrice', 'outputs': [{'internalType': 'bool', 'name': '', 'type': 'bool'}], 'stateMutability': 'view', 'type': 'function'}, {'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': '_balance', 'type': 'uint256'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'internalType': 'uint256', 'name': '', 'type': 'uint256'}], 'name': 'book_list', 'outputs': [{'internalType': 'uint256', 'name': 'book_id', 'type': 'uint256'}, {'internalType': 'uint256', 'name': 'price', 'type': 'uint256'}], 'stateMutability': 
'view', 'type': 'function'}, {'inputs': [{'internalType': 'uint256', 'name': '_tokenId', 'type': 'uint256'}], 'name': 'getApproved', 'outputs': [{'internalType': 'address', 'name': 'operator', 'type': 'address'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'internalType': 'address', 'name': '_owner', 'type': 'address'}], 'name': 'getBookByOwner', 'outputs': [{'internalType': 'uint256[]', 'name': '', 'type': 'uint256[]'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'internalType': 'uint256', 'name': '_doc_id', 'type': 'uint256'}], 'name': 'getBookListIdByBookId', 'outputs': [{'internalType': 'uint256', 'name': '', 'type': 'uint256'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'internalType': 'address', 'name': '_owner', 'type': 'address'}], 'name': 'getCountByOwner', 'outputs': [{'internalType': 'uint256', 'name': '', 'type': 'uint256'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'internalType': 'uint256', 'name': '_book_id', 'type': 'uint256'}], 'name': 'getOwnerByBookId', 'outputs': [{'internalType': 'address', 'name': '', 'type': 'address'}], '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': [], 'name': 'name', 'outputs': [{'internalType': 'string', 'name': '', 'type': 'string'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [], 'name': 'owner', 
'outputs': [{'internalType': 'address', 'name': '', 'type': 'address'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'internalType': 'uint256', 'name': '_tokenId', 'type': 'uint256'}], 'name': 'ownerOf', 'outputs': [{'internalType': 'address', 'name': '_owner', 'type': 'address'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [], 'name': 'renounceOwnership', 'outputs': [], 'stateMutability': 'nonpayable', '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': 'tokenId', 'type': 'uint256'}], 'name': 'tokenURI', 'outputs': [{'internalType': 'string', 'name': '', 'type': 'string'}], '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'}, {'inputs': [{'internalType': 'address', 'name': 'newOwner', 'type': 'address'}], 'name': 'transferOwnership', 'outputs': [], 'stateMutability': 'nonpayable', 'type': 'function'}]

address = '0x676E39Bfaf83B12eb6afd3Ff7c4874ed4c2027FE'

print(abi)
print(address)

# [Tips]
#
#  gethやWalletアプリで、スマートコントラクトを登録する場合に画面上に表示されるアドレスには、チェックサムが付いていない
# もし、それらで登録したスマートコントラクトのメソッドをWeb3から呼び出す場合、
#  以下のようにチェックサム付きアドレスに変換する必要がある
#
#  address = Web3.toChecksumAddress('0x71c8f9cc77c4c91b5039784ba947e12084cb65a7')
#

# スマートコントラクトのabiとチェックサム付きアドレスを指定し、トークンを生成する
token = w3.eth.contract(abi=abi, address=address)

# スマートコントラクトのメソッドを実行するアカウントをアンロックする(即時アンロック)(ganache用)
if w3.geth.personal.unlock_account(w3.eth.accounts[0], '',0): 
# スマートコントラクトのメソッドを実行するアカウントをアンロックする(即時アンロック)(geth用)
#if w3.geth.personal.unlock_account(w3.eth.accounts[0], 'hoge1',0): 

    # ※geth上でマイニングを開始(miner.start())しておく

    book_id = 123               # 書籍番号(トークンID)
    price = 1000000000000000    # 価格(wei)

    # 書籍番号(トークンID)を作成 
    # スマートコントラクトの更新系メソッドを実行するためのトランザクションを生成する
    tx_hash=token.functions.CreateBook(w3.eth.accounts[0],book_id,price).transact({'from': w3.eth.accounts[0],'gas': 5000000})

    # トランザクションがブロックチェーンに登録されるのを待つ
    tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)

    # 登録されたトランザクションのチェックサム付きアドレスを取得する
    tx_address = tx_receipt.transactionHash
    print(tx_address)

    # トランザクションの確認
    transaction=w3.eth.getTransaction(tx_address)
    print(transaction)

    # 現在の残高を取得
    org_balance=w3.eth.getBalance(w3.eth.accounts[0])
    print(org_balance)

    # 送金確認
    while(True):

        # 現在の残高を取得
        balance=w3.eth.getBalance(w3.eth.accounts[0])
        print(balance)

        # 残高が増えたか(価格分が送金されたか)確認
        if balance >= org_balance+price:
            break

        # 30秒待つ
        time.sleep(30)


    # スマートコントラクトの移転を許可する(所有者:account[0] ⇒ 購入者:account[1])
    # スマートコントラクトの更新系メソッドを実行するためのトランザクションを生成する
    tx_hash=token.functions.approve(w3.eth.accounts[1],book_id).transact({'from': w3.eth.accounts[0],'gas': 5000000})

    # トランザクションがブロックチェーンに登録されるのを待つ
    tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)

    # 登録されたトランザクションのチェックサム付きアドレスを取得する
    tx_address = tx_receipt.transactionHash
    print(tx_address)

    # トランザクションの確認
    transaction=w3.eth.getTransaction(tx_address)
    print(transaction)

3)購入者のコード

 ①購入者は、 BookContarctのPayPriceRedirectメソッドを使って、自分のアカウントで、書籍番号(トークンID)の所有者に書籍番号(トークンID) の価格を支払う

 ②購入者は、BookContract_ERC721のgetApprovedメソッドを使って、所有者より移転が許可されたことを確認する
 ③購入者は、BookContract_ERC721のtransferFromメソッドを使って、自分のアカウントで文書番号(トークンID)を自分へと移転する

 ④購入者は、BookContract_ERC721のbalanceOf、getBookByOwnerメソッドを使って、 書籍番号(トークンID)が自分のアカウントアドレスに移転されたことを確認する

# -*- coding: utf-8 -*-
import time
import os

#Web3関連
from web3 import Web3, HTTPProvider,EthereumTesterProvider

path = os.getcwd()
print(path)
os.chdir('C:\\Users\\swata\\Desktop\\Python\\bookcontract\\web3')

# GanacheのURL(IPアドレス、ポート)を指定し接続し、web3のインスタンスを生成
w3 = Web3(HTTPProvider('http://localhost:7545'))
# gethのURL(IPアドレス、ポート)を指定し接続し、web3のインスタンスを生成
#w3 = Web3(HTTPProvider('http://localhost:8545'))

# デプロイされた書籍販売用スマートコントラクト(BookContract.sol、BookContract_ERC721.so)のabiとaddress
abi = [{'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': False, 'internalType': 'uint256', 'name': 'book_id', 'type': 'uint256'}, {'indexed': False, 'internalType': 'uint256', 'name': 'price', 'type': 'uint256'}], 'name': 'Event_CreateBook', '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': 'Event_Transfer', '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': True, 'internalType': 'uint256', 'name': 'tokenId', 'type': 'uint256'}], 'name': 'Transfer', 'type': 'event'}, {'inputs': [{'internalType': 'uint256', 'name': '', 'type': 'uint256'}], 
'name': 'BookIdToOwner', 'outputs': [{'internalType': 'address payable', 'name': '', 'type': 'address'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'internalType': 'address payable', 'name': '_sc_owner_addr', 'type': 'address'}, {'internalType': 'uint256', 
'name': '_book_id', 'type': 'uint256'}, {'internalType': 'uint256', 'name': '_price', 'type': 'uint256'}], 'name': 'CreateBook', 'outputs': [], 'stateMutability': 'nonpayable', 'type': 'function'}, {'inputs': [{'internalType': 'uint256', 'name': '_book_id', 'type': 'uint256'}], 'name': 'PayPriceRedirect', 'outputs': [], 'stateMutability': 'payable', 'type': 'function'}, {'inputs': [{'internalType': 'uint256', 'name': '_doc_id', 'type': 'uint256'}, {'internalType': 'uint256', 'name': '_price_wei', 'type': 'uint256'}], 'name': '_CheckPrice', 'outputs': [{'internalType': 'bool', 'name': '', 'type': 'bool'}], 'stateMutability': 'view', 'type': 'function'}, {'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': '_balance', 'type': 'uint256'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'internalType': 'uint256', 'name': '', 'type': 'uint256'}], 'name': 'book_list', 'outputs': [{'internalType': 'uint256', 'name': 'book_id', 'type': 'uint256'}, {'internalType': 'uint256', 'name': 'price', 'type': 'uint256'}], 'stateMutability': 
'view', 'type': 'function'}, {'inputs': [{'internalType': 'uint256', 'name': '_tokenId', 'type': 'uint256'}], 'name': 'getApproved', 'outputs': [{'internalType': 'address', 'name': 'operator', 'type': 'address'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'internalType': 'address', 'name': '_owner', 'type': 'address'}], 'name': 'getBookByOwner', 'outputs': [{'internalType': 'uint256[]', 'name': '', 'type': 'uint256[]'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'internalType': 'uint256', 'name': '_doc_id', 'type': 'uint256'}], 'name': 'getBookListIdByBookId', 'outputs': [{'internalType': 'uint256', 'name': '', 'type': 'uint256'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'internalType': 'address', 'name': '_owner', 'type': 'address'}], 'name': 'getCountByOwner', 'outputs': [{'internalType': 'uint256', 'name': '', 'type': 'uint256'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'internalType': 'uint256', 'name': '_book_id', 'type': 'uint256'}], 'name': 'getOwnerByBookId', 'outputs': [{'internalType': 'address', 'name': '', 'type': 'address'}], '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': [], 'name': 'name', 'outputs': [{'internalType': 'string', 'name': '', 'type': 'string'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [], 'name': 'owner', 
'outputs': [{'internalType': 'address', 'name': '', 'type': 'address'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [{'internalType': 'uint256', 'name': '_tokenId', 'type': 'uint256'}], 'name': 'ownerOf', 'outputs': [{'internalType': 'address', 'name': '_owner', 'type': 'address'}], 'stateMutability': 'view', 'type': 'function'}, {'inputs': [], 'name': 'renounceOwnership', 'outputs': [], 'stateMutability': 'nonpayable', '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': 'tokenId', 'type': 'uint256'}], 'name': 'tokenURI', 'outputs': [{'internalType': 'string', 'name': '', 'type': 'string'}], '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'}, {'inputs': [{'internalType': 'address', 'name': 'newOwner', 'type': 'address'}], 'name': 'transferOwnership', 'outputs': [], 'stateMutability': 'nonpayable', 'type': 'function'}]

address = '0x676E39Bfaf83B12eb6afd3Ff7c4874ed4c2027FE'

print(abi)
print(address)

# [Tips]
#
#  gethやWalletアプリで、スマートコントラクトを登録する場合に画面上に表示されるアドレスには、チェックサムが付いていない
# もし、それらで登録したスマートコントラクトのメソッドをWeb3から呼び出す場合、
#  以下のようにチェックサム付きアドレスに変換する必要がある
#
#  address = Web3.toChecksumAddress('0x71c8f9cc77c4c91b5039784ba947e12084cb65a7')
#

# スマートコントラクトのabiとチェックサム付きアドレスを指定し、トークンを生成する
token = w3.eth.contract(abi=abi, address=address)

# [Tips]
#
#  gethやWalletアプリで、スマートコントラクトを登録する場合に画面上に表示されるアドレスには、チェックサムが付いていない
# もし、それらで登録したスマートコントラクトのメソッドをWeb3から呼び出す場合、
#  以下のようにチェックサム付きアドレスに変換する必要がある
#
#  address = Web3.toChecksumAddress('0x71c8f9cc77c4c91b5039784ba947e12084cb65a7')
#


# スマートコントラクトのabiとチェックサム付きアドレスを指定し、トークンを生成する
token = w3.eth.contract(abi=abi, address=address)

# スマートコントラクトのメソッドを実行するアカウントをアンロックする(即時アンロック)(ganache用)
if w3.geth.personal.unlock_account(w3.eth.accounts[1], '',0): 
# スマートコントラクトのメソッドを実行するアカウントをアンロックする(即時アンロック)(geth用)
#if w3.geth.personal.unlock_account(w3.eth.accounts[1], 'hoge2',0): 

    book_id = 123               # 書籍番号(トークンID)
    price = 1000000000000000    # 価格(wei)

    #書籍番号(トークンID)の所有者に書籍番号(トークンID) の価格を支払う 
    # スマートコントラクトの更新系メソッドを実行するためのトランザクションを生成する
    tx_hash=token.functions.PayPriceRedirect(book_id).transact({'from': w3.eth.accounts[1],'value': price,'gas': 5000000})

    # トランザクションがブロックチェーンに登録されるのを待つ
    tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)

    # 登録されたトランザクションのチェックサム付きアドレスを取得する
    tx_address = tx_receipt.transactionHash
    print(tx_address)

    transaction=w3.eth.getTransaction(tx_address)
    print(transaction)
    print(transaction['from'])  # from 
    print(transaction['to'])    # スマートコントラクトのアドレス
    print(transaction['value']) # price


    # 移転許可の確認
    while(True):

        # 移転許可取得
        result=token.functions.getApproved(book_id).call()
        print(result)

        # 書籍番号(トークンID)の移転許可が自分のアカウントとなっているか確認
        if result == w3.eth.accounts[1]:
            # 許可された
            break

        # 30秒待つ
        time.sleep(30)


    # スマートコントラクトの移転(所有者:w3.eth.accounts[0] ⇒ 購入者:w3.eth.accounts[1])
    # ※実施は購入者(w3.eth.accounts[1])
    tx_hash=token.functions.transferFrom(w3.eth.accounts[0],w3.eth.accounts[1],book_id).transact({'from': w3.eth.accounts[1],'gas': 5000000})

    # トランザクションがブロックチェーンに登録されるのを待つ
    tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)

    # 登録されたトランザクションのチェックサム付きアドレスを取得する
    tx_address = tx_receipt.transactionHash
    print(tx_address)

    transaction=w3.eth.getTransaction(tx_address)
    print(transaction)


    #参照系メソッドの呼び出し

    # 書籍番号(トークンID)数の取得
    result=token.functions.balanceOf(w3.eth.accounts[1]).call()
    print(result)

    # 自アカウントの書籍番号(トークンID)の取得
    result=token.functions.getBookByOwner(w3.eth.accounts[1]).call()
    print(result)

■  truffleとイーサリアム(ethereum)のプライベートチェーンとの実動作確認

Ganacheではなく、イーサリアム(ethereum)のプライベートチェーン を使う場合、
gethを以下のように起動します。

IPアドレスは、ローカルアドレス(127.0.0.1)、ポートは8545とし、”rpcapi”オプションに、”net”を指定します。

geth –syncmode “full” –cache=1024 –networkid “15” –datadir “./eth_private_net” –nodiscover –mine –rpc –rpcapi eth,web3,personal,net –rpcaddr “localhost” –rpcport “8545” –rpccorsdomain “*” –allow-insecure-unlock console 2>> “./eth_private_net/geth.log”

アカウントは、同様に、所有者:account[0]、購入者:account[1]、運営者:account[2]とします。
各アカウントで、残高があることを確認し、不足すれば、マイニングをして増やします。

各トランザクションのGASは、運営者の account[2] が取得するものとし、account[2]でマイニングを開始させます。

Web3用Pyrhonコードの変更点は、以下の通りです。

1)接続するIPアドレスとポート

 # gethのURL(IPアドレス、ポート)を指定し接続し、web3のインスタンスを生成
 w3 =Web3(HTTPProvider(‘http://localhost:8545’))

2)アカウントのパスワード

・test_web3_book_manage.py 

    # 登録するアカウントのアドレスとパスワード(geth用)   
 from_address = w3.eth.accounts[2] 
 from_address_password = ‘hoge3’


・test_web3_book_owner.py

# アカウントアンロック(geth用パスワード)
 if w3.geth.personal.unlock_account(w3.eth.accounts[0], ‘hoge1’,0):

・test_web3_book_buyer.py

# アカウントアンロック(geth用パスワード)
 if w3.geth.personal.unlock_account(w3.eth.accounts[1], ‘hoge2’,0):

■ まとめ

スマートコントラクトで、混乱するのは、メソッドを呼び出す際にfromなどで指定するアカウントアドレス(msg.sender)と、メソッドの引数に指定したアカウントアドレスがあることです。常に、メソッドを呼び出したアカウントが誰なのかを意識しておく必要があります。

プログラミングを考えるとき、スマートコントラクトの業務フローを最初に考えて、必要なロジックを検討する必要があります。これをしないと、プログラミングをしている途中で、混乱します。

スマートコントラクトの操作は、中心となるデータベースがトランザクションとブロックの集まりであるブロックチェーンに置き換わったようなイメージですが、特に、リアルタイム性などは別物です。
ブロックチェーンの特性を検討した実装と運用が求められます。


ソフトウェア開発・システム開発業務/セキュリティ関連業務/ネットワーク関連業務/最新技術に関する業務など、「学習力×発想力×達成力×熱意」で技術開発の実現をサポート。お気軽にお問合せ下さい




-ブロックチェーン

執筆者:


comment

メールアドレスが公開されることはありません。

関連記事

実践入門「スマートコントラクト」とは?

ブロックチェーン技術を使った仮想通貨プラットフォームであるEtherReum上では、暗号通貨のトランザクションをブロックチェーンのブロックとして生成できるだけではなく、独自のトランザクションを”プログ …

決済とブロックチェーンの関係

モノを買う時、お金を支払う行為を「決済する」と言います。決済とは、お互いに合意して決めた量の自分のお金を他人に引き渡すことです。意思決定と口座から口座へ資金を移動する業務が合体した言葉です。決済者とは …

Chinese (Simplified)Chinese (Traditional)EnglishFilipinoFrenchGermanHindiJapaneseKoreanMalayThaiVietnamese