# Alchemy Road to Web3第二周部署买咖啡Dapp

By [Licrazy](https://paragraph.com/@doger) · 2022-08-25

---

### TL;DR

本文主要由六部分组成,分别为：准备、搭建环境、代码编译、代码测试、部署Dapp、发布与提交。

### 一、准备：

1、结合[之前前序](https://mirror.xyz/doger.eth/UwHqj3KpiH_YIgNV_KgkliLcf2fwatK8_cKiXAfZZ6E)文档，创建好本周的github代码库以及同步到replit.com上。

如图，依次点击右上角github账号--“repositories”--填写“name”（随意）--“create repository”**_(切记勾选“add a README file”不然会是一个空库，无法导入replit)_**

![](https://storage.googleapis.com/papyrus_images/4a2f9045e5922de5fb99319fe2728d1496155af2ef2ce2f27901e77da304e4c0.png)

![](https://storage.googleapis.com/papyrus_images/e291aca4c78c9fc0c043df159d84ef34bc929fa0eb4f1c5f85f18d467e267a93.png)

2、登陆你的[replit](https://replit.com/)账号，依次点击“create”--“import”，然后选择上一步创建的代码库“magweek2”，再次点击“import from github”，完成代码的导入工作

![](https://storage.googleapis.com/papyrus_images/ef7290fc73a4b1267e7bc9a67082aacb2dab7521e623adddee543f98ed1ad205.png)

![](https://storage.googleapis.com/papyrus_images/2dd1d734cbc55284aeaff5215f4349e828d2827af6f1423c01740e3e0bd914a5.png)

3、进去项目后，如下图所示，点击“Done”

![](https://storage.googleapis.com/papyrus_images/fb43a694c2c6533a91035f7e56817134d3014ca87273c898093a5d5c8ac30d3b.png)

### 二、搭建环境

1、点击右边“Shell”，首先输入mkdir BuyABCACoffee-contracts并回车（ABC3个字母处，可改为任意名字，后文涉及到ABC处的均可替换为自己定义的名字，也可以不改~）；

再次输入cd BuyABCACoffee-contracts并回车。

![](https://storage.googleapis.com/papyrus_images/dac565e6a333d69c42cde06a17a66d8296655b23e6642d731ef5820e64b9164e.png)

2、输入npm init -y并回车。

![](https://storage.googleapis.com/papyrus_images/5b8b4406e4c92a13820441e45526efc5ebb530d113d96bf660294f72a7df63d6.png)

3、输入npx hardhat并回车。(中间大约需要点5次回车，就是那几个√的地方需要按回车才能继续）

![](https://storage.googleapis.com/papyrus_images/5d80199f6b2d741d93e77aa9891728c7db24e80cd063c1c49d5af6a776b02a2c.png)

大概成功安装后会如下图所示：

![](https://storage.googleapis.com/papyrus_images/bb1cb259fd0b1199aaf5df0b3b9debc18aee8db57c824f47584aa6396f769159.png)

4、输入npm install --save-dev @nomiclabs/hardhat-waffle 并回车，等待安装完成。（中间各种warn不要管）

![](https://storage.googleapis.com/papyrus_images/28e917ce39d8c6a809f77c90a8ed183003521c03b3f5555bc72133a22d9991ae.png)

5、输入npm install dotenv并回车，等待安装完成。**_至此环境搭建步骤已经完成。_**

![](https://storage.googleapis.com/papyrus_images/6f016c21ddff94f6c9cedf373ee32e0f6c110fec6b8edbd3437db696d800c566.png)

### 三、项目代码编译

1、点开contracts目录，把Lock.sol重命名（rename）为**BuyABCACoffee.sol**(注意，文件名最好与前面创建的目录前缀一致)

![](https://storage.googleapis.com/papyrus_images/3bac0a6243f0b8ac950b0abe43b0f5760868a1851609e2a4c310be46bf6ceeb3.png)

2、点击刚才改名的“**BuyABCACoffee**”文件，将里面的内容替换为如下代码：

**_（特别注意ABC可以改成自己设置的名字，需与前面保持一致。 记得保存文件内容）_**

// SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.9;

// Import this file to use console.log

import "hardhat/console.sol";

// Switch this to your own contract address once deployed, for bookkeeping!

// Example Contract Address on Goerli: 0xDBa03676a2fBb6711CB652beF5B7416A53c1421D

contract BuyABCACoffee {

// 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));

}

}

2、在scripts目录鼠标右键或者左键点击**_3个点_**处，选“Add file”，输入buycoffee.js回车来新建文件。

![](https://storage.googleapis.com/papyrus_images/1120c836ca85c1498034aa0a895a1898329b59a81a78266574fac4bf581b1bd8.png)

3、在“buycoffee.js”文件里输入以下代码。（同样，ABC可以修改，需与前面保持一致。若前面改了，此处修改会较多） 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 BuyABCACoffee = await hre.ethers.getContractFactory("BuyABCACoffee");

const buyABCACoffee = await BuyABCACoffee.deploy();

// Deploy the contract.

await buyABCACoffee.deployed();

console.log("BuyABCACoffee deployed to:", buyABCACoffee.address);

// Check balances before the coffee purchase.

const addresses = \[owner.address, tipper.address, buyABCACoffee.address\];

console.log("== start ==");

await printBalances(addresses);

// Buy the owner a few coffees.

const tip = {value: hre.ethers.utils.parseEther("1")};

await buyABCACoffee.connect(tipper).buyCoffee("Carolina", "You're the best!", tip);

await buyABCACoffee.connect(tipper2).buyCoffee("Vitto", "Amazing teacher", tip);

await buyABCACoffee.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 buyABCACoffee.connect(owner).withdrawTips();

// Check balances after withdrawal.

console.log("== withdrawTips ==");

await printBalances(addresses);

// Check out the memos.

console.log("== memos ==");

const memos = await buyABCACoffee.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);

});

完成后如下图所示：

![](https://storage.googleapis.com/papyrus_images/badb6d5b75c7966f17dceb0a285c836dc22279bb2f9f2874a136145c9db19209.png)

4、点击deploy.js文件，将原文替换成以下代码。

![](https://storage.googleapis.com/papyrus_images/626536345f55e6f63f587777f8969a65dff2b044d9b49b96e3c2bb0e22faad70.png)

// scripts/deploy.js

const hre = require("hardhat");

async function main() {

// We get the contract to deploy.

const BuyABCACoffee = await hre.ethers.getContractFactory("BuyABCACoffee");

const buyABCACoffee = await BuyABCACoffee.deploy();

await buyABCACoffee.deployed();

console.log("BuyABCACoffee deployed to:", buyABCACoffee.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);

});

5、在scripts目录左键点击右侧3个点处，选“Add file”，新建withdraw.js文件，并回车。

![](https://storage.googleapis.com/papyrus_images/799c617b4c473ac3959b9b72ed274459420ea875ee69c78dcc1d35b73fe0e335.png)

6、在上一步新建的“withdraw.js”文件里，输入以下代码。

// scripts/withdraw.js

const hre = require("hardhat");

const abi = require("../artifacts/contracts/BuyABCACoffee.sol/BuyABCACoffee.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="0x314Cb061D81759F339c6F026c86D09Ad528A31b5";

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 buyABCACoffee = 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, buyABCACoffee.address);

console.log("current balance of contract: ", await getBalance(provider, buyABCACoffee.address), "ETH");

// Withdraw funds if there are funds to withdraw.

if (contractBalance !== "0.0") {

console.log("withdrawing funds..")

const withdrawTxn = await buyABCACoffee.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);

});

7、左键点击BuyABCACoffee-contracts右边三个点，选“Add file”，新建.env文件(**注意，此文件最前面有小数点**)。

![](https://storage.googleapis.com/papyrus_images/2b87fc0b9e92000a1bb98d8ccf7b7358734ccf6da940b13b341b0094377f9e9c.png)

8、先暂时将以下代码输入.env文件

GOERLI\_URL=[https://eth-goerli.alchemyapi.io/v2/](https://eth-goerli.alchemyapi.io/v2/) GOERLI\_API\_KEY= PRIVATE\_KEY= 9、登陆alchemy账号并创建ETH-Goerli的API（如何创建见 [前序文章](https://mirror.xyz/doger.eth/UwHqj3KpiH_YIgNV_KgkliLcf2fwatK8_cKiXAfZZ6E) 第4步） 点击VIEW KEY，然后复制下图所示API KEY，粘贴到上一步的GOERLI\_API\_KEY=后面；复制HTTPS，粘贴到上一步的GOERLI\_URL=后面。 再去metamask钱包导出钱包私钥（如何导出见 [前序文章](https://mirror.xyz/doger.eth/UwHqj3KpiH_YIgNV_KgkliLcf2fwatK8_cKiXAfZZ6E) 第5步，最好新建一个新钱包），将私钥粘贴到上一步的PRIVATE\_KEY=后面。 10、点击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/](https://hardhat.org/config/) to learn more const GOERLI = process.env.GOERLI\_URL; const PRIVATE = process.env.PRIVATE\_KEY; //console.log(GOERLI); /\*\* \* @type import('hardhat/config').HardhatUserConfig \*/ module.exports = { solidity: "0.8.9", networks: { goerli: { url: GOERLI, accounts: \[PRIVATE\] } } }; 四、代码完成，开始项目测试 1、又回到右边栏了，点击“Shell”，若观察回到上一层目录了，请执行`cd BuyABCACoffee-contracts` （同样**ABC**为你应与前面你设置的名字一样\*\*）\*\*切换到子目录中。没有则不管。以后遇到都需要这么处理，即cd +文件名 。如下图： 然后输入npx hardhat run scripts/buycoffee.js并回车。（中间会遇到有一次”？直接回车，以后遇到该问题也是如此操作）如下图： 当运行完成后，看图可发现我们的控制台又回到了上一层目录，同理我们通过输入cd BuyABCACoffee-contracts回到子目录。 2、输入npx hardhat run scripts/deploy.js并回车。(中间会遇到有一次”？直接回车，以后遇到该问题也是如此操作） 当出现如下图：deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3，则代表运行成功。当然，这只是尝试部署，还没真正到链上。 3、输入npx hardhat run scripts/deploy.js --network goerli并回车。 若出现下图:INSUFFICIENT\_FUNDS代码，则代表钱包每天Goerli网上的ETH。去[https://www.goerlifaucet.com](https://www.goerlifaucet.com) 并登陆Alchemy 帐户以获得一些免费的测试币。 正常成功的部署应该是如下图所示，会出现deployed to：后面的地址就是我们部署的合约地址，复制下来后续步骤会要用到。 **这里有的朋友可能等很久都不会出现合约地址,可以试着按下回车，或者直接去自己的钱包查看链上是否有合约部署记录。** 4、输入npx hardhat run scripts/withdraw.js并回车（中间可能会有一次回车）。 五、部署dapp 1、先回到上一层magweek2(你们命名的可能不一样)，输入cd 并回车(如果已经回到上一层目录了，就不用管了),然后输入cd magweek2。 输入npx create-next-app buyabcacoffee-dapp，并回车 (此目录名可以自命名，后续需保持与此一致。中间可能有一次回车。 **出现SUCCESS表示成功。** 2、输入cd buyabcacoffee-dapp并回车，然后输入npm install --save ethers并回车。 3、安装成功后，左边栏不一定及时出现新目录，需要手动刷新。 （1）先点左边第二个按钮version control，直到出现红框出的buyabcacoffee即可。 （2）再点回第一个按钮file，等待更新目录，会等几分钟。 4、为了让dapp找到合约，需要把BuyABCACoffee产生的合约属性文件拷贝到dapp目录下。操作流程如下：（ABC如果前面有改，则是你们改的名字） （1）点开BuyABCACoffee-contracts目录，选择artifacts/contract/BuyABCACoffee.sol这个目录，选择其中叫BuyABCACoffee.json的文件，先复制其里面的内容到其他地方（比如新建一个记事本拷贝进去），点BuyABCACoffee.json文件右边的三个点，选“rename“，复制其文件名，如图 （2）点击buyabcacoffee-dapp目录右边的三个点，选择“Add folder“，新建utils文件夹。 （3）点击utils右边三个点，选择“Add file“，新建BuyABCACoffee.json文件，再将之前复制的内容拷贝到该文件，即完成。 5、打开pages目录，点击index.js文件，将原文用以下代码替换掉。 **注意：①下面ABC几处同样需要和前面保持一致。②第19行代码“contractAddress”处填上第四大步第3点里面记录的合约地址。③名字（hymnmark**有3处）可以改为任意名字。④@FinderTechnical**处可以改成自己的twitter号。** `import abi from '../utils/BuyABCACoffee.json';` `import { ethers } from "ethers";` `import Head from 'next/head'` `import Image from 'next/image'` `import React, { useEffect, useState } from "react";` `import styles from '../styles/Home.module.css'`   `export default function Home() {`   `// Contract Address & ABI`   `const contractAddress = "0x2a112B58765c93B0151C10D0Aa54a70698859676";`   `const contractABI = abi.abi;`     `// Component state`   `const [currentAccount, setCurrentAccount] = useState("");`   `const [name, setName] = useState("");`   `const [message, setMessage] = useState("");`   `const [memos, setMemos] = useState([]);`     `const onNameChange = (event) => {`     `setName(event.target.value);`   `}`     `const onMessageChange = (event) => {`     `setMessage(event.target.value);`   `}`     `// Wallet connection logic`   `const isWalletConnected = async () => {`     `try {`       `const { ethereum } = window;`         `const accounts = await ethereum.request({ method: 'eth_accounts' })`       `console.log("accounts: ", accounts);`         `if (accounts.length > 0) {`         `const account = accounts[0];`         `console.log("wallet is connected! " + account);`       `} else {`         `console.log("make sure MetaMask is connected");`       `}`     `} catch (error) {`       `console.log("error: ", error);`     `}`   `}`     `const connectWallet = async () => {`     `try {`       `const { ethereum } = window;`         `if (!ethereum) {`         `console.log("please install MetaMask");`       `}`         `const accounts = await ethereum.request({`         `method: 'eth_requestAccounts'`       `});`         `setCurrentAccount(accounts[0]);`     `} catch (error) {`       `console.log(error);`     `}`   `}`     `const buyCoffee = async () => {`     `try {`       `const { ethereum } = window;`         `if (ethereum) {`         `const provider = new ethers.providers.Web3Provider(ethereum, "any");`         `const signer = provider.getSigner();`         `const buyABCACoffee = new ethers.Contract(`           `contractAddress,`           `contractABI,`           `signer`         `);`           `console.log("buying coffee..")`         `const coffeeTxn = await buyABCACoffee.buyCoffee(`           `name ? name : "anon",`           `message ? message : "Enjoy your coffee!",`           `{ value: ethers.utils.parseEther("0.001") }`         `);`           `await coffeeTxn.wait();`           `console.log("mined ", coffeeTxn.hash);`           `console.log("coffee purchased!");`           `// Clear the form fields.`         `setName("");`         `setMessage("");`       `}`     `} catch (error) {`       `console.log(error);`     `}`   `};`     `// Function to fetch all memos stored on-chain.`   `const getMemos = async () => {`     `try {`       `const { ethereum } = window;`       `if (ethereum) {`         `const provider = new ethers.providers.Web3Provider(ethereum);`         `const signer = provider.getSigner();`         `const buyABCACoffee = new ethers.Contract(`           `contractAddress,`           `contractABI,`           `signer`         `);`           `console.log("fetching memos from the blockchain..");`         `const memos = await buyABCACoffee.getMemos();`         `console.log("fetched!");`         `setMemos(memos);`       `} else {`         `console.log("Metamask is not connected");`       `}`       `} catch (error) {`       `console.log(error);`     `}`   `};`     `useEffect(() => {`     `let buyABCACoffee;`     `isWalletConnected();`     `getMemos();`       `// Create an event handler function for when someone sends`     `// us a new memo.`     `const onNewMemo = (from, timestamp, name, message) => {`       `console.log("Memo received: ", from, timestamp, name, message);`       `setMemos((prevState) => [`         `...prevState,`         `{`           `address: from,`           `timestamp: new Date(timestamp * 1000),`           `message,`           `name`         `}`       `]);`     `};`       `const { ethereum } = window;`       `// Listen for new memo events.`     `if (ethereum) {`       `const provider = new ethers.providers.Web3Provider(ethereum, "any");`       `const signer = provider.getSigner();`       `buyABCACoffee = new ethers.Contract(`         `contractAddress,`         `contractABI,`         `signer`       `);`         `buyABCACoffee.on("NewMemo", onNewMemo);`     `}`       `return () => {`       `if (buyABCACoffee) {`         `buyABCACoffee.off("NewMemo", onNewMemo);`       `}`     `}`   `}, []);`     `return (`     `<div className={styles.container}>`       `<Head>`         `<title>Buy hymnmark a Coffee!</title>`         `<meta name="description" content="Tipping site" />`         `<link rel="icon" href="/favicon.ico" />`       `</Head>`         `<main className={styles.main}>`         `<h1 className={styles.title}>`   `Buy hymnmark a Coffee!`         `</h1>`           `{currentAccount ? (`           `<div>`             `<form>`               `<div>`                 `<label>`                   `Name`                 `</label>`                 `<br />`                   `<input`                   `id="name"`                   `type="text"`                   `placeholder="anon"`                   `onChange={onNameChange}`                 `/>`               `</div>`               `<br />`               `<div>`                 `<label>`                   `Send hymnmark a message`                 `</label>`                 `<br />`                   `<textarea`                   `rows={3}`                   `placeholder="Enjoy your coffee!"`                   `id="message"`                   `onChange={onMessageChange}`                   `required`                 `>`                 `</textarea>`               `</div>`               `<div>`                 `<button`                   `type="button"`                   `onClick={buyCoffee}`                 `>`                   `Send 1 Coffee for 0.001ETH`                 `</button>`               `</div>`             `</form>`           `</div>`         `) : (`             `<button onClick={connectWallet}> Connect your wallet </button>`           `)}`       `</main>`         `{currentAccount && (<h1>Memos received</h1>)}`         `{currentAccount && (memos.map((memo, idx) => {`         `return (`           `<div key={idx} style={{ border: "2px solid", "borderRadius": "5px", padding: "5px", margin: "5px" }}>`             `<p style={{ "fontWeight": "bold" }}>"{memo.message}"</p>`             `<p>From: {memo.name} at {memo.timestamp.toString()}</p>`           `</div>`         `)`       `}))}`         `<footer className={styles.footer}>`         `<a`           `href="https://alchemy.com/?a=roadtoweb3weektwo"`           `target="_blank"`           `rel="noopener noreferrer"`         `>`           `Created by @FinderTechnical for Alchemy's Road to Web3 lesson two!`         `</a>`       `</footer>`     `</div>`   `)` `}` 6、开始准备运行dapp了。点击最上面Files右边的三个点，选择“show hidden files“,显示.replit的文件。 7、点击.replit，将原文用以下代码替换：（若之前目录名字是自定义的，请换成你们自定义的） ，最后点击上方绿色按钮“**RUN**” run = "npm --prefix=./buyabcacoffee-dapp run dev" entrypoint = './buyabcacoffee-dapp/pages/index.js' 8、最终出现如下图，即代表成功。我改过名字，所以显示是maggy。 点开右上角“open in a new tab”，可以试用下打赏功能。 9、点击“connect your wallet”连上钱包，进行打赏，然后可以查看钱包是否交易成功。至此所有任务步骤都已经完成，dapp已经完美运行。 10、切记！！！！！！！！删掉.env文件（env文件里有私钥），如下图。 六、发布与提交 1、发布，切记在发布钱删掉.env文件。发布没什么好说的，该填的框框填起来，就是无脑下一步，最后会有个**网址**保存下来作为最后的任务证明就行啦。 2、提交任务 Week 2: [https://forms.gle/HeJrvT72mRcnasr99](https://forms.gle/HeJrvT72mRcnasr99) 打开该网址提交任务。 主要是提交repit项目地址即上一步保存的网址，类似于：[https://replit.com/你的github名字/项目名字?v=1](https://replit.com/%E4%BD%A0%E7%9A%84github%E5%90%8D%E5%AD%97/%E9%A1%B9%E7%9B%AE%E5%90%8D%E5%AD%97?v=1) 这样子。 还有提交**第四步第3点**部署的合约地址，类似于：[https://goerli.etherscan.io/address/合约地址。](https://goerli.etherscan.io/address/%E5%90%88%E7%BA%A6%E5%9C%B0%E5%9D%80%E3%80%82)

---

*Originally published on [Licrazy](https://paragraph.com/@doger/alchemy-road-to-web3-dapp)*
