以太坊发币(ERC20)姿势解读

作者: 胡键 | 来源:发表于2018-07-08 22:34 被阅读31次

这周,让我们跟随着一些简单的例子从近处看看以太坊DApp的开发细节。当然啦,本文也会涉及到以太坊中的一个热门场景:“ICO”,满足一下各位苦逼的开发当一回大佬的愿望,;)

背景

本文用到的开发工具:

  • Node
  • Truffle
  • 相关的包:
    • yargs,cli库
    • web3,json-rpc抽象
    • truffle-contract,合约抽象
    • openzeppelin-solidity,安全合约库

文章中创建的项目为一个“node + truffle”工程,对外提供cli。这个cli暴露了两条命令:

$./app.js help
app.js [命令]

命令:
  app.js simple-data <action> [from]        access simple-data contract from an
  [value]                                   external address.
  app.js myico <command> [purchaser]        commands about my ico.
  [value]

选项:
  --version  显示版本号                                                   [布尔]
  --help     显示帮助信息                                                 [布尔]

选择以cli而非gui的方式作为dapp的前端主要的理由:当前的重点是迅速掌握以太坊dapp开发的套路。cli相比起gui来讲,省去了很多麻烦事。而且,我对于gui的开发,实在兴趣不大。

准备

那么,让我们先来准备工程的架子:

  1. mkdir 目录 && cd 目录
  2. npm init
  3. truffle init
  4. npm install yargs --save

执行完成后,cli工程需要基本环境就都具备了。之后,在项目的工程根下创建app.js,它将作为整个工程的入口。并且工程采用Command Module的方式组织。

app.js的内容如下:

#!/usr/bin/env node

require('yargs')
  .command(require('./simple-data.js'))
  .command(require('./myico.js'))
  .help()
  .argv

其中的两条命令以module方式组织,分别对应:

  • simple-data,简单合约交互
  • myico,ico合约交互

simple-data

simple-data命令展示了一个简单的前端和合约交互的例子,为编写复杂交互提供了参考。它的整个过程如下:

  1. npm install web3 --save
  2. npm install truffle-contract --save
  3. 编写合约
pragma solidity ^0.4.23;

contract SimpleData {
    address public owner;
    
    uint data;

    constructor() public {
        owner = msg.sender;
    }

    function set(uint x) public{
        data = x;
    }

    function get() view public returns (uint) {
        return data;
    }

}

  1. 编写Migration文件
const SimpleData = artifacts.require("./SimpleData.sol");

module.exports = function(deployer) {
  deployer.deploy(SimpleData);
};
  1. 编写Command
const Web3 = require('web3');
const web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:9545"));

const contract = require('truffle-contract');
const SimpleDataContract = require('./build/contracts/SimpleData.json');
const simpleData = contract(SimpleDataContract);
simpleData.setProvider(web3.currentProvider);
if (typeof simpleData.currentProvider.sendAsync !== "function") {
    simpleData.currentProvider.sendAsync = function() {
        return simpleData.currentProvider.send.apply(
            simpleData.currentProvider, arguments
        );
    };
}

exports.command = 'simple-data <action> [from] [value]';
exports.describe = 'access simple-data contract from an external address.';
exports.handler = function(argv) {
    if(argv.action == 'get') {
        simpleData.deployed().then(function(instance){
            instance.get().then(function(result){
                console.log(+result);
            })
        });
    } else if(argv.action == 'set') {
        if(!argv.value) {
            console.log('No value provided!');
            return;
        }

        simpleData.deployed().then(function(instance){
            instance.set(argv.value, {from: argv.from}).then(function(result){
                console.log(result);
            })
        });
    } else {
        console.log('Unknown action!');
    }
}

说明:

  1. http://localhost:9545”对应“truffle develop”环境中的端口。
  2. “./build/contracts/SimpleData.json”由“truffle compile”产生,这个json文件将和truffle-contract一起配合产生针对于合约的抽象。
  3. “if(typeof ... !== "function") { ... }”这个if block是对于truffle-contract这个issue的walkaround。
  4. 随后的exports则是yargs command module的接口要求。对于命令:“simple-data <action> [from] [value]”,其格式由yargs解析:
    • <...>,必填
    • [...],选填

编译部署之后,就可以简单的试用了(先给app.js可执行权限):

  • app.js simple-data get
  • app.js simple-data set 地址 值

my-ico

接下来,就到了最让人期待的时刻:发币,更准确的说是基于ERC 20的代币发放。因为以太坊中各个币种有不同的含义和用途,比如另一个常见的币种:ERC 721,它发行的每个token都是独一无二不可互换的,比如以太猫。

关于发币,究其本质就是实现特定的合约接口。从开发的投入产出比来讲,我建议采用OpenZeppelin:

  • 跟钱打交道的事情需要慎重,由不安全合约导致的问题屡见不鲜。
  • 自己编写安全合约并不简单。
  • OpenZeppelin包含了当前安全合约的最简实践,其背后的公司本身就提供安全审计服务。
  • 可复用合约代码加快了合约的开发。

以下是使用OpenZeppelin开发ico的过程:

  1. npm install -E openzeppelin-solidity
  2. ico的过程由两个合约组成,它们都将直接基于OpenZeppelin的合约完成。
    • coin,代币
    • crowdsale,众筹
  3. 代币合约
pragma solidity ^0.4.23;

import "../node_modules/openzeppelin-solidity/contracts/token/ERC20/MintableToken.sol";

contract MyCoin is MintableToken {

    string public name = "MY COIN";   // 代币名称
    string public symbol = "MYC";    // 代币代码
    uint8 public decimal = 18;      // 位数

}
  1. 众筹合约
pragma solidity ^0.4.23;

import "../node_modules/openzeppelin-solidity/contracts/crowdsale/emission/MintedCrowdsale.sol";
import "../node_modules/openzeppelin-solidity/contracts/crowdsale/validation/TimedCrowdsale.sol";

contract MyCrowdsale is TimedCrowdsale, MintedCrowdsale {
    constructor (
        uint256 _openingTime,
        uint256 _closingTime,
        uint256 _rate,
        address _wallet,
        MintableToken _token
        ) 
        public
        Crowdsale(_rate, _wallet, _token)
        TimedCrowdsale(_openingTime, _closingTime) {
    }
}

几行代码就完成了核心合约的开发,这要归功于咱们选的框架,;)

  1. Migration脚本
const MyCoin = artifacts.require("./MyCoin.sol");
const MyCrowdsale = artifacts.require("./MyCrowdsale.sol");

module.exports = function(deployer, network, accounts) {
    const openingTime = web3.eth.getBlock('latest').timestamp + 2;
    const closingTime = openingTime + 3600;
    const rate = new web3.BigNumber(1000);
    const wallet = accounts[1];

    deployer.deploy(MyCoin).then(function() {
        return deployer.deploy(MyCrowdsale, openingTime, closingTime, rate, wallet, MyCoin.address);
    }).then(function() {
        return MyCoin.deployed();
    }).then(function(instance) {
        var coin = instance;
        coin.transferOwnership(MyCrowdsale.address);
    });
};

说明:

  • 上面的合约定义很清楚地表明,众筹需要有Token的地址,因此众筹合约需要在Token部署成功之后进行。
  • 众筹合约需要得到Token的所有权才能进行发行。一开始,Token的owner是部署者(这里是account[0]),因此在众筹合约部署完成之后需要完成Token所有权的移交。

最后一步非常关键,否则会出现类似下面的错误:

Error: VM Exception while processing transaction: revert
    at Object.InvalidResponse ...
...
  1. 最后,就是myico的命令编写了。
const Web3 = require('web3');
const web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:9545"));

const contract = require('truffle-contract');
const MyCoin = require('./build/contracts/MyCoin.json');
const myCoin = contract(MyCoin);
myCoin.setProvider(web3.currentProvider);
if (typeof myCoin.currentProvider.sendAsync !== "function") {
    myCoin.currentProvider.sendAsync = function() {
        return myCoin.currentProvider.send.apply(
            myCoin.currentProvider, arguments
        );
    };
}
const MyCrowdsale = require('./build/contracts/MyCrowdsale.json');
const myCrowdsale = contract(MyCrowdsale);
myCrowdsale.setProvider(web3.currentProvider);
if (typeof myCrowdsale.currentProvider.sendAsync !== "function") {
    myCrowdsale.currentProvider.sendAsync = function() {
        return myCrowdsale.currentProvider.send.apply(
            myCrowdsale.currentProvider, arguments
        );
    };
}

exports.command = 'myico <command> [purchaser] [value]';
exports.describe = 'commands about my ico.';
exports.handler = function(argv) {
    if(argv.command == 'hasClosed') {
        myCrowdsale.deployed().then(function(instance){
            instance.hasClosed().then(function(result){
                console.log(result);
            });
        });
    } else if(argv.command == 'deliver') {
        myCrowdsale.deployed().then(function(instance){
            instance.token().then(function(address){
                instance.sendTransaction({from: argv.purchaser, value: web3.utils.toWei(argv.value.toString(), "ether")})
                .then(function(result){
                    console.log('done!');
                }).catch(function(error){
                    console.error(error);
                });
            });
        });
    } else if(argv.command == 'totalSupply') {
        myCrowdsale.deployed().then(function(instance){
            instance.token().then(function(address){
                let coinInstance = myCoin.at(address);
                coinInstance.totalSupply().then(function(result){
                    console.log(+result);
                });
            });
        });
    } else if(argv.command == 'balance') {
        myCrowdsale.deployed().then(function(instance){
            instance.token().then(function(address){
                let coinInstance = myCoin.at(address);
                coinInstance.balanceOf(argv.purchaser).then(function(balance){
                    console.log(+balance);
                });
            });
        });
    } else {
        console.log('Unknown command!');
    }
}

有了前面simple-data命令代码的说明,这里的代码应该不会有任何难点了。其中的子命令:

  • hasClosed,众筹是否结束
  • totalSupply,众筹中产生的token总量
  • balance,某个账户的代币数
  • deliver,给账户发行代币

到这里,相信大家应该不会再觉得“发币”有什么神秘的了,接下来如何将其应用到业务中,作为业务的催化剂(而不是割韭菜利器)就靠各位的想象力了。

相关文章

网友评论

  • 8337ea5e8883:老师,您的文章写的非常好,方便加微信请教您吗?
    胡键:@梨涡浅笑_efc4 谢谢,最近太忙了,估计很难呀😂
    8337ea5e8883:@胡键 是这样。看到您的文章质量都非常高,想邀请您成为虫洞社区的首批优质内容签约作者。虫洞社区是专业的区块链技术学习社区。是基于U Network的第一个DAPP,U Network是全球首个内容价值预测和发布平台,由Draper Dragon、丹华等几十家顶级Tokenfund领投,并已登录了5家交易所。虫洞社区鼓励内容生产者产生高质量内容,并给予合理的回报,也希望能帮助内容消费者获得高质量的区块链内容,并让数字货币投资者获得有价值的投资洞见。不知道是否方便加您微信细聊?
    胡键:@梨涡浅笑_efc4 谢谢,有问题直接在文后评论区发吧,因为加的人实在有点多😔
  • 167cd2c26545:是啊应该主要用于业务催化,可是大部分人所在的行业的相关人员并不了解区块链,不说推广币,就是推广区块链是什么都很麻烦

本文标题:以太坊发币(ERC20)姿势解读

本文链接:https://www.haomeiwen.com/subject/wsfauftx.html