整个技术栈涉及的工具和技术比较多,所以先拉个列表:
开发环境(Docker Images)准备
Base Image 是微软打包的开发镜像,有很多个语言版本,可以直接通过docker hub 下载。我为了开发方便,基于node镜像又封装了一个镜像,加入了一些基础包。
1 2 3 4 5 6 7 8 9 10 11 12 13 FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:latest as baseRUN apt-get update && \ apt-get install --no-install-recommends -y \ build-essential \ curl && \ rm -rf /var/lib/apt/lists/* && \ rm -rf /etc/apt/sources.list.d/* RUN mkdir -p /home/app WORKDIR /home/app RUN npm install --global web3 ethereumjs-testrpc ganache-cli truffle
开发工具(Remote Development )准备 VSCode安装完成之后,需要安装VSCode Remote插件。在插件搜索框中搜索remote,就可以看到Remote三件套:SSH、Containers、WSL。SSH和Containers就不多解释了,WSL是Windows Subsystem for Linux,如果操作系统是windows11可以直接开启WSL,通过windows docker desktop在WSL里启用docker,效果是完全一样的
Attach到容器 安装完插件之后,就可以看到romote图标,点击进去后切换到containers就可以看到运行中的镜像了,选中后鼠标右键Attach到镜像,就会开启一个新的vscode。这样整个开发环境就准备完成了
测试项目 测试项目一共两个部分,SmartContract和DApps。
1 2 3 4 5 6 7 8 9 10 mkdir contractcd contractnpm init -y truffle init mkdir myappcd myappnpx create-react-app myapp
SmartContract 测试用的账户可以提前创建,并通过 Goerli PoW Faucet 来获取测试用的ETH
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 package mainimport ( "context" "crypto/ecdsa" "encoding/hex" "fmt" "log" "math" "math/big" "os" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/params" "golang.org/x/crypto/sha3" ) func main () { client := InitClient() CreateAccountNewWallets(client) } func InitClient () *ethclient.Client { client, err := ethclient.Dial("https://goerli.infura.io/v3/YourApiKey" ) if err != nil { log.Fatal(err) } fmt.Println("we have a connection" ) return client } func CreateAccountNewWallets (client *ethclient.Client) { privateKey, err := crypto.GenerateKey() if err != nil { log.Fatal(err) } privateKeyBytes := crypto.FromECDSA(privateKey) fmt.Println("privateKey" ) fmt.Println(hexutil.Encode(privateKeyBytes)[2 :]) publicKey := privateKey.Public() publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) if !ok { log.Fatal("error casting public key to ECDSA" ) } publicKeyBytes := crypto.FromECDSAPub(publicKeyECDSA) fmt.Println("publicKeyBytes" ) fmt.Println(hexutil.Encode(publicKeyBytes)[4 :]) address := crypto.PubkeyToAddress(*publicKeyECDSA).Hex() fmt.Println("address" ) fmt.Println(address) hash := sha3.New512() hash.Write(publicKeyBytes[1 :]) fmt.Println(hexutil.Encode(hash.Sum(nil )[12 :])) }
SmartContract 是用solidity编写的,并非标准代码,仅用于测试
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 // SPDX-License-Identifier: MIT pragma solidity >=0.4.22 <0.9.0; import "../node_modules/@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "../node_modules/@openzeppelin/contracts/access/Ownable.sol"; import "../node_modules/@openzeppelin/contracts/utils/Counters.sol"; import "../node_modules/@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; import "../node_modules/@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; contract TestNFT is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable{ using Counters for Counters.Counter; Counters.Counter private _tokenIds; IERC721Enumerable public whitelistedNftContract; event Minted(address indexed minter, uint nftID, string uri); constructor() ERC721("TestNFT", "NFT"){} function mintNFT(string memory _uri, address _toAddress) public onlyOwner returns (uint256) { uint256 newItemId = _tokenIds.current(); _mint(_toAddress, newItemId); _setTokenURI(newItemId, _uri); _tokenIds.increment(); emit Minted(_toAddress, newItemId, _uri); return newItemId; } function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) { super._burn(tokenId); } function tokenURI(uint256 tokenId) public view override(ERC721, ERC721URIStorage) returns (string memory){ return super.tokenURI(tokenId); } function _beforeTokenTransfer(address from, address to, uint256 tokenId) internal override(ERC721, ERC721Enumerable){ super._beforeTokenTransfer(from, to, tokenId); } function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable) returns (bool){ return super.supportsInterface(interfaceId); } }
SmartContract的测试代码
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 const { web3 } = require ("@openzeppelin/test-environment" );const { expect } = require ("chai" );const { BigNumber } = require ("bignumber.js" );const TestNFTContract = artifacts.require ("TestNFT" );contract ("TestNFT" , (accounts ) => { describe ("testnft" , () => { beforeEach (async () => { this .contract = await TestNFTContract .new ({ from : accounts[0 ] }); }); it ("It should mint NFT successfully" , async () => { const tokenURI = "ipfs://QmXzG9HN7Z4kFE2yHF81Vjb2xDYu53tqhRciktrt15JpAN" ; const mintResult = await this .contract .mintNFT ( tokenURI, accounts[0 ], { from : accounts[0 ] } ); console .log (mintResult); expect (mintResult.logs [1 ].args .nftID .toNumber ()).to .eq (0 ); expect (mintResult.logs [1 ].args .uri ).to .eq (tokenURI); expect (mintResult.logs [1 ].args .minter ).to .eq (accounts[0 ]); }); }); describe ("owner()" , () => { it ("returns the address of the owner" , async () => { const testntf = await TestNFTContract .deployed (); const owner = await testntf.owner (); assert (owner, "the current owner" ); }); it ("matches the address that originally deployed the contract" , async () => { const testntf = await TestNFTContract .deployed (); const owner = await testntf.owner (); const expected = accounts[0 ]; assert.equal (owner, expected, "matches address used to deploy contract" ); }); }); });
SmartContract编译与测试
在封装开发镜像的时候,就已经安装了开发智能合约的工具包truffle,以下是truffle配置文件
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 require ("dotenv" ).config ();const path = require ("path" );const HDWalletProvider = require ("@truffle/hdwallet-provider" );const mnemonic = process.env .MNEMONIC ;module .exports = { contracts_build_directory : path.join (__dirname,"build/contracts" ), networks : { development : { provider : () => new HDWalletProvider (mnemonic, `https://goerli.infura.io/v3/yourapikey` ), network_id : "5" , }, }, mocha : { reporter : 'xunit' , reporterOptions : { output : 'TEST-results.xml' } }, compilers : { solc : { version : "0.8.14" , } }, };
准备好之后就可以开始合约的编译和测试了
1 2 3 4 5 6 7 8 truffle compile truffle test truffle migrate
DApps(Decentralized Applications) D apps是由React开发,功能很简单的前端页面,主要目的是调用之前创建的合约铸造NFT
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 import './App.css' ;import React , {Component } from "react" ;import testnft from "./contracts/TestNFT.json" ;class App extends Component { state = { storageValue : null , web3 : null , account : null , toaccount : null , contract : null }; componentDidMount = async () =>{ try { const Web3 = require ('web3' ); const toaccount = 'to account address' ; const account = 'from account address' ; const HttpProvider ='https://goerli.infura.io/v3/yourapikey' ; const provider = new Web3 .providers .HttpProvider (HttpProvider ); const web3 = new Web3 (provider); web3.eth .defaultAccount = account; web3.eth .accounts .wallet .add ('from account private key' ); const instance = new web3.eth .Contract ( testnft.abi , 'address from after creating a contract' , ); this .setState ({ web3, account, toaccount, contract : instance }, this .runExample ); }catch (error){ alert ( `Failed to load web3, accounts, or contract. Check console for details.` , ); console .error (error); } }; runExample = async () => { const { web3, account, toaccount, contract } = this .state ; const response = await contract.methods .mintNFT ('ipfs://QmYGkmHAhySYR6zvizG3xoMEyLPs2h68swsg5BeStxL5uK' , toaccount).send ( {from : account, gasLimit : 3500000 , gasPrice : web3.utils .toWei ('1' , 'Gwei' )} ); this .setState ({ storageValue : 'from: ' + response['from' ] + '. to: ' + response['to' ] + '. In the block: ' + response['blockNumber' ] }); }; render ( ) { if (!this .state .web3 ) { return <div > !Loading Web3, accounts, and contract...{this.state.web3},test</div > ; } return ( <div className ="App" > <h1 > Good to Go!</h1 > <p > Your Truffle Box is installed and ready.</p > <h2 > Smart Contract Example</h2 > <p > This is a simple demo of casting NFT, if successfully executed, will print the <strong > from</strong > and <strong > to</strong > addresses, and the <strong > blocknumber</strong > </p > <p > Try changing the value stored on <strong > runExample()</strong > of App.js. </p > <div > The stored value is: {this.state.storageValue}</div > </div > ); } } export default App ;
React测试代码
1 2 3 4 5 6 7 8 import { render, screen } from '@testing-library/react' ;import App from './App' ;test ('renders learn react link' , () => { render (<App /> ); const linkElement = screen.getByText (/Good to Go!/i ); expect (linkElement).toBeInTheDocument (); });
React编译和测试
1 2 3 4 5 npm run build npm test --reporters=default --reporters=jest-junit
Azure Devops Pipeline 现在我们可以开始在Azure Devops上创建 Pipeline了。
在Azure Devops中创建新的项目,Version control 选择Git,
创建好项目之后,在Repos/Files中找到repository的地址,点击Generate GIt Credentials生成Password。之后在本地设置Git连接到这个远程库
初始化项目并推送到Remote Repository,使用上一步生成的密码,也可以使用SSH
1 2 3 4 5 6 7 git init git config --global user.email "YouEmail@email.com" git config --global user.name "YourName" git add . git commit -m "init project & add README file" git remote add origin https://YourRemoteRepositoryAddressForHTTPS git push -u origin --all
将代码推送到 GitHub 后,导航到 Azure DevOps Pipelines 页面,然后单击 Create Pipeline 按钮
在 Where is your code? 时选择Azure Repos Git。之后选择存放代码的repo,然后选择 Starter pipeline。
Azure Pipelines 可以由Stages、Jobs和Steps 组成。在开始之前需要布置pipeline的Stages和Jobs。定义Stages和Jobs之间的依赖关系并查看整个pipeline。
初始结构
使用web editor更新代码以定义管道结构。整个pipeline有六个阶段
1、build:编译、测试和打包工件
2、dev:部署基础设施、合约和前端
3、dev_validation:等待手动验证dev并删除dev环境
4、qa:部署基础设施、合约和前端
5、qa_validation 等待手动验证 qa 并删除 qa 环境
6、prod:部署基础设施、合约和前端
在第一部分中,加入了开发环境的部署、Smart Contract和DApps的编译和测试,以及测试结果的输出。最后将Smart Contract和DApps项目文件、测试文件打包,以便后续部署使用。
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 trigger: - main pool: vmImage: ubuntu-latest stages: - stage: build jobs: - job: compile_test steps: - script: npm install --global web3 ethereumjs-testrpc ganache-cli truffle displayName: "install npm global package" - script: npm install displayName: "Install npm package" workingDirectory: $(System.DefaultWorkingDirectory)/contracts3 - script: truffle compile displayName: "Compile contracts" workingDirectory: $(System.DefaultWorkingDirectory)/contracts3 - script: truffle test displayName: "Test contracts" workingDirectory: $(System.DefaultWorkingDirectory)/contracts3 - task: PublishTestResults@2 displayName: "Publish contract test results" inputs: testRunTitle: "Contract" testResultsFormat: "JUnit" failTaskOnFailedTests: true testResultsFiles: "**/TEST-*.xml" - task: CopyFiles@2 displayName: Package tests inputs: Contents: | $(System.DefaultWorkingDirectory)/contracts3/test/** package.json TargetFolder: "$(Build.ArtifactStagingDirectory)/tests" - task: PublishPipelineArtifact@1 displayName: Publish contract tests inputs: targetPath: "$(Build.ArtifactStagingDirectory)/tests" artifact: "tests" publishLocation: "pipeline" - task: CopyFiles@2 displayName: Package contracts inputs: Contents: | $(System.DefaultWorkingDirectory)/contracts3/package.json $(System.DefaultWorkingDirectory)/contracts3/migrations/** $(System.DefaultWorkingDirectory)/contracts3/truffle-config.js $(System.DefaultWorkingDirectory)/contracts3/contracts/** TargetFolder: "$(Build.ArtifactStagingDirectory)/contracts" - task: PublishPipelineArtifact@1 displayName: Publish contracts inputs: targetPath: "$(Build.ArtifactStagingDirectory)/contracts" artifact: "contracts" publishLocation: "pipeline" - script: npm install displayName: "Install frontend dependencies" workingDirectory: $(System.DefaultWorkingDirectory)/myapp - script: npm run build displayName: "Build frontend" workingDirectory: $(System.DefaultWorkingDirectory)/myapp - script: npm test -- --reporters=default --reporters=jest-junit displayName: "Test frontend" workingDirectory: $(System.DefaultWorkingDirectory)/myapp env: CI: true - task: PublishTestResults@2 displayName: "Publish frontend test results" inputs: testRunTitle: "Frontend" testResultsFormat: "JUnit" failTaskOnFailedTests: true testResultsFiles: "myapp/junit*.xml" - task: PublishPipelineArtifact@1 displayName: Publish frontend inputs: targetPath: "$(System.DefaultWorkingDirectory)/myapp/build" artifact: "client" publishLocation: "pipeline" - stage: dev dependsOn: build jobs: - job: iac - job: deploy_contracts dependsOn: iac - job: deploy_frontend dependsOn: - iac - deploy_contracts - stage: dev_validation dependsOn: dev jobs: - job: wait_for_dev_validation - job: delete_dev dependsOn: wait_for_dev_validation - stage: qa dependsOn: dev_validation jobs: - job: iac - job: deploy_contracts dependsOn: iac - job: deploy_frontend dependsOn: - iac - deploy_contracts - stage: qa_validation dependsOn: qa jobs: - job: wait_for_qa_validation - job: delete_qa dependsOn: wait_for_qa_validation - stage: prod dependsOn: qa_validation jobs: - job: iac - job: deploy_contracts dependsOn: iac - job: deploy_frontend dependsOn: - iac - deploy_contracts
保存并运行Pipeline,确认结构正确并运行成功
项目文件打包
DApp测试结果的输出
Smart Contract测试结果的输出,所有的测试案例都通过了