初学以太坊智能合约

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

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

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

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

设置开发环境

需要安装

  • Nodejs
  • Git

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

1
2
3
4
5
# 安装truffle
npm install -g truffle

# 确认是否安装完成
truffle version

使用 Truffle Box 创建 Truffle 项目

1
2
3
4
5
6
# 新建文件夹
mkdir pet-shop-tutorial
# 进入文件夹
cd pet-shop-tutorial
# 使用truffle 创建项目框架
truffle unbox pet-shop

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

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

挑选主要文件讲解一下

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

开始写第一个智能合约

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

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

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

contract Adoption {

}

设置变量

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

1
2
3
4
5
pragma solidity ^0.5.0;

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

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

1
2
3
4
5
6
7
8
// 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作为确认。

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

1
2
3
4
// 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的目录的根部,并输入。

1
truffle compile

迁移到区块链

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

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

1
2
3
4
5
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 确认已经成功运行

运行迁移

1
truffle migrate

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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 ETH


2_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 ETH


Summary
=======
> 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即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// app.js
App = {
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

回到开发目录下

1
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的接口。通过以下代码即可创建一个账号,后续再通过循环就可以创建和存储了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//通过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转账即可。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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的开发流程,如果有纰漏也请各位老板指正。感谢感谢~