本文描述了在dotNet核心中使用像以太坊這樣的區塊鏈平臺的過程。目標受衆是其餘想要從以太坊開始的dotNet開發者。須要瞭解區塊鏈。在本文中,咱們構建了一個完整的示例,容許你與自定義編寫的智能合約進行交互。php
第一代區塊鏈的能夠被視爲僅比特幣而沒有智能合約。儘管如此,第二代區塊鏈的表現明顯給人更有但願。隨着比特幣之外的更多區塊鏈平臺,變得更加成熟,區塊鏈有了更多可能性。以太坊區塊鏈更像是一個使用加密貨幣的智能合約的分佈式分類帳。以太坊的重點更多地放在智能合約部分,而後是加密貨幣。以太幣(以太坊的加密貨幣)的目的是爲執行採礦合約或執行合約的交易提供報酬。java
智能合約是爲以太坊虛擬機編寫的一段代碼。這能夠用Solidity編寫並編譯爲字節代碼。此字節代碼放在分類賬中並變爲不可變但仍能夠與之交互,而且能夠更改狀態。正如以太坊文檔所說:「從實用的角度來看,EVM能夠被認爲是一個包含數百萬個對象的大型分散計算機,稱爲」賬戶「,它們可以維護內部數據庫,執行代碼並相互通訊。「從開發人員的角度來看,你能夠將Solidity視爲相似Javascript的語言,這有點受限。因爲Solidity代碼在區塊鏈中運行,所以有充分的理由限制它。像隨機數這樣簡單的東西也是一個挑戰。也沒法經過Http調用獲取數據,由於全部事實須要在系統中。你仍然能夠調用合約並輸入數據來改變狀態,所以外部影響是可行的。node
首先安裝Mist瀏覽器和Geth。Mist瀏覽器是一個GUI,可用做Ether的錢包。Geth是代碼鏈接到的程序接口,Geth鏈接到以太坊的區塊鏈。對於本文,咱們將使用testnet。這樣咱們就能夠免費開採一些以太幣。啓動Mist後,從菜單中選擇使用測試網。建立一個賬戶並挖掘一些以太幣(菜單項目開發並開始挖掘)。python
過了一段時間,你會有一些以太幣。這在交易時很方便。即便發佈合約或執行合約也要花費成本。如今讓咱們關閉錢包,不然你沒法打開一個新的geth過程。因此在控制檯中啓動已安裝的Geth:android
「\Program Files\Geth\geth」 --testnet --rpcapi eth,web3,personal --rpc
上圖是咱們命令的結果。咱們看到它正在接收當前的區塊鏈緩存,而且它的http端點正在localhost:8545
上進行偵聽。這很重要,由於咱們須要Mist瀏覽器和其餘應用程序使用IPC或RPC訪問它。因爲在Windows上只支持IPC實現,咱們不能在dotNetCore中使用它。咱們在解決方案中使用web3 RPC
。git
如今你能夠再次打開錢包。只是不能開始挖掘,由於有獨立的Geth正在運行。程序員
如今是時候開始開發,打開Visual Studio並建立一個新項目了。請注意,咱們的Github提供了該代碼。建立「ASP.NET核心Web應用程序」,而後選擇「Web.API模板」。咱們將建立一個服務,其中包含一些與區塊鏈交互的方法,並向區塊鏈發佈合約。這個存錢合約將存儲咱們的代幣餘額。合約開採後咱們能夠調用合約方法。沒什麼高大上的,也不是一個完整的應用程序,但很高興看到咱們能作什麼。咱們選擇使用Azure Table存儲來保持系統的持久性,它快速且便宜。github
首先將這些依賴項添加到Project.json中:web
"Nethereum.Web3": "2.0.0-rc1", "Portable.BouncyCastle": "1.8.1.1", "WindowsAzure.Storage": "8.1.1"
保存並查看正在恢復的軟件包。前兩個是以太坊相關,最後一個用於表存儲。Nethereum.Web3
是經過RPC json
訪問本地Geth進程的完整類庫。BouncyCastle
是Nethereum所需的加密庫。mongodb
首先,咱們須要一個模型來捕獲咱們的以太坊合約狀態。以太坊沒有任何選擇讓合約退出區塊鏈,主要是出於安全/不可變的緣由。一旦合約被放入區塊鏈,就沒法更改,也沒法檢索到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中取得餘額以後,這就像是人民幣的分數,而後是10^18因子而不是10^2。咱們可使用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工程師不可多得的比特幣開發學習課程。
匯智網原創翻譯,轉載請標明出處。這裏是原文