当前位置: 首页 > 工具软件 > web3j > 使用案例 >

Springboot+web3j(4.7)+实战+填坑

经伟
2023-12-01

实现功能:获取合约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 客户端

 类似资料: