全方位讲解Move开发测试部署工具栈

如何用Move开发智能合约?本文全方位讲解Move开发测试部署工具栈。

## **Move项目的开发实战** * 本文由Starcoin社区原创 作者:WGB 根据Starcoin & Move直播课《Move开发实战》整理,点击[查看原文](https://starcoin.org/zh/developer/blog/move_development/)。 Move 编程语言最早出现在 Facebook 的 Diem 区块链项目中,它是面向数字资产编程的智能合约语言,Move具有多种特性,涉及安全、开发效率等方面。 如果想要完整的开发一个Move语言的项目,个人觉得要了解Move项目的开发流程,相对于其他语言的项目来说,Move语言的基本流程都比较相似,都有开发、单元测试、集成测试、本地发布与调用、链上部署与调用等等。但由于合约编程语言的不同,开发工具与每一项具体步骤也不同,所以对于希望开发Move项目和希望了解Move语言开发的开发者或关注者来说,可以通过本篇《Move项目的开发实战》来了解和熟悉Move项目的开发,需要注意Move语言的开发暂时需要使用类unix系统进行开发,推荐使用MacOS或者ubuntu20.04进行开发。如果没有Mac,可以使用虚拟机下的ubuntu进行开发。 ## **一、新建Move项目** 开发项目的第一个步骤就是创建一个新的项目,这个过程可以自己创建项目的树状目录,也可以通过使用 move-cli 进行创建。move-cli是官方推出的一个move的开发工具,有创建、编译、测试等功能,可以从官方的[github](https://github.com/starcoinorg/starcoin/)下载move-cli,也可以通过下载完整的starcoin包后在解压包内找到move-cli,还可以通过clone后自行编译编译的方式获取。 **直接下载:** ![](https://pic3.zhimg.com/80/v2-fe9b8a4c59473881a8cf3955b6525586_1440w.jpg)### **1. 使用move-cli创建项目** 在下载好move-cli后,可以通过命令创建新的项目,对于move-cli的其他命令可以通过--help 来查看具体功能,随后我们也会在项目过程中使用它们中的一部分。 **创建 hello_world 项目** ```text move scaffold hello_world ``` **创建的目录结构:** ```text 执行: tree hello_world 结果: hello_world/ ├── args.txt ├── src │ ├── modules │ └── scripts └── tests ``` * src下的modules存放的就是要写的合约代码,scripts存放的是写的脚本代码 ## **二、开发与调试** ### **1. hello world** 在创建好目录后就可以在src目录下写脚本和模块,可以在scripts目录中创建一个hello_world.move,并在里面填写代码,代码的含义是在屏幕打印 hello world 的 十进制 ascii 码 vector,这主要是暂时在Move中未支持string类型,这项支持已经在社区中有一些进度,可以等待后续的更新。 ### **(1) hello_world.move** ```text script { use 0x1::Debug; fun main() { Debug::print(&b"hello world"); } } ``` **代码示例:** ![](https://pic3.zhimg.com/80/v2-0a7f3c1ff9d8479b6c93b99bde15438a_1440w.jpg)### **(2)执行验证** ```text 执行: move run src/scripts/hello_world.move 结果: [debug] (&) [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100] ``` **执行示例:** ![](https://pic4.zhimg.com/80/v2-1357646590662c52f66c590db6513f93_1440w.png)### **2. 编译** 在上一小节的hello world 中使用的是script脚本的方式,但是在Move合约项目中的核心还是module模块,通过module模块中的函数和脚本组合可以实现多种多样的功能。通过对模块的编译,可以将模块部署到区块链中使用,在编译之前也可以通过check功能进行语法检查,以便减少开发中遗漏的问题。 ### **(1) 编写Test.move代码** 首先,在src/modules下编写一个Test.move,在其中实现一个自定义的Struct以及创建、修改和销毁Struct函数。 ```text address 0x2{ module Test { use 0x1::Signer; struct Resource has key { i: u64 } public fun publish(account: &signer) { move_to(account, Resource { i: 10 }) } public fun write(account: &signer, i: u64) acquires Resource { borrow_global_mut<Resource>(Signer::address_of(account)).i = i; } public fun unpublish(account: &signer) acquires Resource { let Resource { i: _ } = move_from(Signer::address_of(account)); } public fun value_of(addr: address):u64 acquires Resource{ borrow_global<Resource>(addr).i } } } ``` **(2) check 编译** 对于编写的Move代码,可能在编写的过程中忘记一些符号或着变量的使用。所以可以通过move check命令对代码的语法进行检查编译,如果语法出现错误就会在屏幕中显示,如果语法没有错误则不会打印任何信息。 在move check时默认使用的是stdlib标准库中的库代码,如果想要依赖于链上的已有合约代码,可以通过使用--mode starcoin --starcoin-rpc [http://main.seed.starcoin.org:9850](https://main.seed.starcoin.org%3A9850/) 等选项进行链上依赖检查。具体的选项和功能可以使用move check --help 来查看。 **执行check:** ```text 执行: move check src/modules/Test.move 结果: ``` **执行本地check示例:** ![](https://pic4.zhimg.com/80/v2-9630c97abf1ac2b6dcc2afc52db3db3b_1440w.png)**链上check:** ```text move check \ --mode starcoin \ --starcoin-rpc http://main.seed.starcoin.org:9850 \ --block-number 1000000 \ src/modules/Test.move ``` **执行链上check示例:** ![](https://pic3.zhimg.com/80/v2-6f922320d2dd9a36eefc85fc3d584322_1440w.jpg)## **三、单元测试** 在Move开发过程中通过check检查没有语法错误后,依然不能掉以轻心,因为代码中的错误不只有语法错误,更多的是业务逻辑的错误和代码编写中的逻辑错误,对于这些错误,可以使用功能强大的单元测试来针对小范围的代码进行测试。 ### **1. 编写module代码** 编写MyModule.move代码进行单元测试,将需要测试的代码用类似宏的方式标记,对于需要测试的项目用 `#[test]` ,如果不关心代码中的assert的中止码可以使用`[expected_failure]`跳过,对于只需要在测试下的函数可以使用`#[test]`,对于需要传递参数的函数可以通过`#[test(a = @0x1, b = @0x2)]`传递参数。 ```text address 0x2{ module MyModule { struct MyCoin has key { value: u64 } public fun make_sure_non_zero_coin(coin: MyCoin): MyCoin { assert(coin.value > 0, 0); coin } public fun has_coin(addr: address): bool { exists<MyCoin>(addr) } #[test] fun make_sure_non_zero_coin_passes() { let coin = MyCoin { value: 1 }; let MyCoin { value: _ } = make_sure_non_zero_coin(coin); } #[test] // Or #[expected_failure] if we don't care about the abort code #[expected_failure(abort_code = 0)] fun make_sure_zero_coin_fails() { let coin = MyCoin { value: 0 }; let MyCoin { value: _ } = make_sure_non_zero_coin(coin); } #[test_only] // test only helper function fun publish_coin(account: &signer) { move_to(account, MyCoin { value: 1 }) } #[test(a = @0x1, b = @0x2)] fun test_has_coin(a: signer, b: signer) { publish_coin(&a); publish_coin(&b); assert(has_coin(@0x1), 0); assert(has_coin(@0x2), 1); assert(!has_coin(@0x3), 1); } } } ``` ### **2. 执行单元测试** 在编写测试代码后,可以通过move unit-test 来测试代码,测试的结果会在终端进行打印,如果测试通过会打印出PASS。在测试时也可以通过不同的选项来查看测试的信息,如: move unit-test -l src/modules/Mymodule.move 可以查看测试项,move unit-test -s src/modules/Mymodule.move 可以在做测试时统计时间等,更多选项与功能可以通过move unit-test --help来查看和使用。 ### **(1)执行单元测试** ```text 执行: move unit-test src/modules/Mymodule.move 结果: Running Move unit tests [ PASS ] 0x2::MyModule::make_sure_non_zero_coin_passes [ PASS ] 0x2::MyModule::make_sure_zero_coin_fails [ PASS ] 0x2::MyModule::test_has_coin Test result: OK. Total tests: 3; passed: 3; failed: 0 ``` **测试结果:** ![](https://pic3.zhimg.com/80/v2-9679e0ad46259e52c2233f0f4347ad5e_1440w.jpg)### **(2)查看测试项** ```text 执行: move unit-test -l src/modules/Mymodule.move 结果: 0x2::MyModule::make_sure_non_zero_coin_passes: test 0x2::MyModule::make_sure_zero_coin_fails: test 0x2::MyModule::test_has_coin: test ``` **测试结果:** ![](https://pic2.zhimg.com/80/v2-c2e623835874099875e9cd7634bb3e75_1440w.png)### **(3)带有统计的单元测试** ```text 执行: move unit-test -s src/modules/Mymodule.move 结果: Running Move unit tests [ PASS ] 0x2::MyModule::make_sure_non_zero_coin_passes [ PASS ] 0x2::MyModule::make_sure_zero_coin_fails [ PASS ] 0x2::MyModule::test_has_coin ​ Test Statistics: ​ ┌───────────────────────────────────────────────┬────────────┬───────────────────────────┐ │ Test Name │ Time │ Instructions Executed │ ├───────────────────────────────────────────────┼────────────┼───────────────────────────┤ │ 0x2::MyModule::make_sure_non_zero_coin_passes │ 0.000 │ 1 │ ├───────────────────────────────────────────────┼────────────┼───────────────────────────┤ │ 0x2::MyModule::make_sure_zero_coin_fails │ 0.000 │ 1 │ ├───────────────────────────────────────────────┼────────────┼───────────────────────────┤ │ 0x2::MyModule::test_has_coin │ 0.000 │ 1 │ └───────────────────────────────────────────────┴────────────┴───────────────────────────┘ ​ ``` **测试结果:** ![](https://pic1.zhimg.com/80/v2-16d7d2c0da1e82fa922a995ff421522c_1440w.jpg)## **四、功能(集成)测试** 单元测试只适用于小范围的测试,当整个需要进行复杂测试时,则需要通过功能测试来详细的测试,功能测试相对于单元测试增加了区块链测试,可以通过定义账号、定义区块的生成以及交易的产生等等来测试项目代码。 ### **1.功能测试的组成** 功能测试可以分为个板块: * 全局配置 * 新区块的生成 * 区块的交易 * 执行的交易代码 ### **(1) 全局配置** 可以指定全局的账户等等 格式为: ```text //! account: alice, 100000000000,77 //! account: <name> <address> <amount> <sequence-number>` ``` ### **(2) 新区块的生成** 可以在测试中生成区块并指定打包人、区块号和生成时间等等 格式为: ```text //! block-prologue //! author: alice //! block-number: 1 //! block-time: 10000 ``` ### **(3) 区块的交易** 可以生成区块的交易并指定发起人、参数、gas费等等 格式为: ```text //! new-transaction //! sender:alice //! args: 10u64 //! max-gas: 7700000 //! sequence-number:77 //! gas-price: 1 ``` ### **(4) 执行的交易代码** 可以指定发起交易所执行的代码 格式为: ```text script { use 0x2::Test; use 0x1::Signer; fun main(account: signer, expected: u64){ Test::publish(&account); assert(Test::value_of(Signer::address_of(&account)) == expected, 100); } } ``` ### **2.功能测试示例** 指定三个账户并分别设置不同的配置,生成新的区块,并生成新的交易来测试 ```text //! account: alice, 10000000000000, 77 //! account: bob, 0x3 //! account: tom,10000000000 ​ //! block-prologue //! author: alice //! block-number: 1 //! block-time: 10000 ​ //! new-transaction //! sender:alice //! args: 10u64 //! max-gas: 7700000 //! sequence-number:77 //! gas-price: 1 ​ script { use 0x2::Test; use 0x1::Signer; fun main(account: signer, expected: u64){ Test::publish(&account); assert(Test::value_of(Signer::address_of(&account)) == expected, 100); } } ​ //! block-prologue //! author: alice //! block-number: 2 //! block-time: 20000 ``` ## **五、合约的本地发布和调用** 代码测试后,可以通过move-cli去部署代码,因为在链上部署调用对于测试开发环境比较麻烦,所以优先本地测试调用合约。 ### **1.publish 编译并本地部署** 通过check检查编译后的代码就可以通过publish编译生成字节码文件,使用move publish 就可以对代码编译字节码,代码编译后的字节码文件默认存放在当前文件夹的storage中。 publish的编译与check的检查编译类似,在成功以后不会有输出结果,不同的是publish会在hello_world/storage/0x00000000000000000000000000000002/modules目录中生成字节码文件Test.mv,publish和check一样可以使用stdlib或链上的合约进行操作,选项与check相同,可以通过move publish --help 进行查看具体的选项与功能。 **执行publish:** ```text 执行: move publish src/modules/Test.move 结果: ``` **执行结果:** ![](https://pic3.zhimg.com/80/v2-47e9b29df927afdc214ddf6d76666652_1440w.png)### **2. 查看字节码文件** 在通过publish编译出module的字节码后,所有的字节码将在storage/0x00000000000000000000000000000002/modules下产生,如果在调用过程中出现异常,可以通过move view 命令来分析字节码查错 ```text 执行: move view Test.mv 结果: module 2.Test { struct Resource has key { i: u64 } ​ public publish() { 0: MoveLoc[0](Arg0: &signer) 1: LdU64(10) 2: Pack[0](Resource) 3: MoveTo[0](Resource) 4: Ret } public unpublish() { 0: MoveLoc[0](Arg0: &signer) 1: Call[3](address_of(&signer): address) 2: MoveFrom[0](Resource) 3: Unpack[0](Resource) 4: Pop 5: Ret } public write() { 0: CopyLoc[1](Arg1: u64) 1: MoveLoc[0](Arg0: &signer) 2: Call[3](address_of(&signer): address) 3: MutBorrowGlobal[0](Resource) 4: MutBorrowField[0](Resource.i: u64) 5: WriteRef 6: Ret } } ``` **查看字节码的部分结果:** ![](https://pic1.zhimg.com/80/v2-a60c0b15868b6ddb7db8b127c90fed14_1440w.jpg)### **3. 本地调用** 通过本地的部署后,可以通过写script脚本来调用module代码,以测试和验证module代码 ### **(1) 编写脚本代码** 在src/script/下编写publish_resource.move,调用0x2::Test模块下的函数并打印返回值 ```text script { use 0x2::Test; use 0x1::Debug; use 0x1::Signer; fun main(account: signer) { Test::publish(&account); Debug::print(&Test::value_of(Signer::address_of(&account))); } } ``` ### **(2) 执行脚本** 执行脚本以调用Test模块中的函数,在调用时通过move run 调用脚本并指定发起者 ```text 执行: move run src/scripts/publish_resource.move --signers 0x12345 结果: [debug] 10 ``` **执行本地脚本:** ![](https://pic4.zhimg.com/80/v2-9a402541b04851942dcf9ee19230fb07_1440w.png)### **(3) view 查看区块链结果** 在本地调用之后,既可以通过区块链方式查看结果,也可以通过move view方式来查看。 ```text 执行: move view storage/0x00000000000000000000000000012345/resources/0x00000000000000000000000000000002::Test::Resource.bcs 结果: key 0x00000000000000000000000000000002::Test::Resource { i: 10 } ``` **查看本地调用的资源:** ![](https://pic2.zhimg.com/80/v2-9b2828d8fc085f02da43445a01736dc1_1440w.png)## **六、合约的链上部署和调用** 在本地部署测试之后就可以通过dev网络进行链上的部署测试,可以通过starcoin的启动一个dev网络,并使用默认账户进行链上部署和调用合约 ### **1. 启动节点** ```text 启动dev节点 执行: starcoin -n dev console 结果: starcoin% ``` ### **2. 账户管理** ### **(1)查看账户** 查看账户的地址,方便修改module的address ```text 执行: starcoin% account show ​ 结果: { "ok": { "account": { "address": "0xe1fb7f08be5427c9230e7eea99ce21a7", "is_default": true, "is_readonly": false, "public_key": "0xdaa5325889979bf533659448ebca82a13d379c574fe7e9af0b9e06e70c6d971b", "receipt_identifier": "stc1pu8ah7z972snujgcw0m4fnn3p5ulvfsv9" }, "auth_key": "0x31c2ab0ea48eff7623eaa5608d96e4f5e1fb7f08be5427c9230e7eea99ce21a7", "sequence_number": null, "balances": {} } } ``` **查看账户结果:** ![](https://pic2.zhimg.com/80/v2-3c38106b7d1441134f1d11f65b38d815_1440w.jpg)### **(2)获得STC** 获得一些STC用作部署和调用的gas费 ```text 执行: starcoin% dev get-coin ​ 结果: txn 0x0ee2eca20d4158b390be31f3fecaeac9d177f05d2e3e9ea489c83cc453ee0c20 submitted. { "ok": { "block_hash": "0x99ffac9baafb80348cd69952de20309c134e84f60316ea16d974b1a8b0c5b85c", "block_number": "7", "transaction_hash": "0x0ee2eca20d4158b390be31f3fecaeac9d177f05d2e3e9ea489c83cc453ee0c20", "transaction_index": 1, "state_root_hash": "0x5f8beedeb725c9dd434b200969aa7820b2c65bd3abda1860c7b4c2d5310f5ac9", "event_root_hash": "0xdba2769b4e1f4c9170a8ad7b27268debfcabba0bf0e998f2d8fd2e78c0faf252", "gas_used": "119769", "status": "Executed" } } ``` **获得STC结果:** ![](https://pic3.zhimg.com/80/v2-0291b24841de95e5db5e96f6ab998f1e_1440w.jpg)### **(3)解锁账户** 解锁账户以便交易可以签名发出 ```text 执行: starcoin% account unlock ​ 结果: { "ok": { "address": "0xe1fb7f08be5427c9230e7eea99ce21a7", "is_default": true, "is_readonly": false, "public_key": "0xdaa5325889979bf533659448ebca82a13d379c574fe7e9af0b9e06e70c6d971b", "receipt_identifier": "stc1pu8ah7z972snujgcw0m4fnn3p5ulvfsv9" } } ``` **解锁账户结果:** ![](https://pic3.zhimg.com/80/v2-ef61b95e04849a9c375f9f91b0ed9226_1440w.jpg)### **3.修改module模块address** 修改address 以便可以在链上部署 ```text address 0xe1fb7f08be5427c9230e7eea99ce21a7{ module Test { use 0x1::Signer; ​ struct Resource has key { i: u64 } ​ public fun publish(account: &signer) { move_to(account, Resource { i: 10 }) } ​ public fun write(account: &signer, i: u64) acquires Resource { borrow_global_mut<Resource>(Signer::address_of(account)).i = i; } ​ public fun unpublish(account: &signer) acquires Resource { let Resource { i: _ } = move_from(Signer::address_of(account)); } public fun value_of(addr: address):u64 acquires Resource{ borrow_global<Resource>(addr).i } } module TestScript { use 0xe1fb7f08be5427c9230e7eea99ce21a7::Test; ​ public (script) fun publish(account: signer) { Test::publish(&account); } ​ public (script)fun write(account: signer, i: u64) { Test::write(&account,i); } ​ public (script)fun unpublish(account: signer){ Test::unpublish(&account); } public (script)fun value_of(addr: address):u64 { Test::value_of(addr) } } } ``` ### **4. 编译部署** ### **(1)编译为字节码** 部署时需要使用字节码文件部署,所以先编译为字节码文件 ```text 执行: move publish 结果: ​ ``` ### **(2)在dev网络下部署** 在dev下部署module的字节码,节省成本方便开发 ```text 执行: starcoin% dev deploy /home/wgb/code/starcoin/hello_world/storage/0xe1fb7f08be5427c9230e7eea99ce21a7/modules/Test.mv -b dev deploy /home/wgb/code/starcoin/hello_world/storage/0xe1fb7f08be5427c9230e7eea99ce21a7/modules/TestScript.mv -b 结果: 生成新的区块交易 ``` ### **5. 调用** ### **(1) 调用脚本** 调用publish 脚本测试module代码 ```text 执行: starcoin% account execute-function --function 0xe1fb7f08be5427c9230e7eea99ce21a7::TestScript::publish 结果: 生成新的交易 ``` ### **(2) 查看资源** 在执行脚本后可以查看资源是否已经被创建,用来验证脚本和module的可用性 ```text 执行: starcoin% state get resource 0xe1fb7f08be5427c9230e7eea99ce21a7 0xe1fb7f08be5427c9230e7eea99ce21a7::Test::Resource 结果: { "ok": { "raw": "0x0a00000000000000", "json": { "i": 10 } } } ``` **查看链上资源结果:** ![](https://pic2.zhimg.com/80/v2-2f566a0662522b32d3c76bfbbc1958c5_1440w.jpg)### **(3) 调用带参数的脚本** 可以通过带参数的脚本对资源进行修改,以修改链上的状态 ```text 执行: starcoin% account execute-function --function 0xe1fb7f08be5427c9230e7eea99ce21a7::TestScript::write --arg 20u64 结果: 生成新的交易 ``` ### **(4) 查看修改后的资源** 通过查看资源的变化来测试修改的效果 ```text 执行: starcoin% state get resource 0xe1fb7f08be5427c9230e7eea99ce21a7 0xe1fb7f08be5427c9230e7eea99ce21a7::Test::Resource 结果: { "ok": { "raw": "0x0a00000000000000", "json": { "i": 20 } } } ``` **查看链上资源修改结果:** ![](https://pic1.zhimg.com/80/v2-a4582e3ffcd694a980ae30b3c950a480_1440w.jpg)## **七、常见的错误** 在整个项目开发的过程中基本都会遇到一些错误,他们可能发生在编译中,在执行时等等,可以对这些错误进行分类,以便能更好的处理这些问题 ### **1. 编译期错误** 在编写代码时,可能由于疏忽会出现一些语法问题、引用问题,这些问题都是在编译期存在的问题,可以通过move check检测出来。 错误示例: * 语法错误 * 类型错误 * acquire 错误 * 引用错误 ### **2. 链接时错误** 在部署和publish时可能出现链接错误,这些问题大多不会遇到,通过设置依赖、或合约sender等可以解决。 错误示例: * 引用module 不存在 * 引用的 function 参数不匹配 * 合约 sender 不匹配 ### **3. 运行时错误** 运行时的错误是在链上执行时的错误,这些问题需要在编码时做出全面的判断,或者在dev测试时发现问题后及时修补代码等。 错误示例: * 合约中 abort * gas 费不够 * 交易序列号过期 * 交易过期 * 参数类型不匹配 ## **Q & A** 对Move语言的开发,社区的反响也比较强烈,开发者和关注者也提出了一些问题,在此对这些问题进行解答 1、已经部署到链上的合约怎样进行更新? * 对于接口没有变动的合约可以进行直接更新,也可以托管到Dao模块中,通过自发行的Token进行去中心化治理 ,还可以通过设置合约的不可更新让合约固定版本 2、怎么通过 指定seed 链接区块链? * 可以通过starcoin --help 查看 seed的用法,就可以通过指定seed 3、调用线上module 必须要使用高度么? * 是的,必须要指定高度,必须从那个高度分叉出来 4、现在的move有合约模板么? * Move黑客松有很多Move合约,Starcoin的Stdlib也是Move实现的

Move项目的开发实战

  • 本文由Starcoin社区原创 作者:WGB 根据Starcoin & Move直播课《Move开发实战》整理,点击查看原文。

Move 编程语言最早出现在 Facebook 的 Diem 区块链项目中,它是面向数字资产编程的智能合约语言,Move具有多种特性,涉及安全、开发效率等方面。

如果想要完整的开发一个Move语言的项目,个人觉得要了解Move项目的开发流程,相对于其他语言的项目来说,Move语言的基本流程都比较相似,都有开发、单元测试、集成测试、本地发布与调用、链上部署与调用等等。但由于合约编程语言的不同,开发工具与每一项具体步骤也不同,所以对于希望开发Move项目和希望了解Move语言开发的开发者或关注者来说,可以通过本篇《Move项目的开发实战》来了解和熟悉Move项目的开发,需要注意Move语言的开发暂时需要使用类unix系统进行开发,推荐使用MacOS或者ubuntu20.04进行开发。如果没有Mac,可以使用虚拟机下的ubuntu进行开发。

一、新建Move项目

开发项目的第一个步骤就是创建一个新的项目,这个过程可以自己创建项目的树状目录,也可以通过使用 move-cli 进行创建。move-cli是官方推出的一个move的开发工具,有创建、编译、测试等功能,可以从官方的github下载move-cli,也可以通过下载完整的starcoin包后在解压包内找到move-cli,还可以通过clone后自行编译编译的方式获取。

直接下载:

### 1. 使用move-cli创建项目

在下载好move-cli后,可以通过命令创建新的项目,对于move-cli的其他命令可以通过--help 来查看具体功能,随后我们也会在项目过程中使用它们中的一部分。

创建 hello_world 项目

move scaffold hello_world

创建的目录结构:

执行:
    tree hello_world
结果:
    hello_world/
    ├── args.txt
    ├── src
    │   ├── modules
    │   └── scripts
    └── tests
  • src下的modules存放的就是要写的合约代码,scripts存放的是写的脚本代码

二、开发与调试

1. hello world

在创建好目录后就可以在src目录下写脚本和模块,可以在scripts目录中创建一个hello_world.move,并在里面填写代码,代码的含义是在屏幕打印 hello world 的 十进制 ascii 码 vector,这主要是暂时在Move中未支持string类型,这项支持已经在社区中有一些进度,可以等待后续的更新。

(1) hello_world.move

script {
        use 0x1::Debug;
        fun main() {
            Debug::print(&b"hello world");
        }
}

代码示例:

### (2)执行验证

执行:
    move run src/scripts/hello_world.move 
结果:
    [debug] (&) [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]

执行示例:

### 2. 编译

在上一小节的hello world 中使用的是script脚本的方式,但是在Move合约项目中的核心还是module模块,通过module模块中的函数和脚本组合可以实现多种多样的功能。通过对模块的编译,可以将模块部署到区块链中使用,在编译之前也可以通过check功能进行语法检查,以便减少开发中遗漏的问题。

(1) 编写Test.move代码

首先,在src/modules下编写一个Test.move,在其中实现一个自定义的Struct以及创建、修改和销毁Struct函数。

address 0x2{
module Test {
    use 0x1::Signer;

    struct Resource  has key { i: u64 }

    public fun publish(account: &signer) {
        move_to(account, Resource { i: 10 })
    }

    public fun write(account: &signer, i: u64) acquires Resource {
        borrow_global_mut&lt;Resource>(Signer::address_of(account)).i = i;
    }

    public fun unpublish(account: &signer) acquires Resource {
        let Resource { i: _ } = move_from(Signer::address_of(account));
    }
    public fun value_of(addr: address):u64 acquires Resource{
        borrow_global&lt;Resource>(addr).i
    }

}
}

(2) check 编译

对于编写的Move代码,可能在编写的过程中忘记一些符号或着变量的使用。所以可以通过move check命令对代码的语法进行检查编译,如果语法出现错误就会在屏幕中显示,如果语法没有错误则不会打印任何信息。

在move check时默认使用的是stdlib标准库中的库代码,如果想要依赖于链上的已有合约代码,可以通过使用--mode starcoin --starcoin-rpc http://main.seed.starcoin.org:9850 等选项进行链上依赖检查。具体的选项和功能可以使用move check --help 来查看。

执行check:

执行:    
    move check src/modules/Test.move
结果:

执行本地check示例:

链上check:

move check  \
            --mode starcoin \
            --starcoin-rpc http://main.seed.starcoin.org:9850 \
            --block-number 1000000 \
            src/modules/Test.move

执行链上check示例:

## 三、单元测试

在Move开发过程中通过check检查没有语法错误后,依然不能掉以轻心,因为代码中的错误不只有语法错误,更多的是业务逻辑的错误和代码编写中的逻辑错误,对于这些错误,可以使用功能强大的单元测试来针对小范围的代码进行测试。

1. 编写module代码

编写MyModule.move代码进行单元测试,将需要测试的代码用类似宏的方式标记,对于需要测试的项目用 #[test] ,如果不关心代码中的assert的中止码可以使用[expected_failure]跳过,对于只需要在测试下的函数可以使用#[test],对于需要传递参数的函数可以通过#[test(a = @0x1, b = @0x2)]传递参数。

address 0x2{
module MyModule {

    struct MyCoin has key { value: u64 }

    public fun make_sure_non_zero_coin(coin: MyCoin): MyCoin {
        assert(coin.value > 0, 0);
        coin
    }

    public fun has_coin(addr: address): bool {
        exists&lt;MyCoin>(addr)
    }

    #[test]
    fun make_sure_non_zero_coin_passes() {
        let coin = MyCoin { value: 1 };
        let MyCoin { value: _ } = make_sure_non_zero_coin(coin);
    }
    #[test]
    // Or #[expected_failure] if we don't care about the abort code
    #[expected_failure(abort_code = 0)]
    fun make_sure_zero_coin_fails() {
        let coin = MyCoin { value: 0 };
        let MyCoin { value: _ } = make_sure_non_zero_coin(coin);
    }
    #[test_only] // test only helper function
    fun publish_coin(account: &signer) {
        move_to(account, MyCoin { value: 1 })
    }
    #[test(a = @0x1, b = @0x2)]
    fun test_has_coin(a: signer, b: signer) {
        publish_coin(&a);
        publish_coin(&b);
        assert(has_coin(@0x1), 0);
        assert(has_coin(@0x2), 1);
        assert(!has_coin(@0x3), 1);
    }
}
}

2. 执行单元测试

在编写测试代码后,可以通过move unit-test 来测试代码,测试的结果会在终端进行打印,如果测试通过会打印出PASS。在测试时也可以通过不同的选项来查看测试的信息,如: move unit-test -l src/modules/Mymodule.move 可以查看测试项,move unit-test -s src/modules/Mymodule.move 可以在做测试时统计时间等,更多选项与功能可以通过move unit-test --help来查看和使用。

(1)执行单元测试

执行:
    move unit-test src/modules/Mymodule.move
结果:
    Running Move unit tests
    [ PASS    ] 0x2::MyModule::make_sure_non_zero_coin_passes
    [ PASS    ] 0x2::MyModule::make_sure_zero_coin_fails
    [ PASS    ] 0x2::MyModule::test_has_coin
    Test result: OK. Total tests: 3; passed: 3; failed: 0

测试结果:

### (2)查看测试项

执行:
    move unit-test -l src/modules/Mymodule.move
结果:
    0x2::MyModule::make_sure_non_zero_coin_passes: test
    0x2::MyModule::make_sure_zero_coin_fails: test
    0x2::MyModule::test_has_coin: test

测试结果:

### (3)带有统计的单元测试

执行:
    move unit-test -s src/modules/Mymodule.move
结果:
    Running Move unit tests
    [ PASS    ] 0x2::MyModule::make_sure_non_zero_coin_passes
    [ PASS    ] 0x2::MyModule::make_sure_zero_coin_fails
    [ PASS    ] 0x2::MyModule::test_has_coin
​
    Test Statistics:
​
    ┌───────────────────────────────────────────────┬────────────┬───────────────────────────┐
    │                   Test Name                   │    Time    │   Instructions Executed   │
    ├───────────────────────────────────────────────┼────────────┼───────────────────────────┤
    │ 0x2::MyModule::make_sure_non_zero_coin_passes │   0.000    │             1             │
    ├───────────────────────────────────────────────┼────────────┼───────────────────────────┤
    │ 0x2::MyModule::make_sure_zero_coin_fails      │   0.000    │             1             │
    ├───────────────────────────────────────────────┼────────────┼───────────────────────────┤
    │ 0x2::MyModule::test_has_coin                  │   0.000    │             1             │
    └───────────────────────────────────────────────┴────────────┴───────────────────────────┘
​

测试结果:

## 四、功能(集成)测试

单元测试只适用于小范围的测试,当整个需要进行复杂测试时,则需要通过功能测试来详细的测试,功能测试相对于单元测试增加了区块链测试,可以通过定义账号、定义区块的生成以及交易的产生等等来测试项目代码。

1.功能测试的组成

功能测试可以分为个板块:

  • 全局配置
  • 新区块的生成
  • 区块的交易
  • 执行的交易代码

(1) 全局配置

可以指定全局的账户等等

格式为:

//! account: alice, 100000000000,77
//! account: &lt;name> &lt;address> &lt;amount> &lt;sequence-number>`

(2) 新区块的生成

可以在测试中生成区块并指定打包人、区块号和生成时间等等

格式为:

//! block-prologue  
//! author: alice  
//! block-number: 1  
//! block-time: 10000

(3) 区块的交易

可以生成区块的交易并指定发起人、参数、gas费等等

格式为:

//! new-transaction  
//! sender:alice  
//! args: 10u64  
//! max-gas: 7700000  
//! sequence-number:77  
//! gas-price: 1

(4) 执行的交易代码

可以指定发起交易所执行的代码

格式为:

script {  
    use 0x2::Test;  
    use 0x1::Signer;  
    fun main(account: signer, expected: u64){   
        Test::publish(&account);  
        assert(Test::value_of(Signer::address_of(&account)) == expected, 100);  
    }  
}

2.功能测试示例

指定三个账户并分别设置不同的配置,生成新的区块,并生成新的交易来测试

//! account: alice, 10000000000000, 77
//! account: bob, 0x3
//! account: tom,10000000000
​
//! block-prologue
//! author: alice
//! block-number: 1
//! block-time: 10000
​
//! new-transaction
//! sender:alice
//! args: 10u64
//! max-gas: 7700000
//! sequence-number:77
//! gas-price: 1
​
script {
    use 0x2::Test;
    use 0x1::Signer;
    fun main(account: signer, expected: u64){
        Test::publish(&account);
        assert(Test::value_of(Signer::address_of(&account)) == expected, 100);
    }
}
​
//! block-prologue
//! author: alice
//! block-number: 2
//! block-time: 20000

五、合约的本地发布和调用

代码测试后,可以通过move-cli去部署代码,因为在链上部署调用对于测试开发环境比较麻烦,所以优先本地测试调用合约。

1.publish 编译并本地部署

通过check检查编译后的代码就可以通过publish编译生成字节码文件,使用move publish 就可以对代码编译字节码,代码编译后的字节码文件默认存放在当前文件夹的storage中。

publish的编译与check的检查编译类似,在成功以后不会有输出结果,不同的是publish会在hello_world/storage/0x00000000000000000000000000000002/modules目录中生成字节码文件Test.mv,publish和check一样可以使用stdlib或链上的合约进行操作,选项与check相同,可以通过move publish --help 进行查看具体的选项与功能。

执行publish:

执行:
    move publish src/modules/Test.move
结果:

执行结果:

### 2. 查看字节码文件

在通过publish编译出module的字节码后,所有的字节码将在storage/0x00000000000000000000000000000002/modules下产生,如果在调用过程中出现异常,可以通过move view 命令来分析字节码查错

执行:
    move view Test.mv
结果:
    module 2.Test {
        struct Resource has key {
            i: u64
        }
​
        public publish() {
            0: MoveLoc[0](Arg0: &signer)
            1: LdU64(10)
            2: Pack[0](Resource)
            3: MoveTo[0](Resource)
            4: Ret
        }
        public unpublish() {
            0: MoveLoc[0](Arg0: &signer)
            1: Call[3](address_of(&signer): address)
            2: MoveFrom[0](Resource)
            3: Unpack[0](Resource)
            4: Pop
            5: Ret
        }
        public write() {
            0: CopyLoc[1](Arg1: u64)
            1: MoveLoc[0](Arg0: &signer)
            2: Call[3](address_of(&signer): address)
            3: MutBorrowGlobal[0](Resource)
            4: MutBorrowField[0](Resource.i: u64)
            5: WriteRef
            6: Ret
        }
    }

查看字节码的部分结果:

### 3. 本地调用

通过本地的部署后,可以通过写script脚本来调用module代码,以测试和验证module代码

(1) 编写脚本代码

在src/script/下编写publish_resource.move,调用0x2::Test模块下的函数并打印返回值

script {
    use 0x2::Test;
    use 0x1::Debug;
    use 0x1::Signer;
    fun main(account: signer) {
        Test::publish(&account);
        Debug::print(&Test::value_of(Signer::address_of(&account)));
    }
}

(2) 执行脚本

执行脚本以调用Test模块中的函数,在调用时通过move run 调用脚本并指定发起者

执行:
    move run src/scripts/publish_resource.move --signers 0x12345
结果:
    [debug] 10

执行本地脚本:

### (3) view 查看区块链结果

在本地调用之后,既可以通过区块链方式查看结果,也可以通过move view方式来查看。

执行:
    move view storage/0x00000000000000000000000000012345/resources/0x00000000000000000000000000000002::Test::Resource.bcs
结果:
    key 0x00000000000000000000000000000002::Test::Resource {
        i: 10
    }

查看本地调用的资源:

## 六、合约的链上部署和调用

在本地部署测试之后就可以通过dev网络进行链上的部署测试,可以通过starcoin的启动一个dev网络,并使用默认账户进行链上部署和调用合约

1. 启动节点

启动dev节点
执行:
    starcoin -n dev console
结果:
    starcoin%

2. 账户管理

(1)查看账户

查看账户的地址,方便修改module的address

执行:
    starcoin% account show
​
结果:
    {
        "ok": {
            "account": {
            "address": "0xe1fb7f08be5427c9230e7eea99ce21a7",
            "is_default": true,
            "is_readonly": false,
            "public_key": "0xdaa5325889979bf533659448ebca82a13d379c574fe7e9af0b9e06e70c6d971b",
            "receipt_identifier": "stc1pu8ah7z972snujgcw0m4fnn3p5ulvfsv9"
            },
            "auth_key": "0x31c2ab0ea48eff7623eaa5608d96e4f5e1fb7f08be5427c9230e7eea99ce21a7",
            "sequence_number": null,
            "balances": {}
        }
    }

查看账户结果:

### (2)获得STC

获得一些STC用作部署和调用的gas费

执行:
    starcoin% dev get-coin
​
结果:
    txn 0x0ee2eca20d4158b390be31f3fecaeac9d177f05d2e3e9ea489c83cc453ee0c20 submitted.
    {
    "ok": {
        "block_hash": "0x99ffac9baafb80348cd69952de20309c134e84f60316ea16d974b1a8b0c5b85c",
        "block_number": "7",
        "transaction_hash": "0x0ee2eca20d4158b390be31f3fecaeac9d177f05d2e3e9ea489c83cc453ee0c20",
        "transaction_index": 1,
        "state_root_hash": "0x5f8beedeb725c9dd434b200969aa7820b2c65bd3abda1860c7b4c2d5310f5ac9",
        "event_root_hash": "0xdba2769b4e1f4c9170a8ad7b27268debfcabba0bf0e998f2d8fd2e78c0faf252",
        "gas_used": "119769",
        "status": "Executed"
    }
    }

获得STC结果:

### (3)解锁账户

解锁账户以便交易可以签名发出

执行:
    starcoin% account unlock
​
结果:
    {
        "ok": {
            "address": "0xe1fb7f08be5427c9230e7eea99ce21a7",
            "is_default": true,
            "is_readonly": false,
            "public_key": "0xdaa5325889979bf533659448ebca82a13d379c574fe7e9af0b9e06e70c6d971b",
            "receipt_identifier": "stc1pu8ah7z972snujgcw0m4fnn3p5ulvfsv9"
        }
    }

解锁账户结果:

### 3.修改module模块address

修改address 以便可以在链上部署

address 0xe1fb7f08be5427c9230e7eea99ce21a7{
module Test {
    use 0x1::Signer;
​
    struct Resource  has key { i: u64 }
​
    public fun publish(account: &signer) {
        move_to(account, Resource { i: 10 })
    }
​
    public fun write(account: &signer, i: u64) acquires Resource {
        borrow_global_mut&lt;Resource>(Signer::address_of(account)).i = i;
    }
​
    public fun unpublish(account: &signer) acquires Resource {
        let Resource { i: _ } = move_from(Signer::address_of(account));
  }
    public fun value_of(addr: address):u64 acquires Resource{
        borrow_global&lt;Resource>(addr).i
    }
}
module TestScript {
    use 0xe1fb7f08be5427c9230e7eea99ce21a7::Test;
​
    public (script) fun publish(account: signer) {
        Test::publish(&account);
    }
​
    public (script)fun write(account: signer, i: u64) {
        Test::write(&account,i);
    }
​
    public (script)fun unpublish(account: signer){
        Test::unpublish(&account);
    }
    public (script)fun value_of(addr: address):u64 {
        Test::value_of(addr)
    }
}
}

4. 编译部署

(1)编译为字节码

部署时需要使用字节码文件部署,所以先编译为字节码文件

执行:
    move publish
结果:
​

(2)在dev网络下部署

在dev下部署module的字节码,节省成本方便开发

执行:
    starcoin% dev deploy /home/wgb/code/starcoin/hello_world/storage/0xe1fb7f08be5427c9230e7eea99ce21a7/modules/Test.mv -b
    dev deploy /home/wgb/code/starcoin/hello_world/storage/0xe1fb7f08be5427c9230e7eea99ce21a7/modules/TestScript.mv -b
结果:
    生成新的区块交易

5. 调用

(1) 调用脚本

调用publish 脚本测试module代码

执行:
    starcoin% account execute-function --function 0xe1fb7f08be5427c9230e7eea99ce21a7::TestScript::publish
结果:
    生成新的交易

(2) 查看资源

在执行脚本后可以查看资源是否已经被创建,用来验证脚本和module的可用性

执行:
    starcoin% state get resource 0xe1fb7f08be5427c9230e7eea99ce21a7 0xe1fb7f08be5427c9230e7eea99ce21a7::Test::Resource
结果:
    {
        "ok": {
                "raw": "0x0a00000000000000",
                "json": {
                "i": 10
            }
        }
    }

查看链上资源结果:

### (3) 调用带参数的脚本

可以通过带参数的脚本对资源进行修改,以修改链上的状态

执行:
    starcoin% account execute-function --function 0xe1fb7f08be5427c9230e7eea99ce21a7::TestScript::write --arg 20u64
结果:
    生成新的交易

(4) 查看修改后的资源

通过查看资源的变化来测试修改的效果

执行:
    starcoin% state get resource 0xe1fb7f08be5427c9230e7eea99ce21a7 0xe1fb7f08be5427c9230e7eea99ce21a7::Test::Resource
结果:
    {
        "ok": {
                "raw": "0x0a00000000000000",
                "json": {
                "i": 20
            }
        }
    }

查看链上资源修改结果:

## 七、常见的错误

在整个项目开发的过程中基本都会遇到一些错误,他们可能发生在编译中,在执行时等等,可以对这些错误进行分类,以便能更好的处理这些问题

1. 编译期错误

在编写代码时,可能由于疏忽会出现一些语法问题、引用问题,这些问题都是在编译期存在的问题,可以通过move check检测出来。

错误示例:

  • 语法错误
  • 类型错误
  • acquire 错误
  • 引用错误

2. 链接时错误

在部署和publish时可能出现链接错误,这些问题大多不会遇到,通过设置依赖、或合约sender等可以解决。

错误示例:

  • 引用module 不存在
  • 引用的 function 参数不匹配
  • 合约 sender 不匹配

3. 运行时错误

运行时的错误是在链上执行时的错误,这些问题需要在编码时做出全面的判断,或者在dev测试时发现问题后及时修补代码等。

错误示例:

  • 合约中 abort
  • gas 费不够
  • 交易序列号过期
  • 交易过期
  • 参数类型不匹配

Q & A

对Move语言的开发,社区的反响也比较强烈,开发者和关注者也提出了一些问题,在此对这些问题进行解答

1、已经部署到链上的合约怎样进行更新?

  • 对于接口没有变动的合约可以进行直接更新,也可以托管到Dao模块中,通过自发行的Token进行去中心化治理 ,还可以通过设置合约的不可更新让合约固定版本

2、怎么通过 指定seed 链接区块链?

  • 可以通过starcoin --help 查看 seed的用法,就可以通过指定seed

3、调用线上module 必须要使用高度么?

  • 是的,必须要指定高度,必须从那个高度分叉出来

4、现在的move有合约模板么?

  • Move黑客松有很多Move合约,Starcoin的Stdlib也是Move实现的

本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 2021-09-15 16:12
  • 阅读 ( 301 )
  • 学分 ( 5 )
  • 分类:以太坊

评论