大家好,我是帝哥(@CoinmanLabs),昨天我们说了下Alchemy的WEB3之路的第一周任务
今天我们一起来看看第二周任务。首先看看任务的目标是什么?在本教程中,将学习如何使用Alchemy、Hardhat、Ethers.js开发和部署去中心化的“给我买杯咖啡”智能合约,允许访问者发送(假)ETH 作为提示并留下好消息**。**话不多说,我们现在就开始第二周课程吧。
1.先决条件
npm
(npx
) version 8.5.5node
version 16.13.12.创建项目
#创建一个项目目录并且初始化package.json
mkdir BuyMeACoffee-contracts
cd BuyMeACoffee-contracts
npm init -y
# 使用hardhat生成项目框架
npx hardhat
# 该命令建议执行,因为依赖可能有问题
npm install --save-dev hardhat@^2.9.3 @nomiclabs/hardhat-waffle@^2.0.0 ethereum-waffle@^3.0.0 chai@^4.2.0 @nomiclabs/hardhat-ethers@^2.0.0 ethers@^5.0.0
# 当你创建成功后目录如下所示:
.
├── README.md
├── contracts
├── hardhat.config.js
├── node_modules
├── package-lock.json
├── package.json
├── scripts
└── test
contracts- 您的智能合约所在的文件夹
在这个项目中,我们将只创建一个,来组织我们的逻辑BuyMeACoffee
scripts- 您的安全帽 javscript 脚本所在的文件夹
我们将编写逻辑deploy
示例脚本buy-coffee
和一个兑现我们小费的脚本withdraw
hardhat.config.js- 带有solidity版本和部署设置的配置文件
3.开始开发项目
我们可以使用任意的vim方式打开上面创建的项目,帝哥这里就用VScode了,如果有需要的可以去官网下载
我们可以看到生成的目录已经有了一个合约,我们只需要去替换即可,首先将合约文件名替换为 BuyMeACoffee.sol 同时将合约内容替换成下面的。
//SPDX-License-Identifier: Unlicense
// contracts/BuyMeACoffee.sol
pragma solidity ^0.8.0;
// Switch this to your own contract address once deployed, for bookkeeping!
contract BuyMeACoffee {
// Event to emit when a Memo is created.
event NewMemo(
address indexed from,
uint256 timestamp,
string name,
string message
);
// Memo struct.
struct Memo {
address from;
uint256 timestamp;
string name;
string message;
}
// Address of contract deployer. Marked payable so that
// we can withdraw to this address later.
address payable owner;
// List of all memos received from coffee purchases.
Memo[] memos;
constructor() {
// Store the address of the deployer as a payable address.
// When we withdraw funds, we'll withdraw here.
owner = payable(msg.sender);
}
/**
* @dev fetches all stored memos
*/
function getMemos() public view returns (Memo[] memory) {
return memos;
}
/**
* @dev buy a coffee for owner (sends an ETH tip and leaves a memo)
* @param _name name of the coffee purchaser
* @param _message a nice message from the purchaser
*/
function buyCoffee(string memory _name, string memory _message) public payable {
// Must accept more than 0 ETH for a coffee.
require(msg.value > 0, "can't buy coffee for free!");
// Add the memo to storage!
memos.push(Memo(
msg.sender,
block.timestamp,
_name,
_message
));
// Emit a NewMemo event with details about the memo.
emit NewMemo(
msg.sender,
block.timestamp,
_name,
_message
);
}
/**
* @dev send the entire balance stored in this contract to the owner
*/
function withdrawTips() public {
require(owner.send(address(this).balance));
}
}
4.测试部署合约
将scripts下面的deploy.js的内容替换为下面的
const hre = require("hardhat");
// Returns the Ether balance of a given address.
async function getBalance(address) {
const balanceBigInt = await hre.ethers.provider.getBalance(address);
return hre.ethers.utils.formatEther(balanceBigInt);
}
// Logs the Ether balances for a list of addresses.
async function printBalances(addresses) {
let idx = 0;
for (const address of addresses) {
console.log(`Address ${idx} balance: `, await getBalance(address));
idx ++;
}
}
// Logs the memos stored on-chain from coffee purchases.
async function printMemos(memos) {
for (const memo of memos) {
const timestamp = memo.timestamp;
const tipper = memo.name;
const tipperAddress = memo.from;
const message = memo.message;
console.log(`At ${timestamp}, ${tipper} (${tipperAddress}) said: "${message}"`);
}
}
async function main() {
// Get the example accounts we'll be working with.
const [owner, tipper, tipper2, tipper3] = await hre.ethers.getSigners();
// We get the contract to deploy.
const BuyMeACoffee = await hre.ethers.getContractFactory("BuyMeACoffee");
const buyMeACoffee = await BuyMeACoffee.deploy();
// Deploy the contract.
await buyMeACoffee.deployed();
console.log("BuyMeACoffee deployed to:", buyMeACoffee.address);
// Check balances before the coffee purchase.
const addresses = [owner.address, tipper.address, buyMeACoffee.address];
console.log("== start ==");
await printBalances(addresses);
// Buy the owner a few coffees.
const tip = {value: hre.ethers.utils.parseEther("1")};
await buyMeACoffee.connect(tipper).buyCoffee("Carolina", "You're the best!", tip);
await buyMeACoffee.connect(tipper2).buyCoffee("Vitto", "Amazing teacher", tip);
await buyMeACoffee.connect(tipper3).buyCoffee("Kay", "I love my Proof of Knowledge", tip);
// Check balances after the coffee purchase.
console.log("== bought coffee ==");
await printBalances(addresses);
// Withdraw.
await buyMeACoffee.connect(owner).withdrawTips();
// Check balances after withdrawal.
console.log("== withdrawTips ==");
await printBalances(addresses);
// Check out the memos.
console.log("== memos ==");
const memos = await buyMeACoffee.getMemos();
printMemos(memos);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
当我们将上面的替换完毕后,通过命令行运行JS
npx hardhat run scripts/deploy.js
测试我们的合约,当你执行成功会有下面的现实
BuyMeACoffee deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
== start ==
Address 0 balance: 9999.998754619375
Address 1 balance: 10000.0
Address 2 balance: 0.0
== bought coffee ==
Address 0 balance: 9999.998754619375
Address 1 balance: 9998.999752893990255063
Address 2 balance: 3.0
== withdrawTips ==
Address 0 balance: 10002.998708719732606388
Address 1 balance: 9998.999752893990255063
Address 2 balance: 0.0
== memos ==
At 1660268820, Carolina (0x70997970C51812dc3A010C7d01b50e0d17dc79C8) said: "You're the best!"
At 1660268821, Vitto (0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC) said: "Amazing teacher"
At 1660268822, Kay (0x90F79bf6EB2c4f870365E785982E1f101E93b906) said: "I love my Proof of Knowledge"
5.使用 Alchemy 和 MetaMask 将 BuyMeACoffe.sol 智能合约部署到以太坊 Goerli 测试网
新建一个deploy01.js,内容如下:
// scripts/deploy01.js
const hre = require("hardhat");
async function main() {
// We get the contract to deploy.
const BuyMeACoffee = await hre.ethers.getContractFactory("BuyMeACoffee");
const buyMeACoffee = await BuyMeACoffee.deploy();
await buyMeACoffee.deployed();
console.log("BuyMeACoffee deployed to:", buyMeACoffee.address);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
现在我们的项目整体结构就如下了:
运行我们刚才脚本:
npx hardhat run scripts/deploy01.js
如果成功的话,你会看到下面这样的提示:
BuyMeACoffee deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
这里需要注意哦,当我们多次运行的话,你每次都会看到完全相同的部署地址,这是因为当你运行脚本时,Hardhat 工具使用的默认设置是本地开发网络,就在您的计算机上。它快速且具有确定性,非常适合进行一些快速的健全性检查。
但是,为了实际部署到在 Internet 上运行且节点遍布世界各地的测试网络,我们需要更改我们的 Hardhat 配置文件以提供选项。
6.修改hardhat.config.js 进行配置部署
首先我们将hardhat.config.js先行修改如下:
// hardhat.config.js
require("@nomiclabs/hardhat-ethers");
require("@nomiclabs/hardhat-waffle");
require("dotenv").config()
// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more
const GOERLI_URL = process.env.GOERLI_URL;
const PRIVATE_KEY = process.env.PRIVATE_KEY;
/**
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
solidity: "0.8.4",
networks: {
goerli: {
url: GOERLI_URL,
accounts: [PRIVATE_KEY]
}
}
};
# 安装dotenv
npm install dotenv
# 创建一个.env文件
touch .env
# 将下面内容写入env中
GOERLI_URL=https://eth-goerli.alchemyapi.io/v2/<your api key>
GOERLI_API_KEY=<your api key>
PRIVATE_KEY=<your metamask api key>
此外,为了获得所需的环境变量,可以使用以下资源:
GOERLI_URL
- 注册一个帐户炼金术,创建一个 Ethereum -> Goerli 应用程序,并使用 HTTP URLGOERLI_API_KEY
- 从您的同一个 Alchemy Ethereum Goerli 应用程序中,您可以获得 URL 的最后一部分,这将是您的 API KEYPRIVATE_KEY
- 遵循这些来自 MetaMask 的说明导出您的私钥。获取Goerli测试币,去下面网址获取测试币
运行脚本发布到测试网络
npx hardhat run scripts/deploy01.js --network goerli
当你成功部署页面会显示:
:BuyMeACoffee-contracts paul$ npx hardhat run scripts/deploy01.js --network goerli
Compiled 1 Solidity file successfully
BuyMeACoffee deployed to: 0xb322454bc226610Bb343Ac5BD813e1aF83A6BD03
验证下合约是否部署上测试网成功,我们去到测试网查看合约wo m z z
我们可以看到合约已经部署完毕了。
7.实现一个脚本withdraw
我们在刚才的脚本下面新建一个withdraw.js的脚本,内容如下:
// scripts/withdraw.js
const hre = require("hardhat");
const abi = require("../artifacts/contracts/BuyMeACoffee.sol/BuyMeACoffee.json");
async function getBalance(provider, address) {
const balanceBigInt = await provider.getBalance(address);
return hre.ethers.utils.formatEther(balanceBigInt);
}
async function main() {
// Get the contract that has been deployed to Goerli.
const contractAddress="你的合约地址";
const contractABI = abi.abi;
// Get the node connection and wallet connection.
const provider = new hre.ethers.providers.AlchemyProvider("goerli", process.env.GOERLI_API_KEY);
// Ensure that signer is the SAME address as the original contract deployer,
// or else this script will fail with an error.
const signer = new hre.ethers.Wallet(process.env.PRIVATE_KEY, provider);
// Instantiate connected contract.
const buyMeACoffee = new hre.ethers.Contract(contractAddress, contractABI, signer);
// Check starting balances.
console.log("current balance of owner: ", await getBalance(provider, signer.address), "ETH");
const contractBalance = await getBalance(provider, buyMeACoffee.address);
console.log("current balance of contract: ", await getBalance(provider, buyMeACoffee.address), "ETH");
// Withdraw funds if there are funds to withdraw.
if (contractBalance !== "0.0") {
console.log("withdrawing funds..")
const withdrawTxn = await buyMeACoffee.withdrawTips();
await withdrawTxn.wait();
} else {
console.log("no funds to withdraw!");
}
// Check ending balance.
console.log("current balance of owner: ", await getBalance(provider, signer.address), "ETH");
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
在本地测试
# 运行该命令在本地测试
npx hardhat run scripts/withdraw.js
如果没有报错你会有下面的提示:
current balance of owner: 0.14511094798885063 ETH
current balance of contract: 0.0 ETH
no funds to withdraw!
current balance of owner: 0.14511094798885063 ETH
8.使用 Replit 和 Ethers.js 构建前端 Buy Me A Coffee 网站 dapp
首先在Replit IDE将下面的仓库进行fork
在fork之后我们会来到自己的工作台,如下所示:
将文件中写好的变量进行修改
contractAddresspages/index.js
pages/index.js
utils/BuyMeACoffee.json
可以看到 contractAddress 变量已经填充了地址。修改成你自己部署的合约
将复制过来的仓库的Albert修改成你想要的任意名字都可以
将刚才在编译器生成的ABI复制到Replit中的utils/BuyMeACoffee.json
我们同时也去合约看看:
最后课程给我们留了一个挑战,我们一起去看看吧:
第一个挑战,我们可以有多个方法,比如写一个public的变量,将他可以set,能够实时更新,第二个则是首先在前端购买的时候在大咖啡的按钮处传入的参数就是0.003 ETH。
对了,最后别忘记提交课程完成的作业哦
我是懂币帝,用技术带你领略区块链魅力,第一时间获取行业最新资讯:
推特:@CoinmanLabs
微信:CoinmanLabs(社群客服微信进群)