实现功能:获取合约event数据(相当于日志)。
中文文档
目前我找的比较好的文档是 汇智网 的,java以太坊库web3j文档
搭建项目
Springboot版本
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
web3j依赖
<!--Java 操作智能合约 开始-->
<!--web3j-spring-boot-starter使用的web3j版本为3.x。本项目使用web3j的4.x版本-->
<!-- <dependency>-->
<!-- <groupId>org.web3j</groupId>-->
<!-- <artifactId>web3j-spring-boot-starter</artifactId>-->
<!-- <version>1.6.0</version>-->
<!-- </dependency>-->
<!-- https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-stdlib -->
<!--springboot 2.3.4 不需要,2.1.7 需要 -->
<!-- <dependency>-->
<!-- <groupId>org.jetbrains.kotlin</groupId>-->
<!-- <artifactId>kotlin-stdlib</artifactId>-->
<!-- <version>1.3.70</version>-->
<!-- </dependency>-->
<!--okhttp与logging-interceptor可根据项目实际情况选择是否添加-->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.3.1</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>logging-interceptor</artifactId>
<version>4.3.1</version>
</dependency>
<dependency>
<groupId>org.web3j</groupId>
<artifactId>core</artifactId>
<version>4.7.0</version>
</dependency>
<!--Java 操作智能合约 结束-->
fasterxml依赖
<!--这个依赖可根据项目实际情况选择是否添加-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.11.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.11.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.2</version>
</dependency>
web3j maven plugin
我们把合约文件(.sol)放在resources目录下,运行插件,即可生成合约对应的Java类。这个插件会根据你的合约版本自动下载对应的solidity编译器,真正实现一键生成合约java类,非常好用,老外就是牛皮。
<!--mvn web3j:generate-sources-->
<plugin>
<groupId>org.web3j</groupId>
<artifactId>web3j-maven-plugin</artifactId>
<version>4.6.5</version>
<configuration>
<packageName>com.contract</packageName>
<sourceDestination>src/main/java/generated</sourceDestination>
<nativeJavaType>true</nativeJavaType>
<outputFormat>java,bin</outputFormat>
<soliditySourceFiles>
<directory>src/main/resources</directory>
<includes>
<include>**/*.sol</include>
</includes>
</soliditySourceFiles>
<outputDirectory>
<java>src/java/generated</java>
<bin>src/bin/generated</bin>
<abi>src/abi/generated</abi>
</outputDirectory>
<contract>
<!--<includes>-->
<!-- <include>Chip</include>-->
<!--</includes>-->
<!--<excludes>-->
<!-- <exclude>Hello</exclude>-->
<!-- <exclude>Chip</exclude>-->
<!--</excludes>-->
</contract>
<pathPrefixes>
<pathPrefix>dep=../dependencies</pathPrefix>
</pathPrefixes>
</configuration>
</plugin>
创建event过滤器
package com.fc.task.contract.config;
import com.fc.task.contract.contract.Chip;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.web3j.crypto.Credentials;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.DefaultBlockParameterName;
import org.web3j.protocol.core.methods.request.EthFilter;
import org.web3j.protocol.http.HttpService;
import org.web3j.tx.gas.DefaultGasProvider;
import java.io.IOException;
/**
* @author ydw
* @version 1.0
* @date 2020/11/6 10:26
*/
@Configuration
public class ContractConfigDemo {
/**
* 为Spring容器注入一个web3j实例。
* 方便我们后续用 @Autowired 调用web3j
* <p>
* web3j Infura 模块提供了一个Infura Http 客户端(InfuraHttpService),
* 它为Infura特定的Infura-Ethereum-Preferred-Client提供支持。
* 你可以指定geth或Parity客户端响应你的请求,
* 并且可以像普通的HTTPClient一样创建客户端。
* <p>
* 首先,你需要在 infura.io 注册并创建一个project,
* 得到你的 https://mainnet.infura.io/v3/xxxxxxxxxxxxxxxxxxxx (主网)
* or https://rinkeby.infura.io/v3/xxxxxxxxxxxxxxxxxxx (测试网)
*
* @return
*/
@Bean
public Web3j web3j() {
String ip = "https://mainnet.infura.io/v3/xxxxxxxxxxxxxxxxxxxx";
Web3j web3j = Web3j.build(new HttpService(ip));
return web3j;
}
/**
* @param chip 智能合约java类
* @param web3j web3j客户端
* @return
*/
@Bean(name = "inviteFilter") // 如果你有多个过滤器,你需要指定每个过滤器的名字
@Scope("prototype") // 你可能要同时监听多个事件,那么就不能使用同一个实例,因此这里需要每次都生成一个新的对象
public EthFilter ethFilter(Chip chip, Web3j web3j) throws IOException {
// 两种方式生成过滤器
// 方式一:通过智能合约java类
// // 获取当前区块链的区块
// BigInteger startBlockNum = web3j.ethBlockNumber().send().getBlockNumber();
// return new EthFilter(
// // 开始区块
// DefaultBlockParameter.valueOf(startBlockNum),
// // 直接设置开始区块为初始区块。当这样设置时,我们在后续监听event(智能合约的概念,相当于日志)事件时,会得到初始区块到最新区块之间的所有数据。
// // DefaultBlockParameterName.EARLIEST,
//
// // 结束区块。这里直接监听最后一个区块,即最新的区块
// DefaultBlockParameterName.LATEST,
//
// // 智能合约地址
// // 如果监听不到,这里的地址可以把 0x 去掉
// chip.getContractAddress()
// );
// 方式二:通过智能合约地址。这种方式不需要我们把智能合约转为java类,更加灵活。
String contractAddress = "";
String eventTopics = "";
return new EthFilter(
DefaultBlockParameterName.EARLIEST,
DefaultBlockParameterName.LATEST,
contractAddress
)
// 这里我在创建过滤器时,就直接指定eventTopics即指定我要监听的event,
// 也可以先不指定,例如方式一,在后续使用时再指定。
.addSingleTopic(eventTopics);
}
/**
* 往Spring容器注入智能合约Java类
*
* @param web3j
* @return
* @throws IOException
*/
@Bean
public Chip chip(Web3j web3j) throws IOException {
// 加载智能合约,线上智能合约与本地java类映射
Chip chip;
// 链上智能合约地址(部署智能合约后得到的地址)
String chipAddress = "";
// // 方式一:通过 org.web3j.tx.transactionManager
// String yourMetaMaskAddress = "";
// TransactionManager transactionManager = new ClientTransactionManager(web3j, yourMetaMaskAddress);
// Chip chip = Chip.load(chipAddress, web3j, transactionManager, new DefaultGasProvider());
// 方式二:通过 org.web3j.crypto.Credentials
// MetaMask(小狐狸,谷歌浏览器的一个插件,需要翻墙安装,方便进行智能合约交互) 私钥或其他类似于Metamask 产品的私钥
String privateKey = "";
Credentials credentials = Credentials.create(privateKey);
chip = Chip.load(chipAddress, web3j, credentials, new DefaultGasProvider());
return chip;
}
}
创建监听器
package com.fc.task.contract.listener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.methods.request.EthFilter;
/**
* @author ydw
* @version 1.0
* @date 2020/11/6 11:15
*/
@Component
public class ContractListenerDemo implements ApplicationRunner {
@Autowired
Web3j web3j;
@Autowired
@Qualifier("inviteFilter") // 你自己创建的过滤器
EthFilter inviteFilter;
@Override
public void run(ApplicationArguments args) throws Exception {
this.inviteFilterHandle();
}
private void inviteFilterHandle() {
// 当你的过滤器没有指定event topic时
// 方式一:使用智能合约Java类
// inviteFilter.addSingleTopic(EventEncoder.encode(Chip.ADDREWARDCHIP_EVENT));
// 方式二:从 etherscan.io 中获取
/**
* 合约被部署到以太坊后,可通过合约地址查询合约的信息,
* 其中就能看到event,而event中就有你想要的topic
* */
// String eventTopic = "";
// inviteFilter.addSingleTopic(eventTopic);
// 因为我的 inviteFilter 在创建时就已经指定了topic,所以我在这里就再指定。
web3j.ethLogFlowable(inviteFilter).subscribe(log->{
System.out.println("收到事件inviteFilter");
// 在合约中,当event的emit函数的参数被index修饰,这里表现为log中的topics,
// 否则它们将会出现在data中。
});
}
}
过滤器和监听器结合使即可完成监听合约event功能。
定时任务获取event数据
当我们使用监听器获取数据时,很有可能会漏数据,这时我们需要补救措施,我这里使用的是定时器5秒获取一次数据。
这里我们需要用到 https://etherscan.io/ 的api。首先你要去这个网站注册账号,得到自己的apikey。主网API说明点这里
package com.fc.service.mananger;
import com.alibaba.fastjson.JSONObject;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.net.URI;
import java.util.*;
/**
* @author ydw
* @version 1.0
* @date 2020/11/6 11:40
*/
@Component
public class EtherscanApiDemo {
public void getUserRelationship() {
String startBlockNumber = "";// 开始区块
String inviteContractAddress = "";// 合约地址
String apiKey = "";// etherscan.io 中你自己的apikey
String inviteContractTopic = ""; // 合约 event 的 topic
String url = "";
// url: "https://api-cn.etherscan.com/api"; 主网
// url: "https://api-rinkeby.etherscan.io/api" 测试网
Map<String, Object> parameters = new HashMap<>(7);
parameters.put("module", "logs");
parameters.put("action", "getLogs");
parameters.put("fromBlock", startBlockNumber);
parameters.put("toBlock", "latest");
parameters.put("address", inviteContractAddress);
parameters.put("topic0", inviteContractTopic);
parameters.put("apikey", apiKey);
String response = HttpUtil.doGet(url, parameters);
TransactionsResponse tr = JSONObject.parseObject(response, TransactionsResponse.class);
}
}
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* httpClient工具类
*
* @author heyi
*/
class HttpUtil {
private final static int NORMAL_STATUS = 200;
/**
* get请求处理
*
* @param url
* @param args
* @return
*/
public static String doGet(String url, Map<String, Object>... args) {
//创建默认的httpClient实例
CloseableHttpClient httpClient = HttpClients.createDefault();
//httpResponse响应对象
CloseableHttpResponse response = null;
//响应返回结果
String resultString = "";
try {
//创建uri
URIBuilder builder = new URIBuilder(url);
if (args.length > 0) {
args[0].forEach((k, v) -> builder.addParameter(k, String.valueOf(v)));
}
URI uri = builder.build();
// 创建http GET请求
HttpGet httpGet = new HttpGet(uri);
// 执行请求
response = httpClient.execute(httpGet);
// 判断返回状态是否为200
if (response != null && response.getStatusLine().getStatusCode() == NORMAL_STATUS) {
// 获取响应实体
HttpEntity httpEntity = response.getEntity();
resultString = EntityUtils.toString(httpEntity, "UTF-8");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (response != null) {
response.close();
}
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
/**
* post请求处理
*
* @param url
* @param args args[0] formdata args[1] header
* @return
*/
public static String doPost(String url, Map<String, Object>... args) {
//创建默认的httpClient实例
CloseableHttpClient httpClient = HttpClients.createDefault();
//httpResponse响应对象
CloseableHttpResponse response = null;
//响应返回结果
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
if (args.length > 1) {
args[1].forEach((k, v) -> httpPost.setHeader(k, String.valueOf(v)));
}
// 创建参数列表
if (args.length > 0) {
List<NameValuePair> formParams = new ArrayList<NameValuePair>();
args[0].forEach((k, v) -> formParams.add(new BasicNameValuePair(k, String.valueOf(v))));
// 模拟表单
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, "UTF-8");
httpPost.setEntity(entity);
}
//执行http请求
response = httpClient.execute(httpPost);
//获取响应结果
// 判断返回状态是否为200
if (response != null && response.getStatusLine().getStatusCode() == NORMAL_STATUS) {
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (response != null) {
response.close();
}
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
/**
* post请求处理
*
* @param url
* @return
*/
public static String doPostJson(String url, String json) {
//创建默认的httpClient实例
CloseableHttpClient httpClient = HttpClients.createDefault();
//httpResponse响应对象
CloseableHttpResponse response = null;
//响应返回结果
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建参数列表
httpPost.setHeader("Content-Type", "application/json;charset=UTF-8");
StringEntity se = new StringEntity(json, "UTF-8");
se.setContentType("application/json");
httpPost.setEntity(se);
//执行http请求
response = httpClient.execute(httpPost);
//获取响应结果
// 判断返回状态是否为200
if (response != null && response.getStatusLine().getStatusCode() == NORMAL_STATUS) {
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (response != null) {
response.close();
}
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
}
class TransactionsResponse {
private String status;
private String message;
private List<Transactions> result = new ArrayList<Transactions>();
public TransactionsResponse() {
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public List<Transactions> getResult() {
return result;
}
public void setResult(List<Transactions> result) {
this.result = result;
}
@Override
public String toString() {
return "TransactionsResponse [status=" + status + ", message=" + message + ", result=" + result + "]";
}
}
class Transactions {
private String blockNumber;
private String timeStamp;
private String hash;
private String nonce;
private String blockHash;
private String transactionIndex;
private String from;
private String to;
private String value;
private String gas;
private String gasPrice;
private String isError;
private String txreceipt_status;
private String input;
private String contractAddress;
private String cumulativeGasUsed;
private String gasUsed;
private String confirmations;
private String address;
private String[] topics;
private String data;
private String logIndex;
private String transactionHash;
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String[] getTopics() {
return topics;
}
public void setTopics(String[] topics) {
this.topics = topics;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public String getLogIndex() {
return logIndex;
}
public void setLogIndex(String logIndex) {
this.logIndex = logIndex;
}
public String getTransactionHash() {
return transactionHash;
}
public void setTransactionHash(String transactionHash) {
this.transactionHash = transactionHash;
}
public Transactions() {
}
public String getBlockNumber() {
return blockNumber;
}
public void setBlockNumber(String blockNumber) {
this.blockNumber = blockNumber;
}
public String getTimeStamp() {
return timeStamp;
}
public void setTimeStamp(String timeStamp) {
this.timeStamp = timeStamp;
}
public String getHash() {
return hash;
}
public void setHash(String hash) {
this.hash = hash;
}
public String getNonce() {
return nonce;
}
public void setNonce(String nonce) {
this.nonce = nonce;
}
public String getBlockHash() {
return blockHash;
}
public void setBlockHash(String blockHash) {
this.blockHash = blockHash;
}
public String getTransactionIndex() {
return transactionIndex;
}
public void setTransactionIndex(String transactionIndex) {
this.transactionIndex = transactionIndex;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getGas() {
return gas;
}
public void setGas(String gas) {
this.gas = gas;
}
public String getGasPrice() {
return gasPrice;
}
public void setGasPrice(String gasPrice) {
this.gasPrice = gasPrice;
}
public String getIsError() {
return isError;
}
public void setIsError(String isError) {
this.isError = isError;
}
public String getTxreceipt_status() {
return txreceipt_status;
}
public void setTxreceipt_status(String txreceipt_status) {
this.txreceipt_status = txreceipt_status;
}
public String getInput() {
return input;
}
public void setInput(String input) {
this.input = input;
}
public String getContractAddress() {
return contractAddress;
}
public void setContractAddress(String contractAddress) {
this.contractAddress = contractAddress;
}
public String getCumulativeGasUsed() {
return cumulativeGasUsed;
}
public void setCumulativeGasUsed(String cumulativeGasUsed) {
this.cumulativeGasUsed = cumulativeGasUsed;
}
public String getGasUsed() {
return gasUsed;
}
public void setGasUsed(String gasUsed) {
this.gasUsed = gasUsed;
}
public String getConfirmations() {
return confirmations;
}
public void setConfirmations(String confirmations) {
this.confirmations = confirmations;
}
@Override
public String toString() {
return "Transactions{" +
"blockNumber='" + blockNumber + '\'' +
", timeStamp='" + timeStamp + '\'' +
", hash='" + hash + '\'' +
", nonce='" + nonce + '\'' +
", blockHash='" + blockHash + '\'' +
", transactionIndex='" + transactionIndex + '\'' +
", from='" + from + '\'' +
", to='" + to + '\'' +
", value='" + value + '\'' +
", gas='" + gas + '\'' +
", gasPrice='" + gasPrice + '\'' +
", isError='" + isError + '\'' +
", txreceipt_status='" + txreceipt_status + '\'' +
", input='" + input + '\'' +
", contractAddress='" + contractAddress + '\'' +
", cumulativeGasUsed='" + cumulativeGasUsed + '\'' +
", gasUsed='" + gasUsed + '\'' +
", confirmations='" + confirmations + '\'' +
", address='" + address + '\'' +
", topics=" + Arrays.toString(topics) +
", data='" + data + '\'' +
", logIndex='" + logIndex + '\'' +
", transactionHash='" + transactionHash + '\'' +
'}';
}
}
本文有引用这篇文章 web3j 的 Infura Http 客户端