.NetCore使用以太坊區塊鏈簡介
本文描述了在dotNet核心中使用像以太坊這樣的區塊鏈平臺的過程。目標受眾是其他想要從以太坊開始的dotNet開發者。需要了解區塊鏈。在本文中,我們構建了一個完整的示例,允許你與自定義編寫的智慧合約進行互動。
第一代區塊鏈的可以被視為僅比特幣而沒有智慧合約。儘管如此,第二代區塊鏈的表現明顯給人更有希望。隨著比特幣以外的更多區塊鏈平臺,變得更加成熟,區塊鏈有了更多可能性。以太坊區塊鏈更像是一個使用加密貨幣的智慧合約的分散式分類賬。以太坊的重點更多地放在智慧合約部分,然後是加密貨幣。以太幣(以太坊的加密貨幣)的目的是為執行採礦合約或執行合約的交易提供報酬。
智慧合約是為以太坊虛擬機器編寫的一段程式碼。這可以用Solidity編寫並編譯為位元組程式碼。此位元組程式碼放在分類帳中並變為不可變但仍可以與之互動,並且可以更改狀態。正如以太坊文件所說:“從實用的角度來看,EVM可以被認為是一個包含數百萬個物件的大型分散計算機,稱為”帳戶“,它們能夠維護內部資料庫,執行程式碼並相互通訊。“從開發人員的角度來看,你可以將Solidity視為類似Javascript的語言,這有點受限。由於Solidity程式碼在區塊鏈中執行,因此有充分的理由限制它。像隨機數這樣簡單的東西也是一個挑戰。也無法通過Http呼叫獲取資料,因為所有事實需要在系統中。你仍然可以呼叫合約並輸入資料來改變狀態,因此外部影響是可行的。
首先安裝Mist瀏覽器和Geth。Mist瀏覽器是一個GUI,可用作Ether的錢包。Geth是程式碼連線到的程式介面,Geth連線到以太坊的區塊鏈。對於本文,我們將使用testnet。這樣我們就可以免費開採一些以太幣。啟動Mist後,從選單中選擇使用測試網。建立一個帳戶並挖掘一些以太幣(選單專案開發並開始挖掘)。
過了一段時間,你會有一些以太幣。這在交易時很方便。即使釋出合約或執行合約也要花費成本。現在讓我們關閉錢包,否則你無法開啟一個新的geth過程。所以在控制檯中啟動已安裝的Geth:
“\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中:
"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
:
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
介面,這樣我們就可以將它用於依賴注入:
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
來實現介面。
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;
}
}
}
這是很多程式碼。我將跳過Save
和Load -ContractFromTableStorage
,因為這些只是簡單的Azure表互動。
在建構函式中,我們看到與Geth程序的連線,我們連線到埠8545,因此它可以進行RPC json
通訊。
第一個方法實現的是getBalance
。由於一切都圍繞金錢,所以檢查地址的以太幣的餘額是很重要的,比如你的賬戶,錢包甚至合約。在此示例中,所有以太坊互動都通過物件web3完成。在我們在Wei中取得餘額之後,這就像是人民幣的分數,然後是1018因子而不是102。我們可以使用convert.FromWEi
將其轉換回以太幣。
第二個方法實現的是ReleaseContract
。它首先檢查我們是否尚未釋出合約並將其保留在儲存中。如果沒有,我們可以開始解鎖帳戶120秒。當我們想要部署合約或其他東西時,需要解鎖。之後,我們可以呼叫deploy
方法並獲取交易雜湊。這是必要的,因為現在合約將被開採。將挖掘視為區塊鏈的同行所做的過程,以便合約被接受到區塊鏈中。當12個同行已經這樣做時,合約地址被退回。這個挖掘過程需要花錢(又名Gas),並且會從你輸入的_accountAddress
中扣除。這個數量在Wei中,我們在控制器中指定它,它將呼叫EthereumService
。每份合約都有不同的汽油價格。編譯合約時可以使用此值。我們可以在方法SendRequestAsync
中指定合約建構函式引數。在我們的情況下,我們指定2,因為合約釋出時我們的餘額應為2個以太幣。
如上所述,必須挖掘部署才能獲得合約地址。我們需要這個地址來呼叫它上面的方法。在我們的TryGetContractAddress
中,我們檢查我們的合約是否已經在我們的表儲存中有一個地址,如果沒有,我們會詢問以太坊區塊鏈。如果GetTransactionReceipt
返回有效地址,我們可以保留它。
我們服務的最後一個方法是GetContract
,這只是對以太坊合約的引用。如你所見,合約必須存在於表儲存中才能獲得合約地址。我們將在下一部分之後討論呼叫合約。
所以現在我們從dotNet離開下,轉到solidity
程式語言。首先讓我們看看我們的測試合法性;
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程式碼中呼叫addCoints
或subtractCoints
方法。那你為什麼要這樣做呢?它只會花費我們以太?好的好處是,每次呼叫方法都會被新增到分配分類帳中,因此可以在https://testnet.etherscan.io/檢視。
為了釋出這個合約,我們需要將它編譯為位元組程式碼。我們使用Remix網站這個基於網路的基本編輯器可以編譯和測試你的合約。編譯完成後,我們可以獲得位元組程式碼(請不要忘記前面的0x)和介面,也稱為ABI
。在簽訂合約時需要提供這兩個部件。ABI
代表應用程式二進位制介面,就像Web服務的WSDL一樣。
回到Visual Studio,在我們釋出合約並開始呼叫方法之前,我們只需再做四個步驟。
首先,我們建立名為EthereumSettings
的設定檔案:
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
:
"ehtereumAccount": "x",
"ehtereumPassword": "y",
"storageKey": "w",
"storageAccount": "v"
當然,不是使用這些值,而是使用你自己的以太坊帳戶和密碼以及Azure儲存帳戶和金鑰。
第三,我們在我們的startup.cs
中添加了ConfigureServices
方法中的程式碼:
services.Configure<EthereumSettings>(Configuration);
services.AddScoped<IEthereumService, BasicEthereumService>();
對於我們的最後一步,新增一個名為EthereumTestController
的控制器,內容應該是:
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工程師不可多得的比特幣開發學習課程。
匯智網原創翻譯,轉載請標明出處。這裡是原文