0xb23b

Posted on Dec 26, 2021Read on Mirror.xyz

【区块链学习笔记】一起入门DAPP智能合约开发

初学以太坊智能合约

直接打开 http://trufflesuite.com/tutorial/index.html 有新手指导区,英语好的老板可以直接参考原文,下面是根据教程加上我的体验进行的翻译和实践记录。

这个教程可以让你体验做一个dapp宠物商店(pet shop),通过这个教程可以完整的体验一次合约的开发。

这个教程主要会让我们体验这么几个过程

  • 设置开发环境
  • 使用 Truffle Box 创建 Truffle 项目
  • 编写智能合约
  • 编译和迁移智能合约
  • 测试智能合约
  • 创建用户界面以与智能合约交互
  • 在浏览器中与 dapp 交互

设置开发环境

需要安装

  • Nodejs
  • Git

上述两个安装完成之后,npm安装 truffle

# 安装truffle
npm install -g truffle

# 确认是否安装完成
truffle version

使用 Truffle Box 创建 Truffle 项目

# 新建文件夹
mkdir pet-shop-tutorial
# 进入文件夹
cd pet-shop-tutorial
# 使用truffle 创建项目框架
truffle unbox pet-shop

ps:介于国内的网络原因,这一步会经常失败,老板可以考虑科学上网,或者失败之后多运行几次,总有成功的。等不了的可以直接去clone他的远嘛到该目录 https://github.com/trufflesuite/pet-shop-tutorial

成功之后就有如下的目录结构

挑选主要文件讲解一下

├── LICENSE
├── contracts  #包含我们智能合约的 Solidity 源文件。这里有一个重要的契约叫做Migrations.sol,后续教程会讲到
├── migrations #Truffle 使用迁移系统来处理智能合约部署。迁移是一个额外的特殊智能合约,用于跟踪更改。
├── src
├── test # 包含我们智能合约的 JavaScript 和 Solidity 测试
└── truffle-config.js # 配置文件

开始写第一个智能合约

1.在 contracts文件夹下面创建 Adoption.sol

2.添加如下内容到新建的文件

# 注明了 solidity的合约最低版本,^ 表示需要更高的版本,即向上兼容
pragma solidity ^0.5.0;

contract Adoption {

}

设置变量

Solidity 是一种静态类型语言,这意味着必须定义字符串、整数和数组等数据类型。 Solidity 有一种独特的类型,称为地址。地址是以太坊地址,存储为 20 字节值。以太坊区块链上的每个账户和智能合约都有一个地址,可以从该地址发送和接收以太币。

pragma solidity ^0.5.0;

contract Adoption {
	address[16] public adopters;
}
  • 我们定义了一个单一的变量:adopters。这是一个以太坊地址的数组。数组包含一种类型,可以有一个固定或可变的长度。在这种情况下,类型是地址,长度是16。
  • 你还会注意到adopters是公开的。公共变量有自动的getter方法,但在数组的情况下,需要一个key,并且只会返回一个值。稍后,我们将写一个函数来返回整个数组,以便在我们的用户界面中使用。

开发第一个方法:收养一只宠物

// Adopting a pet
function adopt(uint petId) public returns (uint) {
  require(petId >= 0 && petId <= 15);

  adopters[petId] = msg.sender;

  return petId;
}
  • 在Solidity中,函数参数和输出的类型都必须被指定。在这种情况下,我们将接收一个petId(整数)并返回一个整数。
  • 我们正在检查以确保petId是在我们的收养者数组的范围内。Solidity中的数组是从0开始索引的,所以ID值需要在0和15之间。我们使用require()语句来确保ID是在范围内。
  • 如果ID在范围内,我们就把进行呼叫的地址添加到我们的收养者数组中。调用此函数的人或智能合约的地址用msg.sender表示。
  • 最后,我们返回提供的宠物ID作为确认。

开发第二个方法:返回收养数据

// Retrieving the adopters
function getAdopters() public view returns (address[16] memory) {
  return adopters;
}
  • 由于adopters已经被声明,我们可以简单地返回它。确保指定返回类型(在本例中,adopters的类型)为address[16] memory。 memory给出了变量的数据位置。
  • 函数声明中的view关键字意味着该函数将不会修改合同的状态。关于视图所施加的确切限制的进一步信息,可在这里找到。

编译和迁移智能合约

Solidity是一种编译语言,意味着我们需要将我们的Solidity编译成字节码,以便Ethereum虚拟机(EVM)执行。把它看作是把我们的人类可读的Solidity翻译成EVM能理解的东西。 在终端中,确保你在包含dapp的目录的根部,并输入。

truffle compile

迁移到区块链

现在,我们已经成功地编译了我们的合约,是时候将它们迁移到区块链上了!这就是我们的工作。

你会看到在migrations/目录下已经有一个JavaScript文件。1_initial_migration.js。这是处理部署Migrations.sol合约以观察后续的智能合约迁移,并确保我们在未来不会重复迁移未改变的合约。 现在我们准备创建我们自己的迁移脚本。 1.在migrations/目录下创建一个名为2_deploy_contracts.js的新文件。 2.在2_deploy_contracts.js文件中添加以下内容。

var Adoption = artifacts.require("Adoption");module.exports = function(deployer) {  deployer.deploy(Adoption);};

安装Ganache

下载地址:http://trufflesuite.com/ganache

Ganache是一个快速启动个人以太坊区块链,您可以使用它来运行测试、执行命令和检查状态,同时控制链的运行方式。如果不用他,直接在真实的网络部署测试的话,是需要花费eth的。安装这个在本地就不需要了。

这个很简单,下载安装就好,然后双击运行,就可以看到本地跑起来了。

本地的端口跑在 HTTP://127.0.0.1:7545

使用 lsof -i:7544 确认已经成功运行

运行迁移

truffle migrate

然后你就会得到一堆迁移部署的消息

Compiling your contracts...===========================> Everything is up to date, there is nothing to compile.Starting migrations...======================> Network name:    'development'> Network id:      5777> Block gas limit: 6721975 (0x6691b7)1_initial_migration.js======================   Replacing 'Migrations'   ----------------------   > transaction hash:    0xf848aad72fb6ab403f201ff2fa1113d959d1ecb7fe331260752c263925ca403c   > Blocks: 0            Seconds: 0   > contract address:    0xC0112c7071c9Fab2f42D438B738D51b367fca38a   > block number:        1   > block timestamp:     1638962361   > account:             0x4035a5D4709fD2Cb87160Bd29ca5eD88C9E5765F   > balance:             99.99616114   > gas used:            191943 (0x2edc7)   > gas price:           20 gwei   > value sent:          0 ETH   > total cost:          0.00383886 ETH   > Saving migration to chain.   > Saving artifacts   -------------------------------------   > Total cost:          0.00383886 ETH2_deploy_contracts.js=====================   Replacing 'Adoption'   --------------------   > transaction hash:    0xabfd61f1d738ab26c5c0f65ee9cc37787d3cc46656cf9ce59c559affeabbb71b   > Blocks: 0            Seconds: 0   > contract address:    0x705b3e834149c2eF5bbE00440474f0aC3327451A   > block number:        3   > block timestamp:     1638962361   > account:             0x4035a5D4709fD2Cb87160Bd29ca5eD88C9E5765F   > balance:             99.99123784   > gas used:            203827 (0x31c33)   > gas price:           20 gwei   > value sent:          0 ETH   > total cost:          0.00407654 ETH   > Saving migration to chain.   > Saving artifacts   -------------------------------------   > Total cost:          0.00407654 ETHSummary=======> Total deployments:   2> Final cost:          0.0079154 ETH

最终花费了0.0079154的eth,如果直接在线上的话就需要花钱了。。

创建用户界面以与智能合约交互

实例化web3 在文本编辑器中打开/src/js/app.js 检查该文件。注意,有一个全局的App对象来管理我们的应用程序,在init()中加载宠物数据,然后调用函数initWeb3()。web3的JavaScript库与以太坊区块链进行互动。它可以检索用户账户,发送交易,与智能合约互动,等等。

web3的api可以参考 https://github.com/ethereum/web3.js/

这段代码本质上就是先获得一个web3的对象,然后通过该web3api与合约进行交互,大部分都是封装好了的api。

最开始我们写了2个合约的方法function adopt function getAdopters() 定义了一个类型为address的变量adopters

1.初始化web3

2.初始化合约。初始化合约的时候载入Adoption.json这个文件,该文件是我们执行truffle compile生成的。这个文件包含了合约的工程文件(artifact file),Artifacts是关于我们合同的信息,比如它的部署地址和应用二进制接口(ABI)。ABI是一个JavaScript对象,定义了如何与合同互动,包括其变量、函数及其参数。( Artifacts are information about our contract such as its deployed address and Application Binary Interface (ABI). The ABI is a JavaScript object defining how to interact with the contract including its variables, functions and their parameters.)

3.markAdopted判断是否已经被买了,通过adoptionInstance.getAdopters.call();调用所有记录的adopters地址,如果不是初始化的地址,就标记为success,并且disable。返回的合约信息如下图:

4.bindEvents绑定事件,如果可以购买,则触发领养 App.contracts.Adoption.deployed(),然后刷新UI即可。

// app.jsApp = {  web3Provider: null,  contracts: {},  init: async function () {    // Load pets.    $.getJSON("../pets.json", function (data) {      var petsRow = $("#petsRow");      var petTemplate = $("#petTemplate");      for (i = 0; i < data.length; i++) {        petTemplate.find(".panel-title").text(data[i].name);        petTemplate.find("img").attr("src", data[i].picture);        petTemplate.find(".pet-breed").text(data[i].breed);        petTemplate.find(".pet-age").text(data[i].age);        petTemplate.find(".pet-location").text(data[i].location);        petTemplate.find(".btn-adopt").attr("data-id", data[i].id);        petsRow.append(petTemplate.html());      }    });    return await App.initWeb3();  },  initWeb3: async function () {    // Modern dapp browsers...    if (window.ethereum) {      App.web3Provider = window.ethereum;      try {        // Request account access        await window.ethereum.request({ method: "eth_requestAccounts" });      } catch (error) {        // User denied account access...        console.error("User denied account access");      }    }    // Legacy dapp browsers...    else if (window.web3) {      App.web3Provider = window.web3.currentProvider;    }    // If no injected web3 instance is detected, fall back to Ganache    else {      App.web3Provider = new Web3.providers.HttpProvider(        "http://localhost:7545"      );    }    web3 = new Web3(App.web3Provider);    return App.initContract();  },  initContract: function () {    $.getJSON("Adoption.json", function (data) {      // Get the necessary contract artifact file and instantiate it with @truffle/contract      var AdoptionArtifact = data;      App.contracts.Adoption = TruffleContract(AdoptionArtifact);      // Set the provider for our contract      App.contracts.Adoption.setProvider(App.web3Provider);      // Use our contract to retrieve and mark the adopted pets      return App.markAdopted();    });    return App.bindEvents();  },  bindEvents: function () {    $(document).on("click", ".btn-adopt", App.handleAdopt);  },  markAdopted: function () {    var adoptionInstance;    App.contracts.Adoption.deployed()      .then(function (instance) {        adoptionInstance = instance;        return adoptionInstance.getAdopters.call();      })      .then(function (adopters) {        for (i = 0; i < adopters.length; i++) {          if (adopters[i] !== "0x0000000000000000000000000000000000000000") {            $(".panel-pet")              .eq(i)              .find("button")              .text("Success")              .attr("disabled", true);          }        }      })      .catch(function (err) {        console.log(err.message);      });  },  handleAdopt: function (event) {    event.preventDefault();    var petId = parseInt($(event.target).data("id"));    var adoptionInstance;    web3.eth.getAccounts(function (error, accounts) {      if (error) {        console.log(error);      }      var account = accounts[0];      App.contracts.Adoption.deployed()        .then(function (instance) {          adoptionInstance = instance;          // Execute adopt as a transaction by sending account          return adoptionInstance.adopt(petId, { from: account });        })        .then(function (result) {          return App.markAdopted();        })        .catch(function (err) {          console.log(err.message);        });    });  },};$(function () {  $(window).load(function () {    App.init();  });});

安装metamask进行测试

先安装metamask插件,可以在chrome插件商城安装。

如果有了直接点击添加网络

按照如下资料进行填写

这里的http://127.0.0.1:7545就是你本地启动Gnanche的时候启动的

填写完成之后,选择网络,把网络切换到本地测试网络

点击小狐狸插件头像,切换网络即可

切换到本地网络之后,可以直接倒入本地的账号

点击钥匙图标,查看本地的私钥,然后选择小狐狸直接倒入。

倒入成功之后,你账号就有了99.99eth,其他账号没有消耗的话应该都是100ETH

运行你的首个DAPP

回到开发目录下

npm run dev

首个DAPP就跑起来了。点击adopt就可以唤起小狐狸进行领养和gas支付了。

那么至此,基本上就了解了一个DAPP的运行逻辑了。

回顾

梳理一下,就是我们编写了一个solidity的合约,需要先编译成机器码,然后再部署到ETH的网络,部署的时候需要消耗GAS。

部署完成之后,我们就可以通过用户UI去进行交互,调用metamask去与底层web3API进行交流。比如这个demo就是领养宠物,在游戏打金领域就是氪金挖矿了。当然我们可以直接跳过这些,直接用web3去跟合约交流了。

更深入的了解需要学习一下solidity的语法,然后根据游戏去挖掘相应合约的方法调用即可。

可以继续参考老板的未公开合约的游戏怎么制作脚本 思路和方法已经讲明白了,剩下的就靠自己多领悟和学习了

举一反三:多多实践

批量创建账号

既然本地已经有了eth的网络环境了,那么我是不是可以通过web3 api直接批量创建钱包地址呢?

之前一直想看老板批量创建metamask账号的的代码,可惜权限不够,这回理解了应该可以自己动手了~

https://web3js.readthedocs.io/en/v1.2.11/web3-eth-accounts.html#example

查看API,有一个account的接口。通过以下代码即可创建一个账号,后续再通过循环就可以创建和存储了。

//通过web3 api 创建账号const accepts = require('accepts');var Web3 = require('web3');web3 = new Web3(new Web3.providers.HttpProvider("http://127.0.0.1:7545"));let account = web3.eth.accounts.create('test');let keystore = account.encrypt('test');console.log(account, keystore);/***{  address: '0x3E12932eF648bE841e03db1CE46Faec4aFc9AAe1',  privateKey: '0x4291e1585205b5a0217ecea837f2070ea2100a401f753806c15a93dfb84c3661',  signTransaction: [Function: signTransaction],  sign: [Function: sign],  encrypt: [Function: encrypt]} {  version: 3,  id: '8a2ded2a-8469-43d4-88a0-3e4169012d54',  address: '3e12932ef648be841e03db1ce46faec4afc9aae1',  crypto: {    ciphertext: 'e7492608eb06ee703909b6348054f8b98bc30a79a5d4628fad72e012c4a50075',    cipherparams: { iv: 'a2b68822d9b03e788ad8df3a57c95cec' },    cipher: 'aes-128-ctr',    kdf: 'scrypt',    kdfparams: {      dklen: 32,      salt: 'f513ee04af1662d1adab37e8962423d9d4ff65218aadce7b88063eb6e6d1ece0',      n: 8192,      r: 8,      p: 1    },    mac: 'eadc2bb27d5fe6c8889caee2f897eabde4cd9254005af9bdb11f5c7989fe6837'  }}***/

尝试转账

假设我已经创建了一个 0x3E12932eF648bE841e03db1CE46Faec4aFc9AAe1的新账号,

然后我本地导入metamask的账号已经有了99.99eth,我想给新账号转账。

直接通过metamask转账即可。

转账成功,可以用代码查询新建账号的余额,查询成功~

var Web3 = require('web3');//创建 rpc 连接字符串var rpcstring = 'http://127.0.0.1:7545'//创建ws连接字符串var wstring = 'wss://bsc-ws-node.nariox.org:443';// var wscweb3 = new Web3(new Web3.providers.WebsocketProvider(wstring ));var rpcweb3 = new Web3(new Web3.providers.HttpProvider(rpcstring ));//设置web3 使用rpcweb3模式web3 = rpcweb3;const getBNBBalance = async (address) =>{    let result = await web3.eth.getBalance(address)    //由于使用的是大数模式,小数点有18位,所以获得的balance 要除以10^18次方才是正确的数据    //或者使用自带的转换工具    let balance = web3.utils.fromWei(result.toString(10), getweiname());    //打印结果    console.log("地址:" + address +"有" + balance +"个ETH");    return balance;}//通过小数点多少位,转换对应的数据function getweiname(tokendecimals = 18) {  weiname = 'ether';  switch (tokendecimals) {      case 3:          weiname = "Kwei";          break;      case 6:          weiname = 'mwei';          break;      case 9:          weiname = 'gwei';          break;      case 12:          weiname = 'microether ';          break;      case 15:          weiname = 'milliether';          break;      case 18:          weiname = 'ether';          break;  }  return weiname;}let banlance = getBNBBalance('0x3E12932eF648bE841e03db1CE46Faec4aFc9AAe1');console.log('banlance:' + banlance);//banlance:[object Promise]//地址:0x3E12932eF648bE841e03db1CE46Faec4aFc9AAe1有88个ETH

后续

可以再试试通过web3api实现批量转账等测试,等有时间再试试。作为小白,通过这个教程大致梳理了一下整个DAPP的开发流程,如果有纰漏也请各位老板指正。感谢感谢~