对接BTC、ETH、USDT与EOS的中心化钱包设计

2020-03-21  本文已影响0人  XJ2017

背景

去年有几个与公链有关的外包项目对接咱们的中心化钱包,项目除传统项目的CRUD外还有个需求就是对接公链上的账户,实现创建账号、到账通知与代转。

业务流程

1584624328(1).png

简单描述:

问题

思路

设计

数据库设计
1584628276(1).png

表描述:

项目结构设计
1584628996(1).png
1584764549(1).png

模块描述:

API访问权限设计
1584858326(1).png
EOS创建管理账户的设计
1584858182.png
扩展web3j访问ETH官方区块信息的接口
Web3j web3j = Web3j.build(new HttpServiceEx("https://api.etherscan.io/api?apikey=xxx"));
import static okhttp3.ConnectionSpec.CLEARTEXT;

import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.util.Assert;
import org.web3j.protocol.core.Request;
import org.web3j.protocol.core.Response;
import org.web3j.protocol.core.methods.request.Transaction;
import org.web3j.protocol.exceptions.ClientConnectionException;
import org.web3j.protocol.http.HttpService;

import com.ddblock.centwallet.engine.exception.BusinessException;
import com.ddblock.centwallet.engine.exception.SystemException;
import com.ddblock.centwallet.engine.util.JSONUtil;
import com.ddblock.centwallet.eth.exception.NonceException;

import okhttp3.*;
import okhttp3.logging.HttpLoggingInterceptor;

/**
 * @author XiaoJia
 * @since 2019-05-24 9:22
 */
public class HttpServiceEx extends HttpService {
    private static final Logger LOGGER = LogManager.getLogger(HttpServiceEx.class);
    /**
     * 发送ETH裸交易时出现nonce重复的异常消息
     */
    private static final String NONCE_ERROR_MESSAGE = "replacement transaction underpriced";

    /**
     * 发送ETH裸交易时出现nonce重复的异常消息(转出的账号与值是一样的情况下)
     */
    private static final String NONCE_ERROR_MESSAGE2 = "^known transaction: (\\S){64}";

    // ------------------------ 基类代码 begin --------------------------

    private static final CipherSuite[] INFURA_CIPHER_SUITES = new CipherSuite[] {
        CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
        CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
        CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
        CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,

        // Note that the following cipher suites are all on HTTP/2's bad cipher suites list. We'll
        // continue to include them until better suites are commonly available. For example, none
        // of the better cipher suites listed above shipped with Android 4.4 or Java 7.
        CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
        CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
        CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256, CipherSuite.TLS_RSA_WITH_AES_256_GCM_SHA384,
        CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA,
        CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA,

        // Additional INFURA CipherSuites
        CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384,
        CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA256, CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA256};

    private static final ConnectionSpec INFURA_CIPHER_SUITE_SPEC =
        new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS).cipherSuites(INFURA_CIPHER_SUITES).build();
    /**
     * The list of {@link ConnectionSpec} instances used by the connection.
     */
    private static final List<ConnectionSpec> CONNECTION_SPEC_LIST = Arrays.asList(INFURA_CIPHER_SUITE_SPEC, CLEARTEXT);

    // ------------------------ 基类代码 end --------------------------

    private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

    private final String url;
    private final OkHttpClient httpClient;

    public HttpServiceEx(String url) {
        this(url, createOkHttpClient());
    }

    private HttpServiceEx(String url, OkHttpClient httpClient) {
        super(url, httpClient);
        this.url = url;
        this.httpClient = httpClient;
    }

    private static OkHttpClient createOkHttpClient() {
        final OkHttpClient.Builder builder = new OkHttpClient.Builder().connectionSpecs(CONNECTION_SPEC_LIST);
        // 添加超时时间
        builder.readTimeout(30, TimeUnit.SECONDS);
        configureLogging(builder);
        return builder.build();
    }

    private static void configureLogging(OkHttpClient.Builder builder) {
        if (LOGGER.isDebugEnabled()) {
            HttpLoggingInterceptor logging = new HttpLoggingInterceptor(LOGGER::debug);
            logging.setLevel(HttpLoggingInterceptor.Level.BODY);
            builder.addInterceptor(logging);
        }
    }

    @Override
    public <T extends Response> T send(Request request, Class<T> responseType) throws IOException {
        // 通过request对象,生成Get请求参数
        THREAD_LOCAL.set(generateRequestUrl(request));
        try {
            T response = super.send(request, responseType);
            if (response.hasError()) {
                String requestMethod = request.getMethod();
                String requestParams = JSONUtil.toJSONString(request.getParams());
                Integer errorCode = response.getError().getCode();
                String errorMessage = response.getError().getMessage();

                if (NONCE_ERROR_MESSAGE.equals(errorMessage) || errorMessage.matches(NONCE_ERROR_MESSAGE2)) {
                    throw new NonceException("执行方法[%s]参数[%s]失败!错误码[%s]错误信息[%s]", requestMethod, requestParams,
                        errorCode, errorMessage);
                } else {
                    throw new BusinessException("执行方法[%s]参数[%s]失败!错误码[%s]错误信息[%s]", requestMethod, requestParams,
                        errorCode, errorMessage);
                }
            }
            return response;
        } finally {
            THREAD_LOCAL.remove();
        }
    }

    @Override
    protected InputStream performIO(String request) throws IOException {
        String requestUrl = THREAD_LOCAL.get();

        // RequestBody requestBody = RequestBody.create(JSON_MEDIA_TYPE, request);
        // Headers headers = buildHeaders();
        Headers headers = Headers.of(super.getHeaders());

        // okhttp3.Request httpRequest = new
        // okhttp3.Request.Builder().url(url).headers(headers).post(requestBody).build();
        okhttp3.Request httpRequest = new okhttp3.Request.Builder().url(requestUrl).headers(headers).get().build();

        okhttp3.Response response = httpClient.newCall(httpRequest).execute();
        ResponseBody responseBody = response.body();
        if (response.isSuccessful()) {
            if (responseBody != null) {
                // return buildInputStream(responseBody);
                return responseBody.byteStream();
            } else {
                return null;
            }
        } else {
            int code = response.code();
            String text = responseBody == null ? "N/A" : responseBody.string();

            throw new ClientConnectionException("Invalid response received: " + code + "; " + text);
        }
    }

    /**
     * 生成请求URL
     *
     * @param request
     *            请求对象
     *
     * @return 请求RUL
     */
    private String generateRequestUrl(Request request) {
        String method = request.getMethod();
        Object[] paramValues = request.getParams().toArray();

        StringBuilder sb = new StringBuilder();
        sb.append(url).append("&module=proxy&action=").append(method);

        switch (method) {
            case "eth_blockNumber":
                // 无参数
                break;
            case "eth_getBlockByNumber":
                sb.append(generateParamUrl(new String[] {"tag", "boolean"}, paramValues));
                break;
            case "eth_getUncleByBlockNumberAndIndex":
                sb.append(generateParamUrl(new String[] {"tag", "boolean"}, paramValues));
                break;
            case "eth_getBlockTransactionCountByNumber":
                sb.append(generateParamUrl(new String[] {"tag"}, paramValues));
                break;
            case "eth_getTransactionByHash":
                sb.append(generateParamUrl(new String[] {"data"}, paramValues));
                break;
            case "eth_getTransactionByBlockNumberAndIndex":
                sb.append(generateParamUrl(new String[] {"tag", "index"}, paramValues));
                break;
            case "eth_getTransactionCount":
                sb.append(generateParamUrl(new String[] {"address", "tag"}, paramValues));
                break;
            case "eth_sendRawTransaction":
                sb.append(generateParamUrl(new String[] {"hex"}, paramValues));
                break;
            case "eth_getTransactionReceipt":
                sb.append(generateParamUrl(new String[] {"txhash"}, paramValues));
                break;
            case "eth_call":
                // 兼容直接传递交易的情况
                Object[] param = new Object[3];
                if (paramValues.length == 2 && paramValues[0] instanceof Transaction) {
                    Transaction trans = (Transaction)paramValues[0];
                    param[0] = trans.getTo();
                    param[1] = trans.getData();
                    param[2] = paramValues[1];
                } else {
                    param = paramValues;
                }
                sb.append(generateParamUrl(new String[] {"to", "data", "tag"}, param));
                break;
            case "eth_getCode":
                sb.append(generateParamUrl(new String[] {"address", "tag"}, paramValues));
                break;
            case "eth_getStorageAt":
                sb.append(generateParamUrl(new String[] {"address", "position", "tag"}, paramValues));
                break;
            case "eth_gasPrice":
                // 无参数
                break;
            case "eth_estimateGas":
                sb.append(generateParamUrl(new String[] {"to", "value", "gasPrice", "gas"}, paramValues));
                break;
            default:
                throw new SystemException("以太坊区块浏览器暂不支持请求类型[%s]的访问!", method);
        }

        return sb.toString();
    }

    /**
     * 生成参数请求URL
     * 
     * @param paramNames
     *            参数名称集合
     * @param paramValues
     *            参数值集合
     *
     * @return 参数请求URL
     */
    private String generateParamUrl(String[] paramNames, Object[] paramValues) {
        Assert.isTrue(paramNames.length == paramValues.length, "请求参数个数与区块浏览器的参数个数不匹配");

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < paramNames.length; i++) {
            sb.append("&").append(paramNames[i]).append("=").append(paramValues[i]);
        }

        return sb.toString();
    }

}
上一篇 下一篇

猜你喜欢

热点阅读