dotNetCore使用以太坊区块链简介

本文描述了在dotNet核心中使用像以太坊这样的区块链平台的过程。目标受众是其他想要从以太坊开始的dotNet开发者。需要了解区块链。在本文中,我们构建了一个完整的示例,允许你与自定义编写的智能合约进行交互。

第一代区块链的可以被视为仅比特币而没有智能合约。尽管如此,第二代区块链的表现明显给人更有希望。随着比特币以外的更多区块链平台,变得更加成熟,区块链有了更多可能性。以太坊区块链更像是一个使用加密货币的智能合约的分布式分类账。以太坊的重点更多地放在智能合约部分,然后是加密货币。以太币(以太坊的加密货币)的目的是为执行采矿合约或执行合约的交易提供报酬。

智能合约是为以太坊虚拟机编写的一段代码。这可以用Solidity编写并编译为字节代码。此字节代码放在分类帐中并变为不可变但仍可以与之交互,并且可以更改状态。正如以太坊文档所说:“从实用的角度来看,EVM可以被认为是一个包含数百万个对象的大型分散计算机,称为”帐户“,它们能够维护内部数据库,执行代码并相互通信。“从开发人员的角度来看,你可以将Solidity视为类似Javascript的语言,这有点受限。由于Solidity代码在区块链中运行,因此有充分的理由限制它。像随机数这样简单的东西也是一个挑战。也无法通过Http调用获取数据,因为所有事实需要在系统中。你仍然可以调用合约并输入数据来改变状态,因此外部影响是可行的。

首先安装Mist浏览器Geth。Mist浏览器是一个GUI,可用作Ether的钱包。Geth是代码连接到的程序接口,Geth连接到以太坊的区块链。对于本文,我们将使用testnet。这样我们就可以免费开采一些以太币。启动Mist后,从菜单中选择使用测试网。创建一个帐户并挖掘一些以太币(菜单项目开发并开始挖掘)。

过了一段时间,你会有一些以太币。这在交易时很方便。即使发布合约或执行合约也要花费成本。现在让我们关闭钱包,否则你无法打开一个新的geth过程。所以在控制台中启动已安装的Geth:

1
“\Program Files\Geth\geth” --testnet --rpcapi eth,web3,personal --rpc

上图是我们命令的结果。我们看到它正在接收当前的区块链缓存,并且它的http端点正在localhost:8545上进行侦听。这很重要,因为我们需要Mist浏览器和其他应用程序使用IPC或RPC访问它。由于在Windows上只支持IPC实现,我们不能在dotNetCore中使用它。我们在解决方案中使用web3 RPC

现在你可以再次打开钱包。只是不能开始挖掘,因为有独立的Geth正在运行。

现在是时候开始开发,打开Visual Studio并创建一个新项目了。请注意,我们的Github提供了该代码。创建“ASP.NET核心Web应用程序”,然后选择“Web.API模板”。我们将创建一个服务,其中包含一些与区块链交互的方法,并向区块链发布合约。这个存钱合约将存储我们的代币余额。合约开采后我们可以调用合约方法。没什么高大上的,也不是一个完整的应用程序,但很高兴看到我们能做什么。我们选择使用Azure Table存储来保持系统的持久性,它快速且便宜。

首先将这些依赖项添加到Project.json中:

1
2
3
"Nethereum.Web3": "2.0.0-rc1",
"Portable.BouncyCastle": "1.8.1.1",
"WindowsAzure.Storage": "8.1.1"

保存并查看正在恢复的软件包。前两个是以太坊相关,最后一个用于表存储。Nethereum.Web3是通过RPC json访问本地Geth进程的完整类库。BouncyCastle是Nethereum所需的加密库。

首先,我们需要一个模型来捕获我们的以太坊合约状态。以太坊没有任何选择让合约退出区块链,主要是出于安全/不可变的原因。一旦合约被放入区块链,就无法更改,也无法检索到Solidity代码。这就是我们需要将这些信息存储在我们的系统中的原因。在模型文件夹中创建一个名为EthereumContractInfo的文件,该文件派生自Azure Storage类TableEntity

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
using Microsoft.WindowsAzure.Storage.Table;

namespace EthereumStart.Models
{
public class EthereumContractInfo : TableEntity
{
public string Abi { get; set; }
public string Bytecode { get; set; }
public string TransactionHash { get; set; }
public string ContractAddress { get; set; }

public EthereumContractInfo()
{

}

public EthereumContractInfo(string name, string abi, string bytecode, string transactionHash)
{
PartitionKey = "contract";
RowKey = name;
Abi = abi;
Bytecode = bytecode;
TransactionHash = transactionHash;
}
}
}

现在创建一个名为Services的文件夹并创建文件IEthereumService接口,这样我们就可以将它用于依赖注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System.Threading.Tasks;
using EthereumStart.Models;
using Nethereum.Contracts;

namespace EthereumStart.Services
{
public interface IEthereumService
{
string AccountAddress { get; set; }
Task<bool> SaveContractToTableStorage(EthereumContractInfo contract);
Task<EthereumContractInfo> GetContractFromTableStorage(string name);
Task<decimal> GetBalance(string address);
Task<bool> ReleaseContract(string name, string abi, string byteCode, int gas);
Task<string> TryGetContractAddress(string name);
Task<Contract> GetContract(string name);
}
}

所有方法都应该返回一个任务,因为我们希望使实现使用异步。我们的想法是,我们将发布合约,尝试获取它的地址,然后在该地址上调用它的方法。现在我们创建文件BasicEthereumService来实现接口。

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
135
136
137
138
139
140
141
142
143
using Microsoft.Extensions.Options;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Auth;
using Microsoft.WindowsAzure.Storage.Table;
using Nethereum.Web3;
using System;
using System.Threading.Tasks;
using EthereumStart.Models;
using Nethereum.Contracts;

namespace EthereumStart.Services
{
public class BasicEthereumService : IEthereumService
{
private Nethereum.Web3.Web3 _web3;
private string _accountAddress;
private string _password;
private string _storageKey;
private string _storageAccount;

public string AccountAddress
{
get
{
return _accountAddress;
}

set
{
_accountAddress = value;
}
}

public BasicEthereumService(IOptions<EthereumSettings> config)
{
_web3 = new Web3("http://localhost:8545");
_accountAddress = config.Value.EhtereumAccount;
_password = config.Value.EhtereumPassword;
_storageAccount = config.Value.StorageAccount;
_storageKey = config.Value.StorageKey;
}

public async Task<bool> SaveContractToTableStorage(EthereumContractInfo contract)
{
StorageCredentials credentials = new StorageCredentials(_storageAccount, _storageKey);
CloudStorageAccount account = new CloudStorageAccount(credentials, true);
var client = account.CreateCloudTableClient();

var tableRef = client.GetTableReference("ethtransactions");
await tableRef.CreateIfNotExistsAsync();

TableOperation ops = TableOperation.InsertOrMerge(contract);
await tableRef.ExecuteAsync(ops);
return true;
}

public async Task<EthereumContractInfo> GetContractFromTableStorage(string name)
{
StorageCredentials credentials = new StorageCredentials(_storageAccount, _storageKey);
CloudStorageAccount account = new CloudStorageAccount(credentials, true);
var client = account.CreateCloudTableClient();

var tableRef = client.GetTableReference("ethtransactions");
await tableRef.CreateIfNotExistsAsync();

TableOperation ops = TableOperation.Retrieve<EthereumContractInfo>("contract", name);
var tableResult = await tableRef.ExecuteAsync(ops);
if (tableResult.HttpStatusCode == 200)
return (EthereumContractInfo)tableResult.Result;
else
return null;
}

public async Task<decimal> GetBalance(string address)
{
var balance = await _web3.Eth.GetBalance.SendRequestAsync(address);
return _web3.Convert.FromWei(balance.Value, 18);
}

public async Task<bool> ReleaseContract(string name, string abi, string byteCode, int gas)
{

// check contractName
var existing = await this.GetContractFromTableStorage(name);
if (existing != null) throw new Exception($"Contract {name} is present in storage");
try
{
var resultUnlocking = await _web3.Personal.UnlockAccount.SendRequestAsync(_accountAddress, _password, 60);
if (resultUnlocking)
{
var transactionHash = await _web3.Eth.DeployContract.SendRequestAsync(abi, byteCode, _accountAddress, new Nethereum.Hex.HexTypes.HexBigInteger(gas), 2);

EthereumContractInfo eci = new EthereumContractInfo(name, abi, byteCode, transactionHash);
return await SaveContractToTableStorage(eci);
}
}
catch (Exception exc)
{
return false;
}
return false;
}

public async Task<string> TryGetContractAddress(string name)
{
// check contractName
var existing = await this.GetContractFromTableStorage(name);
if (existing == null) throw new Exception($"Contract {name} does not exist in storage");

if (!String.IsNullOrEmpty(existing.ContractAddress))
return existing.ContractAddress;
else
{
var resultUnlocking = await _web3.Personal.UnlockAccount.SendRequestAsync(_accountAddress, _password, 60);
if (resultUnlocking)
{
var receipt = await _web3.Eth.Transactions.GetTransactionReceipt.SendRequestAsync(existing.TransactionHash);
if (receipt != null)
{
existing.ContractAddress = receipt.ContractAddress;
await SaveContractToTableStorage(existing);
return existing.ContractAddress;
}
}
}
return null;
}

public async Task<Contract> GetContract(string name)
{
var existing = await this.GetContractFromTableStorage(name);
if (existing == null) throw new Exception($"Contract {name} does not exist in storage");
if (existing.ContractAddress == null) throw new Exception($"Contract address for {name} is empty. Please call TryGetContractAddress until it returns the address");

var resultUnlocking = await _web3.Personal.UnlockAccount.SendRequestAsync(_accountAddress, _password, 60);
if (resultUnlocking)
{
return _web3.Eth.GetContract(existing.Abi, existing.ContractAddress);
}
return null;
}
}
}

这是很多代码。我将跳过SaveLoad -ContractFromTableStorage,因为这些只是简单的Azure表交互。

在构造函数中,我们看到与Geth进程的连接,我们连接到端口8545,因此它可以进行RPC json通信。

第一个方法实现的是getBalance。由于一切都围绕金钱,所以检查地址的以太币的余额是很重要的,比如你的账户,钱包甚至合约。在此示例中,所有以太坊交互都通过对象web3完成。在我们在Wei中取得余额之后,这就像是人民币的分数,然后是10^18因子而不是10^2。我们可以使用convert.FromWEi将其转换回以太币。

第二个方法实现的是ReleaseContract。它首先检查我们是否尚未发布合约并将其保留在存储中。如果没有,我们可以开始解锁帐户120秒。当我们想要部署合约或其他东西时,需要解锁。之后,我们可以调用deploy方法并获取交易哈希。这是必要的,因为现在合约将被开采。将挖掘视为区块链的同行所做的过程,以便合约被接受到区块链中。当12个同行已经这样做时,合约地址被退回。这个挖掘过程需要花钱(又名Gas),并且会从你输入的_accountAddress中扣除。这个数量在Wei中,我们在控制器中指定它,它将调用EthereumService。每份合约都有不同的汽油价格。编译合约时可以使用此值。我们可以在方法SendRequestAsync中指定合约构造函数参数。在我们的情况下,我们指定2,因为合约发布时我们的余额应为2个以太币。

如上所述,必须挖掘部署才能获得合约地址。我们需要这个地址来调用它上面的方法。在我们的TryGetContractAddress中,我们检查我们的合约是否已经在我们的表存储中有一个地址,如果没有,我们会询问以太坊区块链。如果GetTransactionReceipt返回有效地址,我们可以保留它。

我们服务的最后一个方法是GetContract,这只是对以太坊合约的引用。如你所见,合约必须存在于表存储中才能获得合约地址。我们将在下一部分之后讨论调用合约。

所以现在我们从dotNet离开下,转到solidity程序语言。首先让我们看看我们的测试合法性;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pragma solidity ^0.4.6;
contract CoinsContract {
uint public balance;
function CoinsContract(uint initial) {
balance = initial;
}
function addCoins(uint add) returns (uint b) {
b = balance + add;
return b;
}
function subtractCoins(uint add) returns (uint b) {
b = balance - add;
return b;
}
}

它只是一个基于其构造函数值的piggybank从该余额开始。然后我们可以调用加法和减法来修改我们的代币余额。我知道这是非常基本的但是一开始总是好的,对吗?合约发布后,我们可以从dotNet代码中调用addCointssubtractCoints方法。那你为什么要这样做呢?它只会花费我们以太?好的好处是,每次调用方法都会被添加到分配分类帐中,因此可以在https://testnet.etherscan.io/查看。

为了发布这个合约,我们需要将它编译为字节代码。我们使用Remix网站这个基于网络的基本编辑器可以编译和测试你的合约。编译完成后,我们可以获得字节代码(请不要忘记前面的0x)和接口,也称为ABI。在签订合约时需要提供这两个部件。ABI代表应用程序二进制接口,就像Web服务的WSDL一样。

回到Visual Studio,在我们发布合约并开始调用方法之前,我们只需再做四个步骤。

首先,我们创建名为EthereumSettings的设置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace EthereumStart.Model
{
public class EthereumSettings
{
public EthereumSettings()
{
}
public string EhtereumAccount { get; set; }
public string EhtereumPassword { get; set; }
public string StorageKey { get; set; }
public string StorageAccount { get; set; }

}
}

其次,我们将这些设置添加到appsettings.json:

1
2
3
4
"ehtereumAccount": "x",
"ehtereumPassword": "y",
"storageKey": "w",
"storageAccount": "v"

当然,不是使用这些值,而是使用你自己的以太坊帐户和密码以及Azure存储帐户和密钥。

第三,我们在我们的startup.cs中添加了ConfigureServices方法中的代码:

1
2
services.Configure<EthereumSettings>(Configuration);
services.AddScoped<IEthereumService, BasicEthereumService>();

对于我们的最后一步,添加一个名为EthereumTestController的控制器,内容应该是:

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
using EthereumStart.Services;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Threading.Tasks;

namespace EthereumStart.Controllers
{
[Route("api/[controller]")]
public class EthereumTestController : Controller
{
private IEthereumService service;
private const string abi = @"[{""constant"":false,""inputs"":[{""name"":""add"",""type"":""uint256""}],""name"":""addCoins"",""outputs"":[{""name"":""b"",""type"":""uint256""}],""payable"":false,""type"":""function""},{""constant"":false,""inputs"":[{""name"":""add"",""type"":""uint256""}],""name"":""subtractCoins"",""outputs"":[{""name"":""b"",""type"":""uint256""}],""payable"":false,""type"":""function""},{""constant"":true,""inputs"":[],""name"":""balance"",""outputs"":[{""name"":"""",""type"":""uint256""}],""payable"":false,""type"":""function""},{""inputs"":[{""name"":""initial"",""type"":""uint256""}],""payable"":false,""type"":""constructor""}]";
private const string byteCode = "0x6060604052341561000c57fe5b604051602080610185833981016040528080519060200190919050505b806000819055505b505b610143806100426000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630173e3f41461005157806349fb396614610085578063b69ef8a8146100b9575bfe5b341561005957fe5b61006f60048080359060200190919050506100df565b6040518082815260200191505060405180910390f35b341561008d57fe5b6100a360048080359060200190919050506100f8565b6040518082815260200191505060405180910390f35b34156100c157fe5b6100c9610111565b6040518082815260200191505060405180910390f35b600081600054019050806000819055508090505b919050565b600081600054039050806000819055508090505b919050565b600054815600a165627a7a723058200085d6d7778b3c30ba2e3bf4af4c4811451f7367109c1a9b44916d876cb67c5c0029";
private const int gas = 4700000;

public EthereumTestController(IEthereumService ethereumService)
{
service = ethereumService;
}

[HttpGet]
[Route("getBalance/{walletAddress}")]
public async Task<decimal> GetBalance([FromRoute]string walletAddress)
{
return await service.GetBalance(walletAddress);
}
[HttpGet]
[Route("releaseContract/{name}")]
public async Task<bool> ReleaseContract([FromRoute] string name)
{
return await service.ReleaseContract(name, abi, byteCode, gas);
}

[HttpGet]
[Route("checkContract/{name}")]
public async Task<bool> CheckContract([FromRoute] string name)
{
return await service.TryGetContractAddress(name) != null;
}

[HttpGet]
[Route("exeContract/{name}/{contractMethod}/{value}")]
public async Task<string> ExecuteContract([FromRoute] string name, [FromRoute] string contractMethod, [FromRoute] int value)
{
string contractAddress = await service.TryGetContractAddress(name);
var contract = await service.GetContract(name);
if (contract == null) throw new System.Exception("Contact not present in storage");
var method = contract.GetFunction(contractMethod);
try
{
// var result = await method.CallAsync<int>(value);
var result = await method.SendTransactionAsync(service.AccountAddress, value);
return result.ToString();
}
catch (Exception ex)
{
return "error";
}
}

[HttpGet]
[Route("checkValue/{name}/{functionName}")]
public async Task<int> CheckValue([FromRoute] string name, [FromRoute] string functionName)
{
string contractAddress = await service.TryGetContractAddress(name);
var contract = await service.GetContract(name);
if (contract == null) throw new System.Exception("Contact not present in storage");
var function = contract.GetFunction(functionName);

var result = await function.CallAsync<int>();

return result;
}
}
}

它看起来很多代码,但它是一些方法。首先,我们有合约的ABI和二进制代码,第二个是我们加载服务的构造函数。然后我们可以调用4个http调用(请自己添加localhost +端口)

  • /api/EthereumTest/getBalance/0xfC1857DD580B41c03D7 e086dD23e7cB e1f0Edd17,这将检查钱包,并应返回5 Ehter。
  • /api/EthereumTest/releaseContract/coins,这将释放合约将结果保存到Azure存储。
  • /api/EthereumTest/checkContract/coins,这将检查合约地址是否可用。如果为true,则存在合约地址,我们可以调用它。这可能需要一些时间(有时2分钟,但有时20秒)。
  • /api/EthereumTest/exeContract/coins/addCoins/123,实际调用合约和方法addCoins的值为123。一旦调用它,就会给出一个交易结果。可以使用CallAsync但是它会在你的本地以太坊VM中调用,因此这不会导致交易。因为它是一个交易,所以返回交易地址。我们也可以在Etherscan网站上看到我们的合约。Etherscan显示了以太坊的主要和测试网络的所有交易。有了这个,你就可以证明你做了一笔交易。这是我们的一个交易可以查看
  • /api/EthereumTest/checkValue/coins/balance,当我们的ExeContract中的交易被挖掘(验证)时,我们也可以查看我们的乘法结果。合约中包含一个公共变量lastResult。可以调用此方法来获取当前状态。在与123签订合约后,余额为125。
  • /api/EthereumTest/exeContract/coins/subtractCoins/5,现在我们减去5个以太币,再次检查余额,它应该是120。

======================================================================

分享一些以太坊、EOS、比特币等区块链相关的交互式在线编程实战教程:

  • C#以太坊,主要讲解如何使用C#开发基于.Net的以太坊应用,包括账户管理、状态与交易、智能合约开发与交互、过滤器和交易等。
  • java以太坊开发教程,主要是针对java和android程序员进行区块链以太坊开发的web3j详解。
  • php以太坊,主要是介绍使用php进行智能合约开发交互,进行账号创建、交易、转账、代币开发以及过滤器和交易等内容。
  • 以太坊入门教程,主要介绍智能合约与dapp应用开发,适合入门。
  • 以太坊开发进阶教程,主要是介绍使用node.js、mongodb、区块链、ipfs实现去中心化电商DApp实战,适合进阶。
  • python以太坊,主要是针对python工程师使用web3.py进行区块链以太坊开发的详解。
  • EOS教程,本课程帮助你快速入门EOS区块链去中心化应用的开发,内容涵盖EOS工具链、账户与钱包、发行代币、智能合约开发与部署、使用代码与智能合约交互等核心知识点,最后综合运用各知识点完成一个便签DApp的开发。
  • java比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Java代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Java工程师不可多得的比特币开发学习课程。
  • php比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Php代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Php工程师不可多得的比特币开发学习课程。

汇智网原创翻译,转载请标明出处。这里是原文