区块链实战系列:众筹空间(NodeJs版本)

1 基础环境准备

1.1环境预览

这里写图片描述

1.2基础工程

Npm install -g truffle
Npm install -g EthereumJS TestRPC
Npm install -g supervisor
Truffle init
Npm init

这里写图片描述

1.3疑难解答

  • Web3依赖c++build tools python(only for python2.7),npm install web3会报?

Recommend:npm install –global –production windows-build-tools View
more detail:https://github.com/nodejs/node-gyp#installation能合约开发

2 智能合约开发

2.1 基础表结构

pragma solidity ^0.4.21;

/**
* 官方文档众筹实现
* https://solidity.readthedocs.io/en/develop/types.html#structs
* 1 存储方式:memory、storage,状态变量默认是storage而local变量默认是memory
* 2 mapping类似java中字典key=>value,但是没有数组常用操作:push
* 3 require类似junit中断言
* 4 函数体操作状态变量必须有修饰符,view:对状态变量读操作,payable:支付操作
* 5 msg全局变量是配合web3使用
**/
contract CrowdFunding {

    // 表结构:众筹项目
    struct Fund {
        // 众筹地址
        address owner;
        // 众筹描述
        string desc;
        // 众筹目标
        uint goal;

        // 已筹金币
        uint coins;
        // 是否结束
        bool finished;

        // 捐赠人数
        uint recordCounts;
        // 捐赠记录
        mapping(uint => Record) records;
        // 原本使用 Record[] records 数组定义
        // 但是貌似目前版本尚不支持
        // 于是将 数组 拆分成 长度 + 映射
        // https://solidity.readthedocs.io/en/develop/types.html#mappings
    }

    // 表结构:捐赠记录
    struct Record {
        // 捐赠成员
        address member;
        // 捐赠金币
        uint coin;
        // 捐赠时间
        uint time;
    }

2.2 查询操作

// 众筹地址列表
Fund[] funds;

function CrowdFunding() public payable {
}

// 获取众筹项目数量
function getFundCount() public view returns (uint) {
    return funds.length;
}

// 获取众筹项目信息
// 参数:项目编号
// 返回:众筹地址 众筹描述 众筹目标 已筹金币 是否结束
function getFundInfo(uint fundIndex) public view returns (address, string, uint, uint, bool) {
    Fund storage fund = funds[fundIndex];
    return (fund.owner, fund.desc, fund.goal, fund.coins, fund.finished);
}

// 获取众筹捐赠人数
function getRecordCount(uint fundIndex) public view returns (uint) {
    return funds[fundIndex].recordCounts;
}


// 获取众筹捐赠记录
// 参数:项目编号 记录编号
// 返回:捐赠成员 捐赠金币 捐赠时间
function getRecordInfo(uint fundIndex, uint recordIndex) public view returns (address, uint, uint) {
    Record storage record = funds[fundIndex].records[recordIndex];
    return (record.member, record.coin, record.time);
}

2.3 发起众筹

// 发起众筹:
function raiseFund(address raiser, string info, uint goal) public {
    //funds.push(Fund(msg.sender, info, goal, 0, false, 0));
    funds.push(Fund(raiser, info, goal, 0, false, 0));
}

2.4 账户支付操作

// 捐赠众筹:
// https://solidity.readthedocs.io/en/latest/miscellaneous.html#modifiers
// payable for functions: Allows them to receive Ether together with a call.
function sendCoin(uint fundIndex) public payable {
    // 默认属性
    // msg.sender 转账账户
    // msg.value 转账数目
    // 智能合约默认单位 wei
    // 1 ether = 10^18 wei

    // 引用拷贝
    Fund storage fund = funds[fundIndex];
    require(!fund.finished);

    // 转账 失败自动退出
    fund.owner.transfer(msg.value);

    // 更新众筹信息
    fund.coins += msg.value;
    fund.records[fund.recordCounts++] = Record(msg.sender, msg.value, now);
    fund.finished = fund.coins >= fund.goal * 1 ether? true : false;
}


// fallback function:回退函数 防止抛出异常
// https://solidity.readthedocs.io/en/latest/contracts.html#fallback-function
// if you want your contract to receive Ether, you have to implement a fallback function.
function() public payable { }

3 NodeJs export众筹模型

3.1 合约编译

Truffle compile --reset
注意--reset是版本发生变化才生效,编译生成的合约\build\contracts\CrowdFunding.json

3.2 NodeJs引入abi协议、web3、truffle-contract

/**
 * 众筹业务控model
 * @author wolf
 * @time   2018-05-01
 */
var Web3 = require('web3');
var contract = require("truffle-contract");

var provider = new Web3.providers.HttpProvider("http://localhost:8545");

//使用truffle-contract包的contract()方法
var CrowdFunding = contract(
   copy编译后文件:/build/contracts/CrowdFunding.json
);

//UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 3): Error: invalid address
//没有默认地址,会报错:务必设置为自己的钱包地址,如果不知道,查看自己的客户端启动时,观察打印到控制台的地址
CrowdFunding.setProvider(provider);
CrowdFunding.defaults({
    from : "0xeeacbf78bb5eeba49acdc67c0fbd5dd46e67facf"
});


module.exports = function () {
    this.web3 = new Web3(provider);
    this.model = CrowdFunding;
}

3.3 合约部署

Truffle migrate --reset

3.4 疑难解答

  • UnhandledPromiseRejectionWarning: Unhandled promise rejection
    (rejection id: 3): Error: invalid address
**Option 1:**
CrowdFunding.defaults({
    from : "0xeeacbf78bb5eeba49acdc67c0fbd5dd46e67facf"
});

**Option2:**
CrowdService.web3.eth.defaultAccount=account;

4 NodeJs众筹业务交互

4.1 web3交互基础信息

/**
 * 账户列表
 * @param req
 * @param res
 */
get_accountsList:function (req, res) {
    res.send(CrowdService.web3.eth.accounts)
},

/**
 * 查看区块数
 * @param req
 * @param res
 */
get_blocksList : function (req, res) {
    res.send(CrowdService.web3.eth.blockNumber);
},

/**
 * 查看默认账户/调用合约的账户
 * @param req
 * @param res
 */
post_defaultAccount: function (req, res) {

    var account = req.body.account;
    var retMsg = new Message();

    if (Checks.isNull(account)) {
        console.log("Current default: " + CrowdService.web3.eth.defaultAccount);
        res.send(retMsg.success('合约调用默认账户:'+ CrowdService.web3.eth.defaultAccount));
    } else {
        CrowdService.web3.eth.defaultAccount=account;
        res.send(retMsg.success('更新合约调用默认账户:'+ CrowdService.web3.eth.defaultAccount))
    }
},


/**
 * 查看账户余额
 * @param req
 * @param res
 */
post_accountBalance : function (req, res) {
    var account = req.body.account;
    retMsg = new Message();
    if (Checks.isNull(account)) {
        res.send(retMsg.fail('账户地址不能为空!'))
    }
    var balance = CrowdService.web3.eth.getBalance(account);
    res.send(balance)
},

4.2 众筹业务交互

/**
 * 发起众筹
 * @param req
 * @param res
 */
post_raiseFund: function (req, res) {
    var raiser = req.body.raiser;
    var info = req.body.info;
    var goal = req.body.goal;
    if (Checks.isNull(raiser)) {
        raiser = CrowdService.web3.eth.accounts[0];
    }

    CrowdService.model.deployed().then(function (instance) {
        return instance.raiseFund(raiser, info, goal, { gas: 1000000 });
    }).then(function (result) {
        console.log(result); //打印交易收据
    });

    res.send(JSON.stringify({"info" : info, "goal": goal, "code": "募集成功!"}));
},

/**
 * 捐赠众筹
 * @param req
 * @param res
 */
post_donate: function (req, res) {
    var retMsg = new Message();
    // sendCoin(uint fundIndex, address donater, uint coin)
    var fundIndex = req.body.fundIndex;
    var donater = req.body.donater;
    var coin = req.body.coin;


    /*if (Checks.isNull(fundIndex) || Checks.isNull(donater) || Checks.isNull(coin)) {
        res.send(JSON.stringify(retMsg.fail('众筹项目、捐赠账户、捐赠数量不能为空!!!')));
    }*/

    CrowdService.web3.eth.defaultAccount = donater;

    CrowdService.model.deployed().then(function (instance) {
        return instance.sendCoin(fundIndex,{value: CrowdService.web3.toWei(coin, 'ether'), from: donater, gas: 1000000 });
    }).then(function (result) {
        console.log(result); //打印交易收据
    });

    res.send(JSON.stringify(retMsg.success('捐赠成功!!!')));
},

/**
 * 众筹列表
 * @param req
 * @param res
 */
post_fundsList: function (req, res) {
    var total;
    var list = new Array();
    var g;

    CrowdService.model.deployed().then(function (instance) {
        g=instance;
        return g.getFundCount();
    }).then(function (total) {
        //promise循环
        var promoseArray=[];
        for (index=0; index < total; index++) {
            promoseArray.push(g.getFundInfo(index));
        }

        Promise.all(promoseArray).then(function (data) {
            res.send(JSON.stringify(data));
        })
    });
},

/**
 * 众筹详情
 * @param req
 * @param res
 */
post_fundInfo: function (req, res) {
    var index = req.body.index;
    CrowdService.model.deployed().then(function (instance) {
        return instance.getFundInfo(index);
    }).then(function (result) {
        res.send(result);
    });
},


/**
 * 获取捐赠记录列表
 * @param req
 * @param res
 */
post_recordsList: function (req, res) {
    var fundIndex = req.body.fundIndex;
    var g;

    CrowdService.model.deployed().then(function (instance) {
        g = instance;
        return g.getRecordCount(fundIndex);
    }).then(function (count) {
        console.log(count);
        if (count <= 0) {
            res.send("没有捐赠记录");
            return;
        }

        var promiseArray=[];
        for(x=0; x<count; x++){
            promiseArray.push(g.getRecordInfo(fundIndex, x));
        }

        Promise.all(promiseArray).then(function (data) {
            res.send(JSON.stringify(data));
        })
    });
},

4.3 基础工具

/**
 * 输出格式类
 * @type {{code: number, msg: string, data: {total: number, pageNo: number, pageSize: number, object: {}, array: Array}}}
 * @author wolf
 * @time 2018/5/1
 */

function Message () {
    this.result = {
        "code": 0,
        "msg": "操作成功",
        "data": {"total": 0, "pageNo": 1, "pageSize": 10, "object": {}, "array": []}
    };
}

Message.prototype.success = function (msg) {
    this.result.msg = msg;
    return this.result;
}

Message.prototype.fail = function (msg) {
    this.result.code = -1;
    this.result.msg = msg;
    return this.result;
}

Message.prototype.listPage = function (total, pageNo, list) {
    this.result.data.total = total;
    this.result.data.pageNo = pageNo;
    this.result.data.array = list;
    return this.result;
}

Message.prototype.setData = function (object) {
    this.result.data.object = object;
    return this.result;
}

module.exports = Message;

4.4 部署配置

var Migrations = artifacts.require("./Migrations.sol");
var Test = artifacts.require("./Test.sol");
var CrowdFunding = artifacts.require("./CrowdFunding.sol");

module.exports = function (deployer) {
    deployer.deploy(Migrations);
    deployer.deploy(Test);
    deployer.deploy(CrowdFunding);
};

4.5 疑难解答

  • Promise循环
var promiseArray=[];
for(x=0; x<count; x++){
    promiseArray.push(g.getRecordInfo(fundIndex, x));
}

Promise.all(promiseArray).then(function (data) {
    res.send(JSON.stringify(data));
})
  • Javascript prototype实现面向对象继承
Message.prototype.success = function (msg) {
    this.result.msg = msg;
    return this.result;
}
  • NodeJs静态导入
/**
 * 检查工具类
 * @author wolf
 * @time 2018/5/1
 */
function Checks() {}

    /**
     * 判断是否为空
     * @param val
     * @returns
     */
    Checks.isNull = function(val) {
        if (val == undefined || val == null || val == "" || val == ''
            || val == "undefined" || val == "null" || val == "NULL") {
            return true;
        }
        return false;
    }

module.exports = Checks;
  • Msg全局变量使用(msg.sender、msg.value)
// 转账 失败自动退出
fund.owner.transfer(msg.value);

// 更新众筹信息
fund.coins += msg.value;
fund.records[fund.recordCounts++] = Record(msg.sender, msg.value, now);

CrowdService.model.deployed().then(function (instance) {
    return instance.sendCoin(fundIndex,{value: CrowdService.web3.toWei(coin, 'ether'), from: donater, gas: 1000000 });
}).then(function (result) {
    console.log(result); //打印交易收据
});
  • 交易中total used gas = usedgas *
    gasPrice,而每次交易都需要预设gas(预估使用gas上限),使用完total used gas > gas:out of gas
    error,相反多余的gas会在交易完退换账户
  • 转账账户余额 = 默认账户总额 - msg.value - total used gas
  • msg.sender定义为调用合约账户,合约也能隐形转换为合同账户

猜你喜欢

转载自blog.csdn.net/wolfjson/article/details/80494720