Alchemy的the Road to Web3-第二周文本教程

今天我们一起来看看第二周任务。首先看看任务的目标是什么?在本教程中,将学习如何使用Alchemy、Hardhat、Ethers.js开发和部署去中心化的“给我买杯咖啡”智能合约,允许访问者发送(假)ETH 作为提示并留下好消息。话不多说,我们现在就开始第二周课程吧。

1.先决条件

  • npm (npx) version 8.5.5

  • node version 16.13.1

  • 如果你会js代码更好

2.创建项目

#创建一个项目目录并且初始化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了,如果有需要的可以去官网下载

post image

我们可以看到生成的目录已经有了一个合约,我们只需要去替换即可,首先将合约文件名替换为 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: 0xa22E44310355205aD388C88361F93bF898e53d9F
== 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: 0xa22E44310355205aD388C88361F93bF898e53d9F

这里需要注意哦,当我们多次运行的话,你每次都会看到完全相同的部署地址,这是因为当你运行脚本时,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 URL

  • GOERLI_API_KEY- 从您的同一个 Alchemy Ethereum Goerli 应用程序中,您可以获得 URL 的最后一部分,这将是您的 API KEY

  • PRIVATE_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: 0xa22E44310355205aD388C88361F93bF898e53d9F

验证下合约是否部署上测试网成功。

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
fork

fork

在fork之后我们会来到自己的工作台,如下所示:

post image

将文件中写好的变量进行修改

  • 更新输入contractAddresspages/index.js

  • 将名称字符串更新为您自己的名字pages/index.js

  • 确保合同 ABI 与您的合同相匹配utils/BuyMeACoffee.json

可以看到 contractAddress 变量已经填充了地址。修改成你自己部署的合约

post image

将复制过来的仓库的Albert修改成你想要的任意名字都可以

post image

将刚才在编译器生成的ABI复制到Replit中的utils/BuyMeACoffee.json

post image
运行项目
运行项目

运行项目.