使用 Web3J 发个以太坊 ERC20 Token

关于 ERC20 规范

ERC20 为 Ethereum 上的 Token 合约标准规范,遵守该规范的 Token 合约可以被各种以太坊钱包、以及相关的平台和项目支持,如在 etherscan 上可以查看遵守 ERC20 规范的 Token 信息和交易记录。

如下为 ERC20 Token 标准接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 // ----------------------------------------------------------------------------
// ERC20 Token Standard Interface
// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20-token-standard.md
// ----------------------------------------------------------------------------
contract ERC20 {
function name() constant returns (string name)
function symbol() constant returns (string symbol)
function decimals() constant returns (uint8 decimals)
function totalSupply() constant returns (uint totalSupply);
function balanceOf(address _owner) constant returns (uint balance);
function transfer(address _to, uint _value) returns (bool success);
function transferFrom(address _from, address _to, uint _value) returns (bool success);
function approve(address _spender, uint _value) returns (bool success);
function allowance(address _owner, address _spender) constant returns (uint remaining);
event Transfer(address indexed _from, address indexed _to, uint _value);
event Approval(address indexed _owner, address indexed _spender, uint _value);
}

其中各个接口方法:

  • name:返回 ERC20 token 的名称,如:Steven Ocean Token;
  • symbol:返回 ERC20 token 的简称,如:SOT;
  • decimals:返回 token 使用小数点的后几位,如设置为3,表示支持 0.001;
  • totalSupply:总的 token 供应量;
  • balanceOf:获取指定账户(address)的余额(token 拥有量);
  • transfer:token 转账,从 token 合约的调用者账户(address)上转移(输入参数)_value 的 token 数到指定的账户(address) _to 上,并且触发 Transfer 事件;
  • transferFrom:从账户地址 _from 发送数量为 _value 的 token 到账户地址 _to,且触发Transfer 事件;transferFrom 方法用于允许 Contract 代理某个账号来转移 token,条件是from 账户必须经过了 approve;
  • approve:允许 _spender 多次提取发起账户中最多为 _value 个 token,如果再次调用此函数,它将以本次 _value 覆盖当前的余量;
  • allowance:返回仍然允许 _spender 从 _owner 账户中提取的 token 余量;

如下两个为 event,会产生对应的 event log:

  • Transfer:该事件是在 token 发生转移时触发的,包括 0 个 token 转移也会触发;
  • Approve:该事件是在成功调用 approve 方法后触发;

使用 Solidity 编写 ERC20 Token

支持在以太坊上编写智能合约的语言主要有 Solidity、Serpent、LLL 和 Mutan,其中 Solidity 语法类似 Javascript,也是目前最主要的语言,Solidity 是静态类型的面向对象的编程语言,用其编写的智能合约需要通过 solc 编译器或者 IDE 环境编译成 EVM 字节码格式才能在 EVM 中执行。当编译好的合约发送到以太坊网络之后,就可以通过 web3.js 或者 web3.js API 来调用了,从而构建一个与之交互的应用。

我们下面使用在线 IDE Remix 来编写我们的智能合约:简单的发行 31415926 个 SOT tokens,支持简单的合约转账功能。

Remix 是以太坊官方推荐和维护的 IDE 环境,支持浏览器在线开发、调试、编译,也支持本地部署该 IDE 环境。

下图为使用 Remix 开发 SOT 合约的环境:

在 Remix 的 Editor 编辑器主要(通过 tab)显示了正在打开的一个或多个合约源码文件,Remix 也会自动编译合约代码,如果有编译错误,会在左边的 compile tab 页面显示。

remix 的官方文档 详细介绍了 remix 的使用。

具体的合约代码如下:

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
62
63
64
65
66
67
68
69
70
71
pragma solidity 0.4.21;

contract Token {
uint256 public totalSupply;
function balanceOf(address _owner) public constant returns (uint256 balance);
function transfer(address _to, uint256 _value) public returns (bool success);
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success);
function approve(address _spender, uint256 _value) public returns (bool success);
function allowance(address _owner, address _spender) public constant returns (uint256 remaining);
event Transfer(address indexed _from, address indexed _to, uint256 _value);
event Approval(address indexed _owner, address indexed _spender, uint256 _value);
}

contract StandardToken is Token {
function transfer(address _to, uint256 _value) public returns (bool success) {
if (balances[msg.sender] >= _value && _value > 0) {
balances[msg.sender] -= _value;
balances[_to] += _value;
emit Transfer(msg.sender, _to, _value);
return true;
} else {
return false;
}
}

function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
if (balances[_to] + _value < balances[_to]) revert(); // Check for overflows
if (balances[_from] >= _value && allowed[_from][msg.sender] >= _value && _value > 0) {
balances[_to] += _value;
balances[_from] -= _value;
allowed[_from][msg.sender] -= _value;
emit Transfer(_from, _to, _value);
return true;
} else {
return false;
}
}

function balanceOf(address _owner) public constant returns (uint256 balance) {
return balances[_owner];
}

function approve(address _spender, uint256 _value) public returns (bool success) {
allowed[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}

function allowance(address _owner, address _spender) public constant returns (uint256 remaining) {
return allowed[_owner][_spender];
}

mapping (address => uint256) balances;
mapping (address => mapping (address => uint256)) allowed;
}

contract SOT is StandardToken {
string public constant name = "Steven Ocean Token";
string public constant symbol = "SOT";
uint256 public constant decimals = 18;
uint256 public constant total = 31415926 * 10**decimals;

function SOT() public {
totalSupply = total;
balances[msg.sender] = total;
}

function () public payable {
revert();
}
}

上述代码支持基本的合约转账和合约代理转账功能,主要围绕两个数据成员 balancesallowed 来实现,其中:

  • balance:主要使用 map 结构缓存账号的 token 余额;
  • allowed:采用二级 map 结构缓存 owner(一级 map key)授权给哪些 spender(二级 map key)可提取的 token 数 value;

编译和编码合约为 Java 代码

因为我们希望能够通过在 Java 代码中实现合约的部署和转账等功能,因为我们需要将合约转换成 Java 代码,这里采用 web3j 来转换。

web3j 转换需要提供合约的 .abi 和 .bin 文件,我们先用 solc 编译器来生成 sot.abi 和 sot.bin 文件,如下命令:

1
2
3
4
5
6
7
8
9
10
11
➜  sot solc sot.sol --abi --bin --optimize -o ./
➜ sot ll
total 48
-rw-r--r-- 1 steven staff 2.4K 4 7 16:30 SOT.abi
-rw-r--r-- 1 steven staff 2.9K 4 7 16:30 SOT.bin
-rw-r--r-- 1 steven staff 1.7K 4 7 16:30 StandardToken.abi
-rw-r--r-- 1 steven staff 2.1K 4 7 16:30 StandardToken.bin
-rw-r--r-- 1 steven staff 1.7K 4 7 16:30 Token.abi
-rw-r--r-- 1 steven staff 0B 4 7 16:30 Token.bin
-rw-r--r--@ 1 steven staff 2.6K 4 7 16:29 sot.sol
➜ sot

可以看到针对每个 contract 类都生成了对应的 .abi 和 .bin 文件。

Remix IDE 中也可以通过查看 details 来获取对应的 abi 和 bin 文件。如下图:

接下来使用 web3j 将合约转换为 Java 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
➜  sot web3j solidity generate --javaTypes SOT.bin SOT.abi -o ./src/main/java -p io.github.stevenocean.contracts

_ _____ _ _
| | |____ (_) (_)
__ _____| |__ / /_ _ ___
\ \ /\ / / _ \ '_ \ \ \ | | | / _ \
\ V V / __/ |_) |.___/ / | _ | || (_) |
\_/\_/ \___|_.__/ \____/| |(_)|_| \___/
_/ |
|__/

Generating io.github.stevenocean.contracts.SOT ... File written to ./src/main/java

➜ sot tree src/main/java
src/main/java
└── io
└── github
└── stevenocean
└── contracts
└── SOT.java

4 directories, 1 file
➜ sot

生成的 SOT.java 文件中生成了一个派生于 Contract 类的 SOT 合约类,该类中实现了 ERC20 token 规范的那些方法,代码摘略如下:

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
public class SOT extends Contract {
private static final String BINARY = "606060......10029";

protected SOT(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) {
super(BINARY, contractAddress, web3j, credentials, gasPrice, gasLimit);
}

public RemoteCall<BigInteger> totalSupply() {
final Function function = new Function("totalSupply",
Arrays.<Type>asList(),
Arrays.<TypeReference<?>>asList(new TypeReference<Uint256>() {}));
return executeRemoteCallSingleValueReturn(function, BigInteger.class);
}

public RemoteCall<BigInteger> total() {
final Function function = new Function("total",
Arrays.<Type>asList(),
Arrays.<TypeReference<?>>asList(new TypeReference<Uint256>() {}));
return executeRemoteCallSingleValueReturn(function, BigInteger.class);
}

public RemoteCall<BigInteger> balanceOf(String _owner) {
final Function function = new Function("balanceOf",
Arrays.<Type>asList(new org.web3j.abi.datatypes.Address(_owner)),
Arrays.<TypeReference<?>>asList(new TypeReference<Uint256>() {}));
return executeRemoteCallSingleValueReturn(function, BigInteger.class);
}

public RemoteCall<TransactionReceipt> transfer(String _to, BigInteger _value) {
final Function function = new Function(
"transfer",
Arrays.<Type>asList(new org.web3j.abi.datatypes.Address(_to),
new org.web3j.abi.datatypes.generated.Uint256(_value)),
Collections.<TypeReference<?>>emptyList());
return executeRemoteCallTransaction(function);
}

public static RemoteCall<SOT> deploy(Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) {
return deployRemoteCall(SOT.class, web3j, credentials, gasPrice, gasLimit, BINARY, "");
}

public static SOT load(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) {
return new SOT(contractAddress, web3j, credentials, gasPrice, gasLimit);
}

// ...

public static class TransferEventResponse {
public Log log;
public String _from;
public String _to;
public BigInteger _value;
}

public static class ApprovalEventResponse {
public Log log;
public String _owner;
public String _spender;
public BigInteger _value;
}
}

后续可以将生成的 SOT.java 文件导入至 Java 项目中。

使用 Web3j 部署合约

部署合约之前先要有个自己的钱包账号的,这个账号可以用 web3j 的 WalletUtils.generateLightNewWalletFile 来创建,如下:

1
2
3
4
5
6
7
/// password: 提供一个生成 keystore 的密码
/// destinationDirectory: 存放 keystore 文件的路径
public static String generateLightNewWalletFile(String password, File destinationDirectory)
throws NoSuchAlgorithmException, NoSuchProviderException,
InvalidAlgorithmParameterException, CipherException, IOException {
return generateNewWalletFile(password, destinationDirectory, false);
}

刚创建好的钱包账号中的余额为 0,而部署合约是需要消耗一定的 ether 的,因此我们得先申请一点 ether,当然我们只能在测试环境下申请,在 rinkeby testnet 中,因为采用的是 PoA(clique) 共识机制,可以通过 faucet 提交如下三个支持的社交媒体的帖子URL,而对应的帖子内容中包括你需要申请 ether 的账号地址:

  • A public tweet on Twitter
  • A public Facebook post
  • A public Google+ link

具体的申请内容和方式请参考 How to get on Rinkeby Testnet in less than 10 minutes 的 Step 4。

申请成功之后,我们的钱包账号中就可以查到余额了,如下为在 etherscan rinkeby testnet 中查看到的账号信息:

其中 Transactions Tab 中第一条交易记录就是在 faucet 申请的 ether 的账号。

好了,ether 来了,开始使用 web3j 部署 SOT 合约,如下代码:

1
2
3
4
5
SOT contract = SOT.deploy(
web3, finalCredentials,
ManagedTransaction.GAS_PRICE,
Contract.GAS_LIMIT).send();
String contractAddress = contract.getContractAddress();

其中 web3 为使用 Web3jFactory.build 构建的实例,如下代码为连接到 rinkeby 测试网络:

1
2
final Web3j web3 = Web3jFactory.build(
new HttpService("https://rinkeby.infura.io/xxxxsyt4bzKIGsctxxxx"));

finalCredentials 为通过 WalletUtils.loadCredentials 从本地 keystore 加载的凭证,如下代码:

1
2
3
4
5
6
/// password: 为 keystore 密码
/// source: keystore 文件路径
public static Credentials loadCredentials(String password, String source)
throws IOException, CipherException {
return loadCredentials(password, new File(source));
}

contract.getContractAddress() 在部署成功 SOT 合约之后返回对应的合约地址。

部署成功之后,我们可以查看到对应的合约信息,如下图:

该合约信息页面中显示了 合约创建者(Contract Creator),ERC20 Token Contract 名称为 Steven Ocean Token(SOT),以及交易列表中显示了关联的首笔交易(To 显示的为 Contract Creation),交易信息 如下图:

在交易信息的 Input Data 中其实承载的是 SOT 合约的 BIN 代码。

另外,可以查看 SOT token 页面,如下图:

其中显示了 SOT token 的很多信息,包括如下几个关键信息:

  • Total Supply:token 总的供应量,这里为我们发行了 31415926 个 SOT;
  • ERC20 Contract:展示了 SOT token 对应的合约地址,即我们在上面看到的那个截图;
  • Token Holders:Token 持有者,下面的列表展示了持有 SOT 的账户地址列表;

注:上图中的 Token Transfers 中的记录是在下一步(合约转账)中完成之后出现的。

合约转账 - 给好基友转点币

我是 SOT token 的创建者,我给自己发行了 31415926 个 token,下面给好基友转点过去。继续使用 web3j 如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 调用 SOT.transfer 方法
Function function = new Function(
"transfer",
Arrays.<Type>asList(new Address(friendAddress),
new Uint256(new BigInteger("128000000000000000000"))),
Collections.<TypeReference<?>>emptyList());
String encodedFunction = FunctionEncoder.encode(function);

// 创建 tx 管理器,并通过 txManager 来发起合约转账
RawTransactionManager txManager = new RawTransactionManager(web3, finalCredentials);
EthSendTransaction transactionResponse = txManager.sendTransaction(
ManagedTransaction.GAS_PRICE, Contract.GAS_LIMIT,
contractAddress, encodedFunction, BigInteger.ZERO);

// 获取 TxHash
String transactionHash = transactionResponse.getTransactionHash();

调用成功后,会提交到以太坊网络中,在交易被确认之前,为 pending 状态,如下图:

在交易最终被确认,并被区块打包之后,如下图: