问题开始:
在区块链钱包的开发中发现交易方法需要先签名才能进行交易(eth_sendTransaction)否则就需要对账户进行解锁.需要开发一个签名提供方法.通过查找资料发现有比较好的JS签名实现方法,但没有很好的java实现,所以想模仿其思路实现一个java版的.
开始设计:
该方法的类图
通过此图可以清晰的看出采用了静态代理的设计模式,新建类Web3jSignerProvider来重写了Web3jService接口的方法,实现了对Service类(既图中的HttpService类)中方法的增强.
而在Web3jSignerProvider中又调用了接口Signer的方法来实现对交易的签名,其中签名的具体实现在PricateKeySigner中实现.
最终实现了,使用者只要初始化Web3jSignerProvider对象就可以在调用正常的交易方法时在底层自动对其交易进行签名.
P.s.虽然这样对单一项目来说反而代码量增加 不如直接copy签名实现方法的代码 在项目中签名再交易 但之所以这样做的原因是:代码泛用性更强,既面对对象性更强.其他项目只要引用了该jar包既在grandle中配置一下就可以做到实现该方法而不用考虑其他东西.
开始实现:
首先实现签名的方法,先上代码:
org.ethereum.core.Transaction transactionObj = new org.ethereum.core.Transaction(nonceByte, gasPriceByte, gasLimitByte, receiveAddress, valueByte, dataByte);
transactionObj.sign(ECKey.fromPrivate(privateKey));
return "0x" + Hex.toHexString(transactionObj.getEncoded());
签名的核心实现,既重构一个Transaction对象并调用其sign()方法对其签名,并返回签名后的交易地址,这里有一个小'坑' 有签名方法的Transaction对象和在交易时使用的Transaction不是一个.所以需要将传入的Transaction的值全取出来并重构.
/**
* A transaction (formally, T) is a single cryptographically
* signed instruction sent by an actor external to Ethereum.
* An external actor can be a person (via a mobile device or desktop computer)
* or could be from a piece of automated software running on a server.
* There are two types of transactions: those which result in message calls
* and those which result in the creation of new contracts.
*/
public class Transaction //可以签名的Transaction对象,在注释中也说明了是一个单独加密的.可以对其进行签名指令.
想要调用签名方法需要对其初始化
public PrivateKeySigner(BigInteger privateKey) throws Exception {
if (privateKey == null) {
throw new Exception("privateKey is null");
}
this.privateKey = privateKey;
}
传入私钥进行签名,并进行入参校验
接下来完善签名方法,参数除了privateKey都从Transaction中取到,并进行入参校验与格式转换.又有一个小坑就是16进制数必须是偶数位,如果奇数位则在其前面补一个0.
@Override
public String sign(Transaction transaction) throws Exception {
String value;
String nonce;
String gasPrice;
String gasLimit;
String data;
String toAddress;
if (transaction.getValue().length() % 2 == 0) {
value = transaction.getValue().replace("0x", "");
} else {
value = transaction.getValue().replace("0x", "0");
}
if (transaction.getNonce().length() % 2 == 0) {
nonce = transaction.getNonce().replace("0x","");
} else {
nonce = transaction.getNonce().replace("0x","0");
}
if (transaction.getGasPrice().length() % 2 == 0) {
gasPrice = transaction.getGasPrice().replace("0x","");
} else {
gasPrice = transaction.getGasPrice().replace("0x","0");
}
if (transaction.getGas().length() % 2 == 0) {
gasLimit = transaction.getGas().replace("0x","");
} else {
gasLimit = transaction.getGas().replace("0x","0");
}
if (transaction.getData() == null) {
data = "";
} else if (transaction.getData().length() % 2 == 0) {
data = transaction.getData().replace("0x","");
} else {
data = transaction.getData().replace("0x","0");
}
if (transaction.getTo() == null) {
throw new Exception("receiveAddress is null");
} else if (transaction.getTo().length() % 2 == 0) {
toAddress = transaction.getTo().replace("0x","");
} else {
toAddress = transaction.getTo().replace("0x","0");
}
if (gasLimit == null) {
gasLimit = "21000"; //min
}
if (gasPrice == null) {
gasPrice = "5000000000"; //min
}
if (value == null) {
throw new Exception("Value is null");
}
byte[] receiveAddress = Hex.decode(toAddress.replace("0x", ""));
byte[] gasPriceByte = Hex.decode(gasPrice.replace("0x", ""));
byte[] gasLimitByte = Hex.decode(gasLimit.replace("0x", ""));
byte[] valueByte = Hex.decode(value);
byte[] dataByte = Hex.decode(data.replace("0x", ""));
byte[] nonceByte = Hex.decode(nonce.replace("0x", ""));
org.ethereum.core.Transaction transactionObj = new org.ethereum.core.Transaction(nonceByte, gasPriceByte, gasLimitByte, receiveAddress, valueByte, dataByte);
transactionObj.sign(ECKey.fromPrivate(privateKey));
return "0x" + Hex.toHexString(transactionObj.getEncoded());
}
至此完成了签名接口的实现,接下来就是分析源码,并代理Service方法,先上接口的代码.
public interface Web3jService {
<T extends Response> T send(
Request request, Class<T> responseType) throws IOException;
<T extends Response> CompletableFuture<T> sendAsync(
Request request, Class<T> responseType);
}
为什么重写这个类呢?因为首先通过JS的实现方法获取了目标,之后通过断点寻找发现
@Override
public <T extends Response> T send(
Request request, Class<T> responseType) throws IOException {
String payload = objectMapper.writeValueAsString(request);
try (InputStream result = performIO(payload)) {
if (result != null) {
return objectMapper.readValue(result, responseType);
} else {
return null;
}
}
}
这里Service的实现方法里,有传入request的方法 而request里包含了所有的交易信息 我只要在这里抓取我想要的方法,并重构request对象(交易信息)就可以实现签名,而且这个HttpService可以直接通过传入地址参数构造出来,我只要让使用者通过new我创建的代理对象而不是之前的HttpService(继承了Service类)就可以达到覆盖原方法的作用.
接下来就是要对其进行重写:
public class Web3jSignerProvider implements org.web3j.protocol.Web3jService{
private HttpService service;
private Signer signer;
public Web3jSignerProvider(String url, Signer signer) throws Exception {
if (url == null) {
throw new Exception("url is null");
}
if (signer == null) {
throw new Exception("Please initialization signer");
}
this.service = new HttpService(url);
this.signer = signer;
}
@Override
public <T extends Response> T send(Request request, Class<T> responseType) throws IOException {
if (request.getMethod().equals("eth_sendTransaction")) {
Transaction transactoin = (Transaction) request.getParams().get(0);
try {
//Reconstruction Requset For Find Nonce
Request requestCount = new Request<>(
"eth_getTransactionCount",
Arrays.asList(transactoin.getFrom(), DefaultBlockParameterName.LATEST.getValue()),
service,
EthGetTransactionCount.class);
EthGetTransactionCount count = (EthGetTransactionCount) requestCount.send();
BigInteger nonce = count.getTransactionCount();
//Reconstruction Transaction For Signer
Transaction signerTransaction = new Transaction(
transactoin.getFrom(),
nonce,
new BigInteger(transactoin.getGasPrice().replace("0x",""), 16),
new BigInteger(transactoin.getGas().replace("0x",""), 16),
transactoin.getTo(),
new BigInteger(transactoin.getValue().replace("0x",""), 16),
transactoin.getData()
);
//Signer
String newTrans = signer.sign(signerTransaction);
//Reconstruction Requset For Send
Request requestRaw = new Request<>(
"eth_sendRawTransaction",
Collections.singletonList(newTrans),
service,
EthSendTransaction.class);
//sendRawTransaction
return (T) requestRaw.send();
} catch (Exception e) {
e.printStackTrace();
}
return null;
} else {
return service.send(request, responseType);
}
}
@Override
public <T extends Response> CompletableFuture<T> sendAsync(Request request, Class<T> responseType) {
if (request.getMethod().equals("eth_sendTransaction")) {
return Async.run(() -> send(request, responseType));
} else {
return service.sendAsync(request, responseType);
}
}
}
因为要签名所以构造方法新加了Signer参数,之后就研究怎样可以重写传递的参数.
public Request(String method, 交易或查询调用的方法
List<S> params, 交易详细信息
Web3jService web3jService,
Class<T> type)
我只需要抓取正常交易的方法,之后调用签名方法替换掉签名后的params的内容和只能使用签名后的交易信息的签名方法就好了.这些信息的发现都可以在Debug中获取.
打包发布:
先修改markdown文档
之后完善grandle配置
uploadArchives {
repositories {
mavenDeployer {
repository(url: "http://...") {
authentication(userName: "...", password: "...")
}
snapshotRepository(url: "http://.../") {
authentication(userName: "...", password: "...")
}
pom.project {
name='cn.kuick.blockchain:web3j-sign-provider'
packaging='jar'
description='A simple web3 standard provider that signs sendTransaction payloads.'
}
}
}
}
artifacts {
archives jar
}
通过在grandle中添加这两个配置,并输入gradle uploadArchives命令来打包上传.
至此这个小模块就实现完毕 ~