記事の内容
Web3jを使ってコントラクトを実行する方法は以下の記事で記載しました。
この記事ではコントラクト操作用のJavaクラスを作成し、そのクラスを経由してコントラクトを実行するというものです。
このやり方ではコントラクトが増えてくるとコントラクトの数だけJavaのクラスが増えてしまい、管理が煩雑になる可能性があります。
そこで、コントラクトのアドレスとファンクション名を指定して実行する方法を記載します。
環境
- solidity:0.4.24
- jdk:1.8
- web3j:4.5.0
- OS:Windows10
サンプルのコントラクト
コントラクトはOpenzeppelinを使用したERC20トークンを発行するものです。
内容はERC20.solを継承したクラスを作成し、コンストラクタだけ用意したものになります。
pragma solidity ^0.4.24;
import "node_modules/openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
string public name = "OrgToken"; //トークンの名称
string public symbol = "ORG"; //トークンの単位表記
uint public decimals = 18; // 小数点の桁数
address public account = msg.sender;
uint256 public _totalS = 10000000000000;
constructor () public {
super._mint(account, _totalS);
}
function balanceOf(address target) public view returns (uint256) {
return super.balanceOf(target);
}
}
コントラクト操作用のJavaクラス
続いてかなり雑な作りですがJavaのクラスです。
基本的な考え方はコントラクトのアドレス、ファンクション名、引数、戻り値が分かれば実行出来るよね。という作りです。
import java.io.IOException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
import org.web3j.abi.FunctionEncoder;
import org.web3j.abi.FunctionReturnDecoder;
import org.web3j.abi.TypeReference;
import org.web3j.abi.datatypes.Address;
import org.web3j.abi.datatypes.Bool;
import org.web3j.abi.datatypes.Function;
import org.web3j.abi.datatypes.Type;
import org.web3j.abi.datatypes.Uint;
import org.web3j.abi.datatypes.Utf8String;
import org.web3j.crypto.Credentials;
import org.web3j.crypto.RawTransaction;
import org.web3j.crypto.TransactionEncoder;
import org.web3j.protocol.admin.Admin;
import org.web3j.protocol.core.DefaultBlockParameterName;
import org.web3j.protocol.core.methods.request.Transaction;
import org.web3j.protocol.core.methods.response.EthCall;
import org.web3j.protocol.core.methods.response.EthGetTransactionCount;
import org.web3j.protocol.core.methods.response.EthSendTransaction;
import org.web3j.protocol.http.HttpService;
import org.web3j.utils.Numeric;
public class MyContractExec {
public static final Admin web3j = Admin.build(new HttpService("http://127.0.0.1:8545"));
private static final String CONTRACT_ADDRESS = "0x715d4c293482e6f72e1cbb3b22a10fbdae3bd7b0";
public static final String OWNER_ADDRESS = "0x945cd603a6754cb13c3d61d8fe240990f86f9f8a";
public static final String TO_ADDRESS = "0x66b4e7be902300f9a15d900822bbd8803be87391";
public static void main(String args[]) {
MyContractExec exec = new MyContractExec();
List<?> result = null;
try {
List<Type> inputParam = new ArrayList<>();
// 引数なし/戻り値uint256
result = exec.execFunction("totalSupply", inputParam, ResultType.INT);
System.out.println("Total Supply : " + ((Uint)result.get(0)).getValue());
// FROM_ADRESS と TO_ADDRESSのBalance確認
confirmBalance(OWNER_ADDRESS, TO_ADDRESS);
// 引数あり(address, uint256)/戻り値bool
inputParam = new ArrayList<>();
inputParam.add(new Address(TO_ADDRESS));
inputParam.add(new Uint(BigInteger.valueOf(123456)));
result = sendSignedTransaction(credentials, "transfer", inputParam, ResultType.BOOL);
System.out.println( ((Bool)result.get(0)).getValue() );
confirmBalance(OWNER_ADDRESS, TO_ADDRESS);
}catch(IOException e) {
e.printStackTrace();
}
}
public static void confirmBalance(String ... args) throws IOException{
MyContractExec exec = new MyContractExec();
List<?> result = null;
int i = 0;
for(String address :args) {
List<Type> inputParam = new ArrayList<>();
// 引数あり(address)/戻り値uint256
inputParam.add(new Address(address));
result = exec.execFunction("balanceOf", inputParam, ResultType.INT);
System.out.println("Balance of ADDRESS[" + i + "] : " + ((Uint)result.get(0)).getValue() );
i++;
}
}
/**
* Transactionの発生しない(値の更新がない)functionの実行
* @param functionName
* @return
* @throws IOException
*/
public List<?> execFunction(String functionName, List<Type> args, ResultType type) throws IOException{
Function function = new Function(
functionName, args, Arrays.<TypeReference<?>>asList( getTypeReference(type) ));
String encodedFunction = FunctionEncoder.encode(function);
EthCall ethCall = web3j.ethCall(
Transaction.createEthCallTransaction(
OWNER_ADDRESS, CONTRACT_ADDRESS, encodedFunction),
DefaultBlockParameterName.LATEST)
.send();
String value = ethCall.getValue();
return FunctionReturnDecoder.decode(value, function.getOutputParameters());
}
/**
* Transactionの発生する(値の更新がある)functionの実行
* @param credentials
* @param functionName
* @param args
* @param type
* @return
*/
public static List<?> sendSignedTransaction(Credentials credentials, String functionName, List<Type> args, ResultType type){
Function function = new Function(
functionName, args, Arrays.<TypeReference<?>>asList( getTypeReference(type) ));
String encodedFunction = FunctionEncoder.encode(function);
try {
// nonce値を取得する
EthGetTransactionCount ethTransactionCount =
web3j.ethGetTransactionCount(credentials.getAddress(), DefaultBlockParameterName.PENDING).send();
BigInteger nonce = ethTransactionCount.getTransactionCount();
//トランザクション生成
RawTransaction rawTransaction = RawTransaction.createTransaction(
nonce,
BigInteger.valueOf(1000),
BigInteger.valueOf(4700000),
CONTRACT_ADDRESS,
encodedFunction);
//署名
byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, credentials);
String hexValue = Numeric.toHexString(signedMessage);
//send
EthSendTransaction ethSendTransaction = web3j.ethSendRawTransaction(hexValue).sendAsync().get();
//エラー確認
if(ethSendTransaction.getError() != null){
System.out.println(ethSendTransaction.getError().getMessage());
}else {
String value = ethSendTransaction.getResult();
return FunctionReturnDecoder.decode(value, function.getOutputParameters());
}
}catch(IOException | ExecutionException | InterruptedException e) {
e.printStackTrace();
}
return null;
}
public static TypeReference<?> getTypeReference(ResultType type){
TypeReference<?> typeReference = null;
switch(type) {
case ADDRESS:
typeReference = new TypeReference<Address>() {};
break;
case BOOL:
typeReference = new TypeReference<Bool>() {};
break;
case STRING:
typeReference = new TypeReference<Utf8String>() {};
break;
case INT:
typeReference = new TypeReference<Uint>() {};
break;
default:
break;
}
return typeReference;
}
}
public enum ResultType {
ADDRESS,
BOOL,
STRING,
INT
}
mainメソッドでやっていることは単純です。
トークンの発行量の確認、残高を確認し、送金。
最後に残高を再確認する。
これをcontractの作りに着目して言い換えると以下のようになります。
- 値の更新なしのファンクション実行
- 値の更新ありのファンクション実行
##1. 値の更新なしのファンクション実行
まずは値の更新が発生しないファンクションの実行です。
ERC20.solの中だと「totalSupply」や「balanceOf」などがあります。
コードだと以下の部分になります。
public List<?> execFunction(String functionName, List<Type> args, ResultType type) throws IOException{
Function function = new Function(
functionName, args, Arrays.<TypeReference<?>>asList( getTypeReference(type) ));
String encodedFunction = FunctionEncoder.encode(function);
EthCall ethCall = web3j.ethCall(
Transaction.createEthCallTransaction(
OWNER_ADDRESS, CONTRACT_ADDRESS, encodedFunction),
DefaultBlockParameterName.LATEST)
.send();
String value = ethCall.getValue();
return FunctionReturnDecoder.decode(value, function.getOutputParameters());
}
やっていることは
- Functionのインスタンス作成
- Functionをバイナリ化
- Ethereumのネットワーク上に送信
- バイナリで返ってきた結果を変換
このサンプルコードでは「execFunction」の引数で呼び出し元から戻り値のタイプに何を返して欲しいかを指定しています。
ResultTypeは列挙型でsolidityで設定している型をJavaではどのクラスとして扱うのかを定義しています。
「getTypeReference」メソッドの中で戻り値にあったクラスを返すようにしています。
最後にバイナリデータとして返ってきた値をデコードして呼び出し元に返しています。
2. 値の更新ありのファンクション実行
次は値の更新があるファンクションの呼び出しです。
これは作りがかなり違います。
public static List<?> sendSignedTransaction(Credentials credentials, String functionName, List<Type> args, ResultType type){
Function function = new Function(
functionName, args, Arrays.<TypeReference<?>>asList( getTypeReference(type) ));
String encodedFunction = FunctionEncoder.encode(function);
try {
// nonce値を取得する
EthGetTransactionCount ethTransactionCount =
web3j.ethGetTransactionCount(credentials.getAddress(), DefaultBlockParameterName.PENDING).send();
BigInteger nonce = ethTransactionCount.getTransactionCount();
//トランザクション生成
RawTransaction rawTransaction = RawTransaction.createTransaction(
nonce,
BigInteger.valueOf(1000), //GAS
BigInteger.valueOf(4700000), //GASLimit
CONTRACT_ADDRESS,
encodedFunction);
//署名
byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, credentials);
String hexValue = Numeric.toHexString(signedMessage);
//send
EthSendTransaction ethSendTransaction = web3j.ethSendRawTransaction(hexValue).sendAsync().get();
//エラー確認
if(ethSendTransaction.getError() != null){
System.out.println(ethSendTransaction.getError().getMessage());
}else {
String value = ethSendTransaction.getResult();
return FunctionReturnDecoder.decode(value, function.getOutputParameters());
}
}catch(IOException | ExecutionException | InterruptedException e) {
e.printStackTrace();
}
return null;
}
やっていることは
- Functionインスタンスの作成
- Functionのバイナリ化
- ナンス値の取得
- トランザクションの生成
- 署名
- トランザクションの送信
- 結果の確認
値の更新ありということでトランザクションを生成し、ブロックに取り込まれたタイミングで更新が確定されます。
なので、値の更新がないファンクションとは呼び出し方がかなり違ってきます。
#所感
コントラクト毎のクラスを作るのも直感的にコードを組めて個人的には好きなのですが、このやり方だとコントラクト呼び出し、値の取得を行う仕組みを作っておけば、かなり汎用的にコードを組めそうです。