ERC20 为 Ethereum 上的 Token 合约标准规范,遵守该规范的 Token 合约可以被各种以太坊钱包、以及相关的平台和项目支持,如在 etherscan 上可以查看遵守 ERC20 规范的 Token 信息和交易记录。
如下为 ERC20 Token 标准接口:
// ----------------------------------------------------------------------------
// 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);
}
其中各个接口方法:
如下两个为 event,会产生对应的 event log:
支持在以太坊上编写智能合约的语言主要有 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 的使用。
具体的合约代码如下:
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();
}
}
上述代码支持基本的合约转账和合约代理转账功能,主要围绕两个数据成员 balances 和 allowed 来实现,其中:
因为我们希望能够通过在 Java 代码中实现合约的部署和转账等功能,因为我们需要将合约转换成 Java 代码,这里采用 web3j 来转换。
web3j 转换需要提供合约的 .abi 和 .bin 文件,我们先用 solc 编译器来生成 sot.abi 和 sot.bin 文件,如下命令:
➜ 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 文件:
➜ 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 规范的那些方法,代码摘略如下:
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 的 WalletUtils.generateLightNewWalletFile 来创建,如下:
/// 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 的账号地址:
具体的申请内容和方式请参考 How to get on Rinkeby Testnet in less than 10 minutes 的 Step 4。
申请成功之后,我们的钱包账号中就可以查到余额了,如下为在 etherscan rinkeby testnet 中查看到的账号信息:
其中 Transactions Tab 中第一条交易记录就是在 faucet 申请的 ether 的账号。
好了,ether 来了,开始使用 web3j 部署 SOT 合约,如下代码:
SOT contract = SOT.deploy(
web3, finalCredentials,
ManagedTransaction.GAS_PRICE,
Contract.GAS_LIMIT).send();
String contractAddress = contract.getContractAddress();
其中 web3 为使用 Web3jFactory.build 构建的实例,如下代码为连接到 rinkeby 测试网络:
final Web3j web3 = Web3jFactory.build(
new HttpService("https://rinkeby.infura.io/xxxxsyt4bzKIGsctxxxx"));
finalCredentials为通过 WalletUtils.loadCredentials 从本地 keystore 加载的凭证,如下代码:
/// 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 的很多信息,包括如下几个关键信息:
注:上图中的 Token Transfers 中的记录是在下一步(合约转账)中完成之后出现的。
我是 SOT token 的创建者,我给自己发行了 31415926 个 token,下面给好基友转点过去。继续使用 web3j 如下代码:
// 调用 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 状态,如下图:
在交易最终被确认,并被区块打包之后,如下图: