0%

Java×以太坊 3使用EIP 1559发送ETH交易

问题

磨刀不误砍柴工。

但是,在磨刀的过程中,总会遇到一些问题。比如,一个简单的发送ETH代币(你可以理解为GAS费代币),在Goerli上可以正常转账,在mumbai上却失败了。

错误如下:

Error processing transaction request: only replay-protected (EIP-155) transactions allowed over RPC

如果你去谷歌搜索该错误,会得到一些回复,但是他们要么不是使用Java构建的,要么是同样没有解决问题。

我去询问AI,因为没有付费,所以还是GPT3.5,他和我说换一个写法(然后换了数次还是失败了)。

最后,我从其中一个看起来比较有希望的写法入手,查看了源码才构建成功的。

环境

Maven依赖

我创建了Mavne项目,pom.xml中导如了以下依赖:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- web3J依赖 -->
<dependency>
<groupId>org.web3j</groupId>
<artifactId>core</artifactId>
<version>4.8.7</version>
</dependency>
<!-- web3J合约依赖 -->
<dependency>
<groupId>org.web3j</groupId>
<artifactId>contracts</artifactId>
<version>4.8.7</version>
</dependency>

代码

没有什么比直接上代码更有说服力的了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package tx1559;
/**
* @author HMB-XS
* @date 2023年11月24日14:53:42
**/
import org.web3j.crypto.Credentials;
import org.web3j.crypto.WalletUtils;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.methods.response.EthSendTransaction;
import org.web3j.protocol.http.HttpService;
import org.web3j.tx.RawTransactionManager;
import org.web3j.tx.gas.DefaultGasProvider;
import org.web3j.utils.Convert;
import java.math.BigInteger;
public class EIP1559TransactionMATIC {

public static void main(String[] args) throws Exception {
// 初始化web3j客户端
Web3j web3 = Web3j.build(new HttpService("https://polygon-mumbai.infura.io/v3/72cc4dbd0f0749089cb4da266cf833bf"));
Credentials credentials = Credentials.create("你的私钥");
// 填入目标地址 给自己发送
String toAddress = "目标地址";
// 交易金额
BigInteger amountInWei = Convert.toWei("0.001", Convert.Unit.ETHER).toBigInteger();
// 获取当前网络的建议费用
BigInteger maxPriorityFeePerGas = web3.ethGasPrice().send().getGasPrice();
// 为了提高成功打包的可能性,可以在建议费用的基础上稍微提高一点
// 在建议费用的基础上增加1 Gwei
BigInteger maxFeePerGas = maxPriorityFeePerGas.add(BigInteger.valueOf(1000000000));
// 构建交易对象
EthSendTransaction ethSendTransaction = new RawTransactionManager(
web3, credentials)
.sendEIP1559Transaction(
80001L,
maxPriorityFeePerGas, // 优先级
maxFeePerGas, // 最大费用
DefaultGasProvider.GAS_LIMIT,
toAddress,
"",
amountInWei
);
String transactionHash = ethSendTransaction.getTransactionHash();
// 交易哈希
System.out.println(transactionHash);
}
}

最后,他会打印成功的交易哈希。

交易提交成功还需要等待被矿工打包才算完成交易。

在上面的代码中,我们创建了一个类RawTransactionManager()..sendEIP1559Transaction()来完成交易。

前面的参数只有两个,web3和credentials。

后面的参数有多种实现,我尝试了其中的一种,更多的可以根据形参和说明来完成。

除了目标地址和金额外,我们还需要额外的指定链ID、以及GAS费的参数。

在询问够GPT后才写出了什么的代码,虽然不完美,但我感觉已经很好了。

不同的链要切换不同的链ID,比如Goerli的链ID为5,其他的自行搜索。

Java的好处就在于虽然我不知道你是怎么做的,但是给我源码就能运行。

安全的钱包

创建方式

其实官方文档有说的,但是文档那玩意,一般人还真没谁看。毕竟问AI不香吗,现在我觉得真不香,之前看文档就好了。

钱包文件

因为我是直接问AI的,所以就跳过,但是还是建议看看。早看了就没有上面的问题了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package wallet;
import org.web3j.crypto.WalletUtils;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* @author HMB-XS
* @date 2023年11月24日15:28:23
* 该类创建钱包,并且保存在资源目录wallet下面,钱包格式json
**/
public class CreateWalletFileExample {

public static void main(String[] args) throws Exception {
String password = "123456789"; // 设置密码
String destination = "src/main/resources/wallet"; // 设置保存文件的路径
// 创建目标文件夹(如果不存在)
Path directory = Paths.get(destination);
Files.createDirectories(directory);
// 生成新的钱包文件并返回文件名
String fileName = WalletUtils.generateFullNewWalletFile(password, directory.toFile());
// 打印结果
System.out.println("Wallet file created: " + fileName);
}
}

在项目的Resources目录wallet下面会生成json的钱包文件。

默认名称格式:

UTC–2023-11-24T15-36-52.251000000Z–0d85fdfa3ed8df5c137045074804105312616ab4.json

为了在项目更加方便的引用,你可以进行重命名,比如我重命名为test.json。

加载钱包

我们可以使用JSON文件加载钱包,这样比直接在代码里面上私钥要安全的多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package wallet;

/**
* @author HMB-XS
* @date 2023年11月24日15:39:48
**/
import org.web3j.crypto.Credentials;
import org.web3j.crypto.WalletUtils;

public class LoadCredentialsExample {

public static void main(String[] args) throws Exception {
String password = "123456789"; // 钱包密码
String keyFilePath = "src/main/resources/wallet/test.json"; // 钱包文件路径

// 使用钱包文件和密码加载凭证对象
Credentials credentials = WalletUtils.loadCredentials(password, keyFilePath);

System.out.println("Address: " + credentials.getAddress());
}
}

交易发送

我们可以在最前面的代码里面将Credentials是生成方法来交易。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package tx1559;

/**
* @author HMB-XS
* @date 2023年11月24日14:53:42
**/
import org.web3j.crypto.Credentials;
import org.web3j.crypto.WalletUtils;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.methods.response.EthSendTransaction;
import org.web3j.protocol.http.HttpService;
import org.web3j.tx.RawTransactionManager;
import org.web3j.tx.gas.DefaultGasProvider;
import org.web3j.utils.Convert;
import java.math.BigInteger;

public class EIP1559TransactionExample {

public static void main(String[] args) throws Exception {
// 初始化web3j客户端
Web3j web3 = Web3j.build(new HttpService("https://goerli.infura.io/v3/bc809e6905c34cb1b7e3536a0934ce06"));
// 使用JSON文件来加载钱包
String password = "123456789"; // 钱包密码
String keyFilePath = "src/main/resources/wallet/test.json"; // 钱包文件路径
// 使用钱包文件和密码加载凭证对象
Credentials credentials = WalletUtils.loadCredentials(password, keyFilePath);
// 填入目标地址 给自己发送
String toAddress = "0xefbc3db9875083cc1d00dad3fe9c8989b2d8d9bb";
// 交易金额
BigInteger amountInWei = Convert.toWei("0.001", Convert.Unit.ETHER).toBigInteger();
// 获取当前网络的建议费用
BigInteger maxPriorityFeePerGas = web3.ethGasPrice().send().getGasPrice();
// 为了提高成功打包的可能性,可以在建议费用的基础上稍微提高一点
// 在建议费用的基础上增加1 Gwei
BigInteger maxFeePerGas = maxPriorityFeePerGas.add(BigInteger.valueOf(1000000000));
// 构建交易对象
EthSendTransaction ethSendTransaction = new RawTransactionManager(
web3, credentials)
.sendEIP1559Transaction(
5L,
maxPriorityFeePerGas, // 优先级
maxFeePerGas, // 最大费用
DefaultGasProvider.GAS_LIMIT,
toAddress,
"",
amountInWei
);
String transactionHash = ethSendTransaction.getTransactionHash();
// 交易哈希
System.out.println(transactionHash);
}
}

上面的代码中,我们演示了Goerli链的EIP1559交易。

这样的代码安全了很多。

老式钱包生成器

我做了点改进,不在控制台输出,而是保存在txt文本中。

为了方便取用,我分别命名了三个文件:key.txt、address.txt、keyaddress.txt

这样就可以直接调用而不做转换了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package wallet;

import org.web3j.crypto.ECKeyPair;
import org.web3j.crypto.Keys;
import org.web3j.utils.Numeric;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.math.BigInteger;
import java.security.SecureRandom;

/**
* @author HMB-XS
* @date 2023年07月16日21:41:51
**/
public class NewWallet {
public static void main(String[] args) {
int num = 10; // 这里输入要生成的数量
try {
// 创建文件写入器
BufferedWriter addressWriter = new BufferedWriter(new FileWriter("src/main/resources/address.txt"));
BufferedWriter keyAddressWriter = new BufferedWriter(new FileWriter("src/main/resources/keyaddress.txt"));
BufferedWriter keyWriter = new BufferedWriter(new FileWriter("src/main/resources/key.txt"));

for (int i = 0; i < num; i++) {
// 生成随机的私钥
SecureRandom secureRandom = new SecureRandom();
byte[] privateKeyBytes = new byte[32];
secureRandom.nextBytes(privateKeyBytes);
String privateKey = Numeric.toHexStringNoPrefix(privateKeyBytes);
// 通过私钥生成公钥和地址
ECKeyPair keyPair = ECKeyPair.create(new BigInteger(privateKey, 16));
String address = Keys.getAddress(keyPair.getPublicKey());
// 输出私钥、公钥和地址
// System.out.println("第" + (i + 1) + "个钱包:");
// System.out.println("0x" + address);
// System.out.println(privateKey);

// 将地址和私钥写入文件
addressWriter.write("0x" + address);
addressWriter.newLine();
keyWriter.write(privateKey);
keyWriter.newLine();
keyAddressWriter.write("第" + (i+1) +"个钱包:");
keyAddressWriter.newLine();
keyAddressWriter.write("0x" + address);
keyAddressWriter.newLine();
keyAddressWriter.write(privateKey);
keyAddressWriter.newLine();
}

// 关闭文件写入器
keyWriter.close();
addressWriter.close();
keyAddressWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

生成后的文件将显示这样。

例如我生成三个作为测试:

key.txt

1
2
3
2a3d084065b790fbbf6eb451947e9e77a7fc9222b3738d308bebfb429d8a2bfc
51484a337537de28eefbb8df693e9917440e489f0f42514178269100e39633fa
7cce608c7a2c60d05ae7d08687504893bab15d60dca9625effecffd1dba993c7

addre.txt

1
2
3
0x33244fdcab048bd249393c03d569235ea0823e3a
0x4ce99a807c4f254ec4743f42aa505a64949d292a
0xe64dad11e301a4fe1deab9740bbb0b41a77a5e52

keyaddress.txt

1
2
3
4
5
6
7
8
9
第1个钱包:
0x33244fdcab048bd249393c03d569235ea0823e3a
2a3d084065b790fbbf6eb451947e9e77a7fc9222b3738d308bebfb429d8a2bfc
第2个钱包:
0x4ce99a807c4f254ec4743f42aa505a64949d292a
51484a337537de28eefbb8df693e9917440e489f0f42514178269100e39633fa
第3个钱包:
0xe64dad11e301a4fe1deab9740bbb0b41a77a5e52
7cce608c7a2c60d05ae7d08687504893bab15d60dca9625effecffd1dba993c7

这样,如果我想批量读取钱包地址,就直接读取addre.txt文件好了。

我让AI帮我写好了代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package test;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
/**
* @author HMB-XS
* @date 2023年11月23日20:15:05
**/
public class ReadTxt {
public static void main(String[] args) {
// AI写就是省时间,技术革命
String filePath = "src/main/resources/address.txt";
try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

这就非常棒了,稍微改进下就可以批量发送交易了。

如果你问为什么不直接使用合约,因为要链上交互记录。