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 状态,如下图:
在交易最终被确认,并被区块打包之后,如下图: