百度区块链引擎BBE

    智能合约开发及部署

    IDE环境准备

    本文采用Solidity语言开发以太坊智能合约。Solidity的开发环境我们直接使用BBE系统中提供的Remix web开发平台。因为BBE中提供的Remix IDE默认直接与目标以太坊网络相连,所以Remix IDE同时也可以作为部署、测试工具使用。

    按照以下方法进入Remix IDE:

    1. 进入以太坊列表页,找到想要部署智能合约的目标以太坊网络,点击右侧的智能合约编辑器,系统会弹出新浏览器tab

    图片

    1. 在跳转前,会提示我们的账户密码,并在登录页中输入用户名和密码

    图片

    1. 等待Remix IDE页面加载完成,智能合约IDE主面板已经可以使用

    图片

    1. 在Run面板中确认已经连接目标以太坊的Web3 Provider

    图片

    1. 在Settings面板选择0.4.24版本的Solidity

    图片

    1. 在最左侧点击“+”,新建一个名为NoteWall.sol的智能合约

    图片

    智能合约实现

    编写智能合约的Solidity基础语法请读者自行阅读或参考完整源码。这里主要介绍便签板合约的主要实现。

    便签对象的实现

    便签板合约的实现关键就是定义一个便签在区块链上存储的数据格式。我们首先定义一个便签的结构体

    struct Note {
    	string title;
    	string content;
    	uint256 id;
    }

    便签由三部分构成,string类型的标题,string类型的便签正文以及一个256位的无符号整数类型表示的便签ID。

    便签组的实现

    接下来就是便签“对象”们如何组织化地保存下来,这里我们需要再定义一个mapping。

    mapping(uint256 => Note) notes;

    这个mapping管理着从ID到Note对象的索引。从用例来分析,便签板需要用到查找的场景主要有两处,一处是用户点击便签,编辑便签更新,另一处是用户查看所有的便签。由于Solidity中mapping不具有遍历性,所以我们需要再维护一个记录便签ID的数组,数组本身是可遍历的。

    uint256[] noteIds;

    新增便签的实现

    新增一个便签除了需要新建一个Note对象,赋值title和content外,最主要的就是要为每一个Note对象分配一个不重复的ID。这里我们利用合约方法原子性的特点,通过维护一个累加的全局ID计数器来实现Note ID的唯一性。

    具体原理就是当新建一个Note对象时,读取全局的ID计数器,将当前计数器值作为Note对象的ID,并将ID值++。

    所以我们需要声明一个nextId,全局变量

    uint256 nextId

    接下来是新增便签的函数

    /// Insert one new note object
    function InsertNote(string _title, string _content) public {
        Note storage note = notes[nextId];
        noteIds.push(nextId);
        note.id = nextId;
        note.title = _title;
        note.content = _content;
        nextId = nextId + 1;
    }

    新增一个需要存储的Note对象,然后将nextId赋给note,并填入对应的标题和正文;此外将note索引到notes mapping中,插入新ID到noteIds数组中。

    更新便签的实现

    更新便签与新增便签稍有差别,需要从给定的Note ID中找到note对象再进行赋值。

    function UpdateNote(uint256 _id, string _title, string _content) public {
        Note storage note = notes[_id];
        note.title = _title;
        note.content = _content;
    }

    获取所有的便签

    按照用例3,用户应该会希望一次性获取所有的便签,但这在Solidity中无法通过一个方法就满足需求,原因是当前的solidity版本不支持方法返回一个struct或者struct的数组。所以我们拆分成两个函数来实现这个功能。第一个函数可以返回所有的便签ID,第二个函数则可以根据便签ID返回便签的标题和正文。

    获取所有便签ID的函数:

    /// Get all note ids
    function GetNoteIds() public view returns(uint256[] _noteIds) {
        return noteIds;
    }

    读取便签标题和正文

    /// Get one note object by its id
    function GetNote(uint256 id) public view returns(string, string) {
        return (notes[id].title, notes[id].content);
    }

    这里我们使用了多值返回的特性。

    所以最终获取所有的便签需要上层通过两步完成,首先调用GetNoteIds获取所有便签ID,之后再遍历便签ID,获取每个便签的标题和正文。虽然从传统软件设计看并不是最优的实现方式,但囿于EVM Solidity本身语法的局限性,智能合约的逻辑有时候需要做一些规避。

    智能合约完整代码

    pragma solidity ^0.4.0;
    contract NoteWall {
        struct Note {
            string title;
            string content;
            uint256 id;
        }
        mapping(uint256 => Note) notes;
        uint256[] noteIds;
        uint256 nextId;
    
        /// Create a new ballot with $(_numProposals) different proposals.
        constructor() public {
            nextId = 0;
        }
    
        /// Get all note ids
        function GetNoteIds() public view returns(uint256[] _noteIds) {
            return noteIds;
        }
        
        /// Get one note object by its id
        function GetNote(uint256 id) public view returns(string, string) {
            return (notes[id].title, notes[id].content);
        }
        
        /// Insert one new note object
        function InsertNote(string _title, string _content) public {
            Note storage note = notes[nextId];
            noteIds.push(nextId);
            note.id = nextId;
            note.title = _title;
            note.content = _content;
            nextId = nextId + 1;
        }
        
        /// Update one note by its id
        function UpdateNote(uint256 _id, string _title, string _content) public {
            Note storage note = notes[_id];
            note.title = _title;
            note.content = _content;
        }
    }

    Remix IDE会自动保存合约文件

    智能合约编译

    在Remix中点击右侧的Compile面板

    图片

    点击Start to compile进行编译,编译完成后如下图:

    图片

    智能合约测试

    Remix开发团队提供了Solidity的测试框架,可以开发完成简单的断言式单测。 新建NoteWall_test.sol合约,此合约用来对NoteWall.sol做单测覆盖。 NoteWall_test.sol的内容如下:

    pragma solidity ^0.4.7;
    import "remix_tests.sol"; // this import is automatically injected by Remix.
    import "./NoteWall.sol";
    
    contract testNoteWall {
        NoteWall noteWallToTest;
        function beforeAll () {
           noteWallToTest = new NoteWall();
        }
        
        /// test InsertNote function
        function checkInsertNote () public {
            noteWallToTest.InsertNote("note title", "note content");
            var noteIds = noteWallToTest.GetNoteIds();
            var lastNoteId = noteIds[noteIds.length - 1];
            var (title, content) = noteWallToTest.GetNote(lastNoteId);
            Assert.equal(title, "note title", "note title should be the note compositions");
            Assert.equal(content, "note content", "note content should be the note compositions");
        }
        
        /// test UpdateNote function
        function checkUpdateNote () public {
            noteWallToTest.InsertNote("note title", "note content");
            var noteIds = noteWallToTest.GetNoteIds();
            var lastNoteId = noteIds[noteIds.length - 1];
            noteWallToTest.UpdateNote(lastNoteId, "new title", "new content");
            var (title, content) = noteWallToTest.GetNote(lastNoteId);
            Assert.equal(title, "new title", "new title should be the updated note compositions");
            Assert.equal(content, "new content", "new content should be the updated note compositions");
        }
        
        /// test GetNoteIds function
        function checkGetNoteIds () public {
            noteWallToTest.InsertNote("note title 0", "note content 0");
            noteWallToTest.InsertNote("note title 1", "note content 1");
            noteWallToTest.InsertNote("note title 2", "note content 2");
            var noteIds = noteWallToTest.GetNoteIds();
            Assert.equal(noteIds.length, 3, "noteIds' length should equal 3");
        }
        
        /// test GetNote function
        function checkGetNote () public {
            noteWallToTest.InsertNote("note title", "note content");
            var noteIds = noteWallToTest.GetNoteIds();
            var lastNoteId = noteIds[noteIds.length - 1];
            var (title, content) = noteWallToTest.GetNote(lastNoteId);
            Assert.equal(title, "note title", "note title should be the note compositions");
            Assert.equal(content, "note content", "note content should be the note compositions");
        }
    }

    由于只是运用了remix_tests.sol中的断言语法,实现并不复杂,此处不做赘述。

    点击右侧Test标签,进入测试面板。勾选browser/NoteWall_test.sol,点击“Run Tests”按钮开始测试。运行完单测后,测试结论应如下图:

    图片

    智能合约部署

    单测运行完成后,我们开始进行智能合约的部署。如果开发者对合约代码没有信心,可以先将合约部署到Remix自带的EVM虚机(Javascript VM)。由于过程是类似的,这里我们将说明直接把合约部署到以太坊网络中的过程。

    点击右侧面板“Run”标签,将GasLimit填为300000000,点击下方“Deploy”按钮进行合约部署。 为了保护用户的安全,需要输入百度云的access key和secret key

    图片

    如果没有的话,可以按照提示获取或创建Access Key即可。

    部署时长取决于您目标区块链网络的出块时长,当部署完成后,在console面板中可以看到交易的信息。如下图:

    图片

    这里是详细的合约部署交易信息。

    我们保留当前的窗口不要关闭,有几处重要的数据需要摘出来。

    获取已部署合约的重要信息

    获取合约地址

    在右侧面板的“Deployed Contracts”中复制

    图片

    此外,我们也可以从合约管理中心获取合约的相关信息

    图片

    熟悉以太坊Dapp开发的用户可能会想到要提取ABI文件进行开发。但是通过合约网关调用智能合约,我们无需配置ABI文件,无需处理复杂的交易生成和解析,把相关交易参数发送给合约网关就可以实现合约方法的调用。

    获取合约网关地址 我们的Dapp需要和合约网关通信,因此我们需要前往管理中心获取合约网关的通信地址

    图片

    在Remix中调用已部署合约

    这一步主要是为了验证已部署合约是否正常工作。我们可以在Remix中直接调用刚才部署的合约。

    验证InsertNote方法

    在Run面板中打开InsertNote框,在_title和_content中填入相应的参数。下图为举例:

    图片

    点击“transact”发起调用交易,同上文一样,passphrase输入123。点击OK后交易发起。 我们可以在Console中看到交易正在等待入块。

    图片

    等待片刻,当交易入块后,我们在Console面板中可以看到交易详情

    图片

    验证GetNoteIds方法

    在Run面板中点击最下方的GetNoteIds,由于是只读方法,不需要发起交易,面板上应该会直接返回结果,应是一个元素的数组。

    图片

    验证GetNote方法

    在Run面板中打开最下方的GetNote,在id字段中填入0,点击call。由于也是只读方法,不需要发起交易,面板上应该会直接返回结果,应是刚才新建的Note信息。

    图片

    验证UpdateNote方法

    我们再验证UpdateNote方法,已知已有NoteId为0,填入新的标题和内容。由于需要发起新的交易,所以需要稍作等待。

    图片

    交易完成:

    图片

    重新GetNote:

    图片

    重新GetNoteIds,确认没有新增加Note:

    图片

    到此,智能合约的开发、测试、部署、验证步骤已经完成了。接下来我们开始进行上层应用层的开发。

    上一篇
    Dapp架构及概要设计
    下一篇
    Dapp应用层开发