├── .eslintrc.json ├── .env-example ├── public ├── favicon.ico ├── vercel.svg └── truffle.svg ├── postcss.config.js ├── next.config.js ├── types.d.ts ├── pages ├── api │ └── hello.ts ├── _app.tsx └── index.tsx ├── course-1.mjs ├── tailwind.config.js ├── components ├── Loading.tsx └── Wallet.tsx ├── course-21.mjs ├── .gitignore ├── tsconfig.json ├── styles └── globals.css ├── package.json ├── hooks ├── useListen.tsx └── useMetamask.tsx ├── LICENSE ├── course-14.mjs ├── course-12.mjs ├── course-19.mjs ├── course-8.mjs ├── course-7.mjs ├── README.md ├── course-11.mjs ├── course-10.mjs ├── course-20.mjs ├── course-2.mjs ├── course-9.mjs ├── course-13.mjs ├── course-4.mjs ├── course-5.mjs ├── course-18.mjs ├── course-17.mjs ├── course-16.mjs ├── course-15.mjs ├── course-3.mjs ├── course-6.mjs └── instructions.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.env-example: -------------------------------------------------------------------------------- 1 | export INFURA_API_KEY=123456abcd 2 | export WALLET_PRIVATE_KEY=123456abcd -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarforever/ethers.js-tutorial/main/public/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig 8 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | type InjectedProviders = { 2 | isMetaMask?: true; 3 | }; 4 | 5 | interface Window { 6 | ethereum: InjectedProviders & { 7 | on: (...args: any[]) => void; 8 | removeListener: (...args: any[]) => void; 9 | removeAllListeners: (...args: any[]) => void; 10 | request(args: any): Promise; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | 3 | import type { AppProps } from "next/app"; 4 | import { MetamaskProvider } from "../hooks/useMetamask"; 5 | 6 | function MyApp({ Component, pageProps }: AppProps) { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | 14 | export default MyApp; 15 | -------------------------------------------------------------------------------- /course-1.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an example referring to https://github.com/WTFAcademy/WTF-Ethers/tree/main/01_HelloVitalik 3 | */ 4 | 5 | import { ethers } from "ethers"; 6 | const provider = ethers.getDefaultProvider(); 7 | const main = async () => { 8 | const balance = await provider.getBalance(`vitalik.eth`); 9 | console.log(`ETH Balance of vitalik: ${ethers.utils.formatEther(balance)} ETH`); 10 | } 11 | main() -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./pages/**/*.{js,ts,jsx,tsx}", 5 | "./components/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: { 9 | colors: { 10 | 'ganache': '#3fe0c5', 11 | 'truffle': '#ff6b4a' 12 | } 13 | }, 14 | }, 15 | plugins: [ 16 | require('@tailwindcss/aspect-ratio'),], 17 | } 18 | -------------------------------------------------------------------------------- /components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { type FC } from "react"; 2 | 3 | const dot = `rounded-full h-2 w-2 mx-0.5 bg-current animate-[blink_1s_ease_0s_infinite_normal_both]"`; 4 | 5 | export const Loading: FC = () => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /course-21.mjs: -------------------------------------------------------------------------------- 1 | // 正则表达式, 2 | // ^0x之后跟前几位要匹配的字符 3 | // .*为通配符 4 | // $之前写最后几位要匹配的字符 5 | // 例子:首位两个0,末尾两个1 6 | // const regex = /^0x00.*11$/ 7 | 8 | import { ethers } from "ethers"; 9 | var wallet // 钱包 10 | const regex = /^0x88888888.*$/ // 表达式 11 | var isValid = false 12 | while(!isValid){ 13 | wallet = ethers.Wallet.createRandom() // 随机生成钱包,安全 14 | isValid = regex.test(wallet.address) // 检验正则表达式 15 | //console.log(wallet.address) 16 | } 17 | // 打印靓号地址与私钥 18 | console.log(`\n靓号地址:${wallet.address}`) 19 | console.log(`靓号私钥:${wallet.privateKey}\n`) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | .env -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body { 7 | padding: 0; 8 | margin: 0; 9 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 10 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 11 | } 12 | 13 | a { 14 | color: inherit; 15 | text-decoration: none; 16 | } 17 | 18 | * { 19 | box-sizing: border-box; 20 | } 21 | 22 | @media (prefers-color-scheme: dark) { 23 | html { 24 | color-scheme: dark; 25 | } 26 | body { 27 | color: white; 28 | background: black; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ethers.js-tutorial", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "ethers": "^5.6.9", 13 | "merkletreejs": "^0.3.9", 14 | "next": "12.3.1", 15 | "react": "18.2.0", 16 | "react-dom": "18.2.0" 17 | }, 18 | "devDependencies": { 19 | "@tailwindcss/aspect-ratio": "^0.4.2", 20 | "@types/node": "18.7.23", 21 | "@types/react": "18.0.21", 22 | "@types/react-dom": "18.0.6", 23 | "autoprefixer": "^10.4.12", 24 | "eslint": "8.24.0", 25 | "eslint-config-next": "12.3.1", 26 | "ethers": "^5.6.9", 27 | "postcss": "^8.4.17", 28 | "tailwindcss": "^3.1.8", 29 | "typescript": "4.8.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /hooks/useListen.tsx: -------------------------------------------------------------------------------- 1 | import { useMetamask } from "./useMetamask"; 2 | 3 | export const useListen = () => { 4 | const { dispatch } = useMetamask(); 5 | 6 | return () => { 7 | window.ethereum.on("accountsChanged", async (newAccounts: string[]) => { 8 | if (newAccounts.length > 0) { 9 | // uppon receiving a new wallet, we'll request again the balance to synchronize the UI. 10 | const newBalance = await window.ethereum!.request({ 11 | method: "eth_getBalance", 12 | params: [newAccounts[0], "latest"], 13 | }); 14 | 15 | dispatch({ 16 | type: "connect", 17 | wallet: newAccounts[0], 18 | balance: newBalance, 19 | }); 20 | } else { 21 | // if the length is 0, then the user has disconnected from the wallet UI 22 | dispatch({ type: "disconnect" }); 23 | } 24 | }); 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 sugarforever 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /course-14.mjs: -------------------------------------------------------------------------------- 1 | import { ethers, utils } from "ethers"; 2 | 3 | // 1. 创建HD钱包 4 | console.log("\n1. 创建HD钱包") 5 | // 生成随机助记词 6 | const mnemonic = utils.entropyToMnemonic(utils.randomBytes(32)) 7 | // 创建HD钱包 8 | const hdNode = utils.HDNode.fromMnemonic(mnemonic) 9 | console.log(hdNode); 10 | 11 | // 2. 通过HD钱包派生20个钱包 12 | console.log("\n2. 通过HD钱包派生20个钱包") 13 | const numWallet = 20 14 | // 派生路径:m / purpose' / coin_type' / account' / change / address_index 15 | // 我们只需要切换最后一位address_index,就可以从hdNode派生出新钱包 16 | let basePath = "m/44'/60'/0'/0"; 17 | let wallets = []; 18 | for (let i = 0; i < numWallet; i++) { 19 | let hdNodeNew = hdNode.derivePath(basePath + "/" + i); 20 | let walletNew = new ethers.Wallet(hdNodeNew.privateKey); 21 | console.log(`第${i + 1}个钱包地址: ${walletNew.address}`) 22 | wallets.push(walletNew); 23 | } 24 | 25 | // 3. 保存钱包(加密json) 26 | console.log("\n3. 保存钱包(加密json)") 27 | const wallet = ethers.Wallet.fromMnemonic(mnemonic) 28 | console.log("通过助记词创建钱包:") 29 | console.log(wallet) 30 | // 加密json用的密码,可以更改成别的 31 | const pwd = "password" 32 | const json = await wallet.encrypt(pwd) 33 | console.log("钱包的加密json:") 34 | console.log(json) 35 | 36 | // 4. 从加密json读取钱包 37 | const wallet2 = await ethers.Wallet.fromEncryptedJson(json, pwd); 38 | console.log("\n4. 从加密json读取钱包:") 39 | console.log(wallet2) 40 | 41 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import { useEffect } from "react"; 3 | import Wallet from "../components/Wallet"; 4 | import { useListen } from "../hooks/useListen"; 5 | import { useMetamask } from "../hooks/useMetamask"; 6 | 7 | const Home: NextPage = () => { 8 | const { dispatch } = useMetamask(); 9 | const listen = useListen(); 10 | 11 | useEffect(() => { 12 | if (typeof window !== undefined) { 13 | // start by checking if window.ethereum is present, indicating a wallet extension 14 | const ethereumProviderInjected = typeof window.ethereum !== "undefined"; 15 | // this could be other wallets so we can verify if we are dealing with metamask 16 | // using the boolean constructor to be explecit and not let this be used as a falsy value (optional) 17 | const isMetamaskInstalled = 18 | ethereumProviderInjected && Boolean(window.ethereum.isMetaMask); 19 | 20 | const local = window.localStorage.getItem("metamaskState"); 21 | 22 | // user was previously connected, start listening to MM 23 | if (local) { 24 | listen(); 25 | } 26 | 27 | // local could be null if not present in LocalStorage 28 | const { wallet, balance } = local 29 | ? JSON.parse(local) 30 | : // backup if local storage is empty 31 | { wallet: null, balance: null }; 32 | 33 | dispatch({ type: "pageLoaded", isMetamaskInstalled, wallet, balance }); 34 | } 35 | }, []); 36 | 37 | return ( 38 | <> 39 | 40 | 41 | ); 42 | }; 43 | 44 | export default Home; 45 | -------------------------------------------------------------------------------- /course-12.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an example referring to https://github.com/WTFAcademy/WTF-Ethers/blob/main/12_ERC721Check/ERC721Check.js 3 | */ 4 | 5 | import { ethers } from "ethers"; 6 | 7 | //准备 alchemy API 可以参考https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL04_Alchemy/readme.md 8 | // const ALCHEMY_MAINNET_URL = 'https://eth-mainnet.g.alchemy.com/v2/oKmOQKbneVkxgHZfibs-iFhIlIAl6HDN'; 9 | 10 | // 本代码在WTF的原始示例代码的基础上替换为如下Infura主网endpoint 11 | const INFURA_MAINNET_URL = `https://mainnet.infura.io/v3/${process.env.INFURA_API_KEY}` 12 | const provider = new ethers.providers.JsonRpcProvider(INFURA_MAINNET_URL); 13 | 14 | // 合约abi 15 | const abiERC721 = [ 16 | "function name() view returns (string)", 17 | "function symbol() view returns (string)", 18 | "function supportsInterface(bytes4) public view returns(bool)", 19 | ]; 20 | // ERC721的合约地址,这里用的BAYC 21 | const addressBAYC = "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d" 22 | // 创建ERC721合约实例 23 | const contractERC721 = new ethers.Contract(addressBAYC, abiERC721, provider) 24 | 25 | // ERC721接口的ERC165 identifier 26 | const selectorERC721 = "0x80ac58cd" 27 | 28 | const main = async () => { 29 | try { 30 | // 1. 读取ERC721合约的链上信息 31 | const nameERC721 = await contractERC721.name() 32 | const symbolERC721 = await contractERC721.symbol() 33 | console.log("\n1. 读取ERC721合约信息") 34 | console.log(`合约地址: ${addressBAYC}`) 35 | console.log(`名称: ${nameERC721}`) 36 | console.log(`代号: ${symbolERC721}`) 37 | 38 | // 2. 利用ERC165的supportsInterface,确定合约是否为ERC721标准 39 | const isERC721 = await contractERC721.supportsInterface(selectorERC721) 40 | console.log("\n2. 利用ERC165的supportsInterface,确定合约是否为ERC721标准") 41 | console.log(`合约是否为ERC721标准: ${isERC721}`) 42 | }catch (e) { 43 | // 如果不是ERC721,则会报错 44 | console.log(e); 45 | } 46 | } 47 | 48 | main() -------------------------------------------------------------------------------- /course-19.mjs: -------------------------------------------------------------------------------- 1 | // provider.on("pending", listener) 2 | import { ethers, utils } from "ethers"; 3 | 4 | // 1. 创建provider和wallet,监听事件时候推荐用wss连接而不是http 5 | console.log("\n1. 连接 wss RPC") 6 | // 准备 alchemy API 可以参考https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL04_Alchemy/readme.md 7 | const ALCHEMY_MAINNET_WSSURL = 'wss://eth-mainnet.g.alchemy.com/v2/oKmOQKbneVkxgHZfibs-iFhIlIAl6HDN'; 8 | const provider = new ethers.providers.WebSocketProvider(ALCHEMY_MAINNET_WSSURL); 9 | let network = provider.getNetwork() 10 | // network.then(res => console.log(`[${(new Date).toLocaleTimeString()}] 连接到 chain ID ${res.chainId}`)); 11 | 12 | console.log("\n2. 限制调用rpc接口速率") 13 | // 2. 限制访问rpc速率,不然调用频率会超出限制,报错。 14 | function throttle(fn, delay) { 15 | let timer; 16 | return function () { 17 | if (!timer) { 18 | fn.apply(this, arguments) 19 | timer = setTimeout(() => { 20 | clearTimeout(timer) 21 | timer = null 22 | }, delay) 23 | } 24 | } 25 | } 26 | 27 | const main = async () => { 28 | let i = 0; 29 | // 3. 监听pending交易,获取txHash 30 | console.log("\n3. 监听pending交易,打印txHash。") 31 | provider.on("pending", async (txHash) => { 32 | if (txHash && i < 100) { 33 | // 打印txHash 34 | console.log(`[${(new Date).toLocaleTimeString()}] 监听Pending交易 ${i}: ${txHash} \r`); 35 | i++ 36 | } 37 | }); 38 | 39 | // 4. 监听pending交易,并获取交易详情 40 | console.log("\n4. 监听pending交易,获取txHash,并输出交易详情。") 41 | let j = 0 42 | provider.on("pending", throttle(async (txHash) => { 43 | if (txHash && j <= 100) { 44 | // 获取tx详情 45 | let tx = await provider.getTransaction(txHash); 46 | console.log(`\n[${(new Date).toLocaleTimeString()}] 监听Pending交易 ${j}: ${txHash} \r`); 47 | console.log(tx); 48 | j++ 49 | } 50 | }, 1000)); 51 | }; 52 | 53 | main() -------------------------------------------------------------------------------- /course-8.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an example referring to https://github.com/WTFAcademy/WTF-Ethers/blob/main/08_ContractListener/ContractListener.js 3 | */ 4 | 5 | // 监听合约方法: 6 | // 1. 持续监听 7 | // contractUSDT.on("事件名", Listener) 8 | // 2. 只监听一次 9 | // contractUSDT.once("事件名", Listener) 10 | 11 | import { ethers } from "ethers"; 12 | 13 | // 准备 alchemy API 14 | // 可以参考https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL04_Alchemy/readme.md 15 | // const ALCHEMY_MAINNET_URL = 'https://eth-mainnet.g.alchemy.com/v2/oKmOQKbneVkxgHZfibs-iFhIlIAl6HDN'; 16 | 17 | // 本代码在WTF的原始示例代码的基础上替换为如下Infura主网endpoint 18 | const INFURA_MAINNET_URL = `https://mainnet.infura.io/v3/${process.env.INFURA_API_KEY}` 19 | // 连接主网 provider 20 | const provider = new ethers.providers.JsonRpcProvider(INFURA_MAINNET_URL); 21 | 22 | // USDT的合约地址 23 | const contractAddress = '0xdac17f958d2ee523a2206206994597c13d831ec7' 24 | // 构建USDT的Transfer的ABI 25 | const abi = [ 26 | "event Transfer(address indexed from, address indexed to, uint value)" 27 | ]; 28 | // 生成USDT合约对象 29 | const contractUSDT = new ethers.Contract(contractAddress, abi, provider); 30 | 31 | 32 | const main = async () => { 33 | // 监听USDT合约的Transfer事件 34 | 35 | try{ 36 | // 只监听一次 37 | console.log("\n1. 利用contract.once(),监听一次Transfer事件"); 38 | contractUSDT.once('Transfer', (from, to, value)=>{ 39 | // 打印结果 40 | console.log( 41 | `ONCE: ${from} -> ${to} ${ethers.utils.formatUnits(ethers.BigNumber.from(value),6)}` 42 | ) 43 | }) 44 | 45 | // 持续监听USDT合约 46 | console.log("\n2. 利用contract.on(),持续监听Transfer事件"); 47 | contractUSDT.on('Transfer', (from, to, value, event)=>{ 48 | console.log( 49 | // 打印结果 50 | `${from} -> ${to} ${ethers.utils.formatUnits(ethers.BigNumber.from(value),6)}`, 51 | event, 52 | new Date().toLocaleString() 53 | ) 54 | }) 55 | 56 | }catch(e){ 57 | console.log(e); 58 | 59 | } 60 | } 61 | main() -------------------------------------------------------------------------------- /course-7.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an example referring to https://github.com/WTFAcademy/WTF-Ethers/blob/main/07_Event/Event.js 3 | */ 4 | 5 | // 检索事件的方法: 6 | // const transferEvents = await contract.queryFilter("事件名", [起始区块高度,结束区块高度]) 7 | // 其中起始区块高度和结束区块高度为选填参数。 8 | 9 | import { ethers } from "ethers"; 10 | // playcode免费版不能安装ethers,用这条命令,需要从网络上import包(把上面这行注释掉) 11 | // import { ethers } from "https://cdn-cors.ethers.io/lib/ethers-5.6.9.esm.min.js"; 12 | 13 | // 利用Alchemy的rpc节点连接以太坊网络 14 | // 准备 alchemy API 可以参考https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL04_Alchemy/readme.md 15 | // const ALCHEMY_GOERLI_URL = 'https://eth-goerli.alchemyapi.io/v2/GlaeWuylnNM3uuOo-SAwJxuwTdqHaY5l'; 16 | 17 | // 本代码在WTF的原始示例代码的基础上替换为如下Infura测试网endpoint 18 | const INFURA_GOERLI_URL = `https://goerli.infura.io/v3/${process.env.INFURA_API_KEY}` 19 | const provider = new ethers.providers.JsonRpcProvider(INFURA_GOERLI_URL); 20 | 21 | // WETH ABI,只包含我们关心的Transfer事件 22 | const abiWETH = [ 23 | "event Transfer(address indexed from, address indexed to, uint amount)" 24 | ]; 25 | 26 | // 测试网WETH地址 27 | const addressWETH = '0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6' 28 | // 声明合约实例 29 | const contract = new ethers.Contract(addressWETH, abiWETH, provider) 30 | 31 | const main = async () => { 32 | 33 | // 获取过去10个区块内的Transfer事件 34 | console.log("\n1. 获取过去10个区块内的Transfer事件,并打印出1个"); 35 | // 得到当前block 36 | const block = await provider.getBlockNumber() 37 | console.log(`当前区块高度: ${block}`); 38 | console.log(`打印事件详情:`); 39 | const transferEvents = await contract.queryFilter('Transfer', block - 10, block) 40 | // 打印事件数量 41 | console.log(`事件数: ${transferEvents.length}`) 42 | // 打印第1个Transfer事件 43 | console.log(transferEvents[0]) 44 | 45 | // 解析Transfer事件的数据(变量在args中) 46 | console.log("\n2. 解析事件:") 47 | const amount = ethers.utils.formatUnits(ethers.BigNumber.from(transferEvents[0].args["amount"]), "ether"); 48 | console.log(`地址 ${transferEvents[0].args["from"]} 转账${amount} WETH 到地址 ${transferEvents[0].args["to"]}`) 49 | } 50 | 51 | main() 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Web3 Unleashed 2 | 3 | Basic Next.js application to be used in a stream for Web3 Unleashed. 4 | 5 | Branches will be created with stages for the rest of the implementation 6 | 7 | ## Instructions: 8 | 9 | You are presented with a Next.js application that has a basic layout and a Context Provider. Your goal is to learn how to use the Metamask API to make this an application that users can interact with. 10 | 11 | Few notes about the initial setup and constraints: 12 | 13 | - You do not need any additional dependencies. 14 | - The context provider is already setup so that any react code under [components](./components/) and [pages](./pages/) can access the context using a hook. 15 | - The context uses a reducer to manage state, so you can dispatch actions to update the state. Feel free to edit and add to the reducer as needed. 16 | - Next.js runs in multiple environments, you will have to remember that Metamask is only available in the browser, so you will need to check for that before using it. 17 | 18 | Have fun! 19 | 20 | ### 1 Basics: connecting the user 21 | 22 | This is the time to get familiar with how to work with a Next.js application and Metamask. 23 | 24 | - Verify if the user has Metamask installed or not. If not, link to the Metamask website. (hint: use the `window.ethereum` object) 25 | - If the user has Metamask installed, offer the user to connect their wallet and save this information in the context. (hint: use the `window.ethereum.request` method) 26 | 27 | ### 2. Novice: use the Metamask API to get information about the user 28 | 29 | - Now that the user can connect, display the wallet address, balance of the wallet. 30 | 31 | ### 3. Intermediate: Two way communication with Metamask 32 | 33 | - Listen to changes in the wallet address and balance and update the UI accordingly. 34 | - Add an option to add a new token to the wallet. 35 | 36 | ### 4. Bonus/Advanced: UX goodies 37 | 38 | - Persist the wallet address and balance in the local storage. 39 | - Add a button to disconnect the wallet. 40 | 41 | ### 5. Cleaning Up: 42 | 43 | - removing event listeners if people disconnect 44 | -------------------------------------------------------------------------------- /course-11.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an example referring to https://github.com/WTFAcademy/WTF-Ethers/blob/main/11_CallStatic/CallStatic.js 3 | */ 4 | 5 | // contract.callStatic.函数名(参数, {override}) 6 | import { ethers } from "ethers"; 7 | 8 | //准备 alchemy API 可以参考https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL04_Alchemy/readme.md 9 | // const ALCHEMY_MAINNET_URL = 'https://eth-mainnet.g.alchemy.com/v2/oKmOQKbneVkxgHZfibs-iFhIlIAl6HDN'; 10 | 11 | // 本代码在WTF的原始示例代码的基础上替换为如下Infura主网endpoint 12 | const INFURA_MAINNET_URL = `https://mainnet.infura.io/v3/${process.env.INFURA_API_KEY}` 13 | const provider = new ethers.providers.JsonRpcProvider(INFURA_MAINNET_URL); 14 | 15 | // 利用私钥和provider创建wallet对象 16 | const privateKey = process.env.WALLET_PRIVATE_KEY 17 | const wallet = new ethers.Wallet(privateKey, provider) 18 | 19 | // DAI的ABI 20 | const abiDAI = [ 21 | "function balanceOf(address) public view returns(uint)", 22 | "function transfer(address, uint) public returns (bool)", 23 | ]; 24 | // DAI合约地址(主网) 25 | const addressDAI = '0x6B175474E89094C44Da98b954EedeAC495271d0F' // DAI Contract 26 | 27 | // 创建DAI合约实例 28 | const contractDAI = new ethers.Contract(addressDAI, abiDAI, provider) 29 | 30 | const main = async () => { 31 | try { 32 | const address = await wallet.getAddress() 33 | // 1. 读取DAI合约的链上信息 34 | console.log("\n1. 读取测试钱包的DAI余额") 35 | const balanceDAI = await contractDAI.balanceOf(address) 36 | console.log(`DAI持仓: ${ethers.utils.formatEther(balanceDAI)}\n`) 37 | 38 | // 2. 用callStatic尝试调用transfer转账10000 DAI,msg.sender为V神,交易将成功 39 | console.log("\n2. 用callStatic尝试调用transfer转账1 DAI,msg.sender为V神地址") 40 | // 发起交易 41 | const tx = await contractDAI.callStatic.transfer("vitalik.eth", ethers.utils.parseEther("10000"), {from: "vitalik.eth"}) 42 | console.log(`交易会成功吗?:`, tx) 43 | 44 | // 3. 用callStatic尝试调用transfer转账10000 DAI,msg.sender为测试钱包地址,交易将失败 45 | console.log("\n3. 用callStatic尝试调用transfer转账1 DAI,msg.sender为测试钱包地址") 46 | const tx2 = await contractDAI.callStatic.transfer("vitalik.eth", ethers.utils.parseEther("10000"), {from: address}) 47 | console.log(`交易会成功吗?:`, tx) 48 | 49 | } catch (e) { 50 | console.log(e); 51 | } 52 | } 53 | 54 | main() 55 | -------------------------------------------------------------------------------- /course-10.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an example referring to https://github.com/WTFAcademy/WTF-Ethers/blob/main/09_EventFilter/EventFilter.js 3 | */ 4 | 5 | import { ethers } from "ethers"; 6 | 7 | // 1. BigNumber 8 | console.group('\n1. BigNumber类'); 9 | 10 | const oneGwei = ethers.BigNumber.from("1000000000"); // 从十进制字符串生成 11 | console.log(oneGwei) 12 | console.log(ethers.BigNumber.from("0x3b9aca00")) // 从hex字符串生成 13 | console.log(ethers.BigNumber.from(1000000000)) // 从数字生成 14 | // 不能从js最大的安全整数之外的数字生成BigNumber,下面代码会报错 15 | ethers.BigNumber.from(Number.MAX_SAFE_INTEGER); 16 | console.log("js中最大安全整数:", Number.MAX_SAFE_INTEGER) 17 | 18 | // 运算 19 | console.log("加法:", oneGwei.add(1).toString()) 20 | console.log("减法:", oneGwei.sub(1).toString()) 21 | console.log("乘法:", oneGwei.mul(2).toString()) 22 | console.log("除法:", oneGwei.div(2).toString()) 23 | // 比较 24 | console.log("是否相等:", oneGwei.eq("1000000000")) 25 | 26 | 27 | // 2. 格式化:小单位转大单位 28 | // 例如将wei转换为ether:formatUnits(变量, 单位):单位填位数(数字)或指定的单位(字符串) 29 | console.group('\n2. 格式化:小单位转大单位,formatUnits'); 30 | console.log(ethers.utils.formatUnits(oneGwei, 0)); 31 | // '1000000000' 32 | console.log(ethers.utils.formatUnits(oneGwei, "gwei")); 33 | // '1.0' 34 | console.log(ethers.utils.formatUnits(oneGwei, 9)); 35 | // '1.0' 36 | console.log(ethers.utils.formatUnits(oneGwei, "ether")); 37 | // `0.000000001` 38 | console.log(ethers.utils.formatUnits(1000000000, "gwei")); 39 | // '1.0' 40 | console.log(ethers.utils.formatEther(oneGwei)); 41 | // `0.000000001` 等同于formatUnits(value, "ether") 42 | console.groupEnd(); 43 | 44 | 45 | // 3. 解析:大单位转小单位 46 | // 例如将ether转换为wei:parseUnits(变量, 单位) 47 | console.group('\n3. 解析:大单位转小单位,parseUnits'); 48 | console.log(ethers.utils.parseUnits("1.0").toString()); 49 | // { BigNumber: "1000000000000000000" } 50 | console.log(ethers.utils.parseUnits("1.0", "ether").toString()); 51 | // { BigNumber: "1000000000000000000" } 52 | console.log(ethers.utils.parseUnits("1.0", 18).toString()); 53 | // { BigNumber: "1000000000000000000" } 54 | console.log(ethers.utils.parseUnits("1.0", "gwei").toString()); 55 | // { BigNumber: "1000000000" } 56 | console.log(ethers.utils.parseUnits("1.0", 9).toString()); 57 | // { BigNumber: "1000000000" } 58 | console.log(ethers.utils.parseEther("1.0").toString()); 59 | // { BigNumber: "1000000000000000000" } 等同于parseUnits(value, "ether") 60 | console.groupEnd(); 61 | -------------------------------------------------------------------------------- /course-20.mjs: -------------------------------------------------------------------------------- 1 | // provider.on("pending", listener) 2 | import { ethers, utils} from "ethers"; 3 | 4 | // 1. 创建provider和wallet,监听事件时候推荐用wss连接而不是http 5 | // 准备 alchemy API 可以参考https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL04_Alchemy/readme.md 6 | const ALCHEMY_MAINNET_WSSURL = 'wss://eth-mainnet.g.alchemy.com/v2/oKmOQKbneVkxgHZfibs-iFhIlIAl6HDN'; 7 | const provider = new ethers.providers.WebSocketProvider(ALCHEMY_MAINNET_WSSURL); 8 | let network = provider.getNetwork() 9 | network.then(res => console.log(`[${(new Date).toLocaleTimeString()}] 连接到 chain ID ${res.chainId}`)); 10 | 11 | // 2. 创建interface对象,用于解码交易详情。 12 | const iface = new utils.Interface([ 13 | "function exactInputSingle(tuple(address tokenIn, address tokenOut, uint24 fee, address recipient, uint deadline, uint amountIn, uint amountOutMinimum, uint160 sqrtPriceLimitX96) calldata) external payable returns (uint amountOut)", 14 | ]) 15 | 16 | // 3. 限制访问rpc速率,不然调用频率会超出限制,报错。 17 | function throttle(fn, delay) { 18 | let timer; 19 | return function(){ 20 | if(!timer) { 21 | fn.apply(this, arguments) 22 | timer = setTimeout(()=>{ 23 | clearTimeout(timer) 24 | timer = null 25 | },delay) 26 | } 27 | } 28 | } 29 | 30 | const main = async () => { 31 | // 4. 监听pending的uniswapV3交易,获取交易详情,然后解码。 32 | // 网络不活跃的时候,可能需要等待几分钟才能监听到一笔。 33 | console.log("\n4. 监听pending交易,获取txHash,并输出交易详情。") 34 | provider.on("pending", throttle(async (txHash) => { 35 | if (txHash) { 36 | // 获取tx详情 37 | let tx = await provider.getTransaction(txHash); 38 | if (tx) { 39 | // filter pendingTx.data 40 | if (tx.data.indexOf(iface.getSighash("exactInputSingle")) !== -1) { 41 | // 打印txHash 42 | console.log(`\n[${(new Date).toLocaleTimeString()}] 监听Pending交易: ${txHash} \r`); 43 | 44 | // 打印解码的交易详情 45 | let parsedTx = iface.parseTransaction(tx) 46 | console.log("pending交易详情解码:") 47 | console.log(parsedTx); 48 | // Input data解码 49 | console.log("Input Data解码:") 50 | console.log(parsedTx.args); 51 | } 52 | } 53 | } 54 | }, 100)); 55 | }; 56 | 57 | main() 58 | -------------------------------------------------------------------------------- /course-2.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an example referring to https://github.com/WTFAcademy/WTF-Ethers/tree/main/02_Provider 3 | */ 4 | 5 | // 导入ethers包 6 | import { ethers } from "ethers"; 7 | // playcode免费版不能安装ethers,用这条命令,需要从网络上import包(把上面这行注释掉) 8 | // import { ethers } from "https://cdn-cors.ethers.io/lib/ethers-5.6.9.esm.min.js"; 9 | 10 | // 利用Alchemy的rpc节点连接以太坊网络 11 | // 准备 alchemy API 可以参考https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL04_Alchemy/readme.md 12 | const ALCHEMY_MAINNET_URL = 'https://eth-mainnet.g.alchemy.com/v2/oKmOQKbneVkxgHZfibs-iFhIlIAl6HDN'; 13 | const ALCHEMY_GOERLI_URL = 'https://eth-goerli.alchemyapi.io/v2/GlaeWuylnNM3uuOo-SAwJxuwTdqHaY5l'; 14 | // 连接以太坊主网 15 | const providerETH = new ethers.providers.JsonRpcProvider(ALCHEMY_MAINNET_URL) 16 | // 连接Goerli测试网 17 | const providerGoerli = new ethers.providers.JsonRpcProvider(ALCHEMY_GOERLI_URL) 18 | 19 | const main = async () => { 20 | // 利用provider读取链上信息 21 | // 1. 查询vitalik在主网和Goerli测试网的ETH余额 22 | console.log("1. 查询vitalik在主网和Goerli测试网的ETH余额"); 23 | const balance = await providerETH.getBalance(`vitalik.eth`); 24 | const balanceGoerli = await providerGoerli.getBalance(`vitalik.eth`); 25 | // 将余额输出在console(主网) 26 | console.log(`ETH Balance of vitalik: ${ethers.utils.formatEther(balance)} ETH`); 27 | // 输出Goerli测试网ETH余额 28 | console.log(`Goerli ETH Balance of vitalik: ${ethers.utils.formatEther(balanceGoerli)} ETH`); 29 | 30 | // 2. 查询provider连接到了哪条链 31 | console.log("\n2. 查询provider连接到了哪条链") 32 | const network = await providerETH.getNetwork(); 33 | console.log(network); 34 | 35 | // 3. 查询区块高度 36 | console.log("\n3. 查询区块高度") 37 | const blockNumber = await providerETH.getBlockNumber(); 38 | console.log(blockNumber); 39 | 40 | // 4. 查询当前gas price 41 | console.log("\n4. 查询当前gas price") 42 | const gasPrice = await providerETH.getGasPrice(); 43 | console.log(gasPrice); 44 | 45 | // 5. 查询当前建议的gas设置 46 | console.log("\n5. 查询当前建议的gas设置") 47 | const feeData = await providerETH.getFeeData(); 48 | console.log(feeData); 49 | 50 | // 6. 查询区块信息 51 | console.log("\n6. 查询区块信息") 52 | const block = await providerETH.getBlock(0); 53 | console.log(block); 54 | 55 | // 7. 给定合约地址查询合约bytecode,例子用的WETH地址 56 | console.log("\n7. 给定合约地址查询合约bytecode,例子用的WETH地址") 57 | const code = await providerETH.getCode("0xc778417e063141139fce010982780140aa0cd5ab"); 58 | console.log(code); 59 | 60 | } 61 | 62 | main() -------------------------------------------------------------------------------- /course-9.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an example referring to https://github.com/WTFAcademy/WTF-Ethers/blob/main/09_EventFilter/EventFilter.js 3 | */ 4 | 5 | import { ethers } from "ethers"; 6 | 7 | // 利用Alchemy的rpc节点连接以太坊网络 8 | // 准备 alchemy API 可以参考https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL04_Alchemy/readme.md 9 | // const ALCHEMY_MAINNET_URL = 'https://eth-mainnet.g.alchemy.com/v2/oKmOQKbneVkxgHZfibs-iFhIlIAl6HDN'; 10 | 11 | // 本代码在WTF的原始示例代码的基础上替换为如下Infura主网endpoint 12 | const INFURA_MAINNET_URL = `https://mainnet.infura.io/v3/${process.env.INFURA_API_KEY}` 13 | const provider = new ethers.providers.JsonRpcProvider(INFURA_MAINNET_URL); 14 | 15 | // 合约地址 16 | const addressUSDT = '0xdac17f958d2ee523a2206206994597c13d831ec7' 17 | // 交易所地址 18 | const accountBinance = '0x28C6c06298d514Db089934071355E5743bf21d60' 19 | // 构建ABI 20 | const abi = [ 21 | "event Transfer(address indexed from, address indexed to, uint value)", 22 | "function balanceOf(address) public view returns(uint)", 23 | ]; 24 | // 构建合约对象 25 | const contractUSDT = new ethers.Contract(addressUSDT, abi, provider); 26 | 27 | 28 | (async () => { 29 | try { 30 | // 1. 读取币安热钱包USDT余额 31 | console.log("\n1. 读取币安热钱包USDT余额") 32 | const balanceUSDT = await contractUSDT.balanceOf(accountBinance) 33 | console.log(`USDT余额: ${ethers.utils.formatUnits(ethers.BigNumber.from(balanceUSDT),6)}\n`) 34 | 35 | // 2. 创建过滤器,监听转移USDT进交易所 36 | console.log("\n2. 创建过滤器,监听转移USDT进交易所") 37 | let filterBinanceIn = contractUSDT.filters.Transfer(null, accountBinance); 38 | console.log("过滤器详情:") 39 | console.log(filterBinanceIn); 40 | contractUSDT.on(filterBinanceIn, (from, to, value) => { 41 | console.log('---------监听USDT进入交易所--------'); 42 | console.log( 43 | `${from} -> ${to} ${ethers.utils.formatUnits(ethers.BigNumber.from(value),6)}` 44 | ) 45 | }).on('error', (error) => { 46 | console.log(error) 47 | }) 48 | 49 | // 3. 创建过滤器,监听交易所转出USDT 50 | let filterToBinanceOut = contractUSDT.filters.Transfer(accountBinance, null); 51 | console.log("\n3. 创建过滤器,监听转移USDT出交易所") 52 | console.log("过滤器详情:") 53 | console.log(filterToBinanceOut); 54 | contractUSDT.on(filterToBinanceOut, (from, to, value) => { 55 | console.log('---------监听USDT转出交易所--------'); 56 | console.log( 57 | `${from} -> ${to} ${ethers.utils.formatUnits(ethers.BigNumber.from(value),6)}` 58 | ) 59 | } 60 | ).on('error', (error) => { 61 | console.log(error) 62 | }); 63 | } catch (e) { 64 | console.log(e); 65 | } 66 | })() -------------------------------------------------------------------------------- /hooks/useMetamask.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, type PropsWithChildren } from "react"; 2 | 3 | type ConnectAction = { type: "connect"; wallet: string; balance: string }; 4 | type DisconnectAction = { type: "disconnect" }; 5 | type PageLoadedAction = { 6 | type: "pageLoaded"; 7 | isMetamaskInstalled: boolean; 8 | wallet: string | null; 9 | balance: string | null; 10 | }; 11 | type LoadingAction = { type: "loading" }; 12 | type IdleAction = { type: "idle" }; 13 | 14 | type Action = 15 | | ConnectAction 16 | | DisconnectAction 17 | | PageLoadedAction 18 | | LoadingAction 19 | | IdleAction; 20 | 21 | type Dispatch = (action: Action) => void; 22 | 23 | type Status = "loading" | "idle" | "pageNotLoaded"; 24 | 25 | type State = { 26 | wallet: string | null; 27 | isMetamaskInstalled: boolean; 28 | status: Status; 29 | balance: string | null; 30 | }; 31 | 32 | const initialState: State = { 33 | wallet: null, 34 | isMetamaskInstalled: false, 35 | status: "loading", 36 | balance: null, 37 | } as const; 38 | 39 | function metamaskReducer(state: State, action: Action): State { 40 | switch (action.type) { 41 | case "connect": { 42 | const { wallet, balance } = action; 43 | const newState = { ...state, wallet, balance, status: "idle" } as State; 44 | const info = JSON.stringify(newState); 45 | window.localStorage.setItem("metamaskState", info); 46 | 47 | return newState; 48 | } 49 | case "disconnect": { 50 | window.localStorage.removeItem("metamaskState"); 51 | if (typeof window.ethereum !== undefined) { 52 | window.ethereum.removeAllListeners(["accountsChanged"]); 53 | } 54 | return { ...state, wallet: null, balance: null }; 55 | } 56 | case "pageLoaded": { 57 | const { isMetamaskInstalled, balance, wallet } = action; 58 | return { ...state, isMetamaskInstalled, status: "idle", wallet, balance }; 59 | } 60 | case "loading": { 61 | return { ...state, status: "loading" }; 62 | } 63 | case "idle": { 64 | return { ...state, status: "idle" }; 65 | } 66 | 67 | default: { 68 | throw new Error("Unhandled action type"); 69 | } 70 | } 71 | } 72 | 73 | const MetamaskContext = React.createContext< 74 | { state: State; dispatch: Dispatch } | undefined 75 | >(undefined); 76 | 77 | function MetamaskProvider({ children }: PropsWithChildren) { 78 | const [state, dispatch] = React.useReducer(metamaskReducer, initialState); 79 | const value = { state, dispatch }; 80 | 81 | return ( 82 | 83 | {children} 84 | 85 | ); 86 | } 87 | 88 | function useMetamask() { 89 | const context = React.useContext(MetamaskContext); 90 | if (context === undefined) { 91 | throw new Error("useMetamask must be used within a MetamaskProvider"); 92 | } 93 | return context; 94 | } 95 | 96 | export { MetamaskProvider, useMetamask }; 97 | -------------------------------------------------------------------------------- /course-13.mjs: -------------------------------------------------------------------------------- 1 | // Interface 接口类 2 | // 利用abi生成 3 | // const interface = ethers.utils.Interface(abi) 4 | // 直接从contract中获取 5 | // const interface2 = contract.interface 6 | import { ethers } from "ethers"; 7 | 8 | //准备 alchemy API 可以参考https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL04_Alchemy/readme.md 9 | // const ALCHEMY_GOERLI_URL = 'https://eth-goerli.alchemyapi.io/v2/GlaeWuylnNM3uuOo-SAwJxuwTdqHaY5l'; 10 | // const provider = new ethers.providers.JsonRpcProvider(ALCHEMY_GOERLI_URL); 11 | 12 | // 本代码在WTF的原始示例代码的基础上替换为如下Infura主网endpoint 13 | const INFURA_GOERLI_URL = `https://goerli.infura.io/v3/${process.env.INFURA_API_KEY}` 14 | const provider = new ethers.providers.JsonRpcProvider(INFURA_GOERLI_URL); 15 | 16 | // 利用私钥和provider创建wallet对象 17 | const privateKey = process.env.WALLET_PRIVATE_KEY 18 | const wallet = new ethers.Wallet(privateKey, provider) 19 | 20 | // WETH的ABI 21 | const abiWETH = [ 22 | "function balanceOf(address) public view returns(uint)", 23 | "function deposit() public payable", 24 | ]; 25 | // WETH合约地址(Goerli测试网) 26 | const addressWETH = '0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6' 27 | 28 | // 声明WETH合约 29 | const contractWETH = new ethers.Contract(addressWETH, abiWETH, wallet) 30 | 31 | const main = async () => { 32 | 33 | const address = await wallet.getAddress() 34 | // 1. 读取WETH合约的链上信息(WETH abi) 35 | console.log("\n1. 读取WETH余额") 36 | // 编码calldata 37 | const param1 = contractWETH.interface.encodeFunctionData( 38 | "balanceOf", 39 | [address] 40 | ); 41 | console.log(`编码结果: ${param1}`) 42 | // 创建交易 43 | const tx1 = { 44 | to: addressWETH, 45 | data: param1 46 | } 47 | // 发起交易,可读操作(view/pure)可以用 provider.call(tx) 48 | const balanceWETH = await provider.call(tx1) 49 | console.log(`存款前WETH持仓: ${ethers.utils.formatEther(balanceWETH)}\n`) 50 | 51 | //读取钱包内ETH余额 52 | const balanceETH = await wallet.getBalance() 53 | // 如果钱包ETH足够 54 | if(ethers.utils.formatEther(balanceETH) > 0.0015){ 55 | 56 | // 2. 调用desposit()函数,将0.001 ETH转为WETH 57 | console.log("\n2. 调用desposit()函数,存入0.001 ETH") 58 | // 编码calldata 59 | const param2 = contractWETH.interface.encodeFunctionData( 60 | "deposit" 61 | ); 62 | console.log(`编码结果: ${param2}`) 63 | // 创建交易 64 | const tx2 = { 65 | to: addressWETH, 66 | data: param2, 67 | value: ethers.utils.parseEther("0.001")} 68 | // 发起交易,写入操作需要 wallet.sendTransaction(tx) 69 | const receipt1 = await wallet.sendTransaction(tx2) 70 | // 等待交易上链 71 | await receipt1.wait() 72 | console.log(`交易详情:`) 73 | console.log(receipt1) 74 | const balanceWETH_deposit = await contractWETH.balanceOf(address) 75 | console.log(`存款后WETH持仓: ${ethers.utils.formatEther(balanceWETH_deposit)}\n`) 76 | 77 | }else{ 78 | // 如果ETH不足 79 | console.log("ETH不足,去水龙头领一些Goerli ETH") 80 | console.log("1. chainlink水龙头: https://faucets.chain.link/goerli") 81 | console.log("2. paradigm水龙头: https://faucet.paradigm.xyz/") 82 | } 83 | } 84 | 85 | main() 86 | 87 | 88 | -------------------------------------------------------------------------------- /course-4.mjs: -------------------------------------------------------------------------------- 1 | // 利用Wallet类发送ETH 2 | // 由于playcode不支持ethers.Wallet.createRandom()函数,我们只能用VScode运行这一讲代码 3 | import { ethers } from "ethers"; 4 | 5 | // 利用Alchemy的rpc节点连接以太坊测试网络 6 | // 准备 alchemy API 可以参考https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL04_Alchemy/readme.md 7 | // const ALCHEMY_GOERLI_URL = 'https://eth-goerli.alchemyapi.io/v2/GlaeWuylnNM3uuOo-SAwJxuwTdqHaY5l'; 8 | const INFURA_GOERLI_URL = `https://goerli.infura.io/v3/${process.env.INFURA_API_KEY}`; 9 | const provider = new ethers.providers.JsonRpcProvider(INFURA_GOERLI_URL); 10 | 11 | // 创建随机的wallet对象 12 | const wallet1 = ethers.Wallet.createRandom() 13 | const wallet1WithProvider = wallet1.connect(provider) 14 | const mnemonic = wallet1.mnemonic // 获取助记词 15 | 16 | // 利用私钥和provider创建wallet对象 17 | const privateKey = process.env.WALLET_PRIVATE_KEY 18 | 19 | const wallet2 = new ethers.Wallet(privateKey, provider) 20 | 21 | // 从助记词创建wallet对象 22 | const wallet3 = new ethers.Wallet.fromMnemonic(mnemonic.phrase) 23 | 24 | const main = async () => { 25 | // 1. 获取钱包地址 26 | const address1 = await wallet1.getAddress() 27 | const address2 = await wallet2.getAddress() 28 | const address3 = await wallet3.getAddress() // 获取地址 29 | console.log(`1. 获取钱包地址`); 30 | console.log(`钱包1地址: ${address1}`); 31 | console.log(`钱包2地址: ${address2}`); 32 | console.log(`钱包3地址: ${address3}`); 33 | console.log(`钱包1和钱包3的地址是否相同: ${address1 === address3}`); 34 | 35 | // 2. 获取助记词 36 | console.log(`\n2. 获取助记词`); 37 | console.log(`钱包1助记词: ${wallet1.mnemonic.phrase}`) 38 | // 注意:从private key生成的钱包没有助记词 39 | // console.log(wallet2.mnemonic.phrase) 40 | 41 | // 3. 获取私钥 42 | console.log(`\n3. 获取私钥`); 43 | console.log(`钱包1私钥: ${wallet1.privateKey.slice(0, 10)}...`) 44 | console.log(`钱包2私钥: ${wallet2.privateKey.slice(0, 10)}...`) 45 | 46 | // 4. 获取链上发送交易次数 47 | console.log(`\n4. 获取链上交易次数`); 48 | const txCount1 = await wallet1WithProvider.getTransactionCount() 49 | const txCount2 = await wallet2.getTransactionCount() 50 | console.log(`钱包1发送交易次数: ${txCount1}`) 51 | console.log(`钱包2发送交易次数: ${txCount2}`) 52 | 53 | // 5. 发送ETH 54 | // 如果这个钱包没goerli测试网ETH了,去水龙头领一些,钱包地址: 0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2 55 | // 1. chainlink水龙头: https://faucets.chain.link/goerli 56 | // 2. paradigm水龙头: https://faucet.paradigm.xyz/ 57 | console.log(`\n5. 发送ETH(测试网)`); 58 | // i. 打印交易前余额 59 | console.log(`i. 发送前余额`) 60 | console.log(`钱包1: ${ethers.utils.formatEther(await wallet1WithProvider.getBalance())} ETH`) 61 | console.log(`钱包2: ${ethers.utils.formatEther(await wallet2.getBalance())} ETH`) 62 | // ii. 构造交易请求,参数:to为接收地址,value为ETH数额 63 | const tx = { 64 | to: address1, 65 | value: ethers.utils.parseEther("0.001") 66 | } 67 | // iii. 发送交易,获得收据 68 | console.log(`\nii. 等待交易在区块链确认(需要几分钟)`) 69 | const receipt = await wallet2.sendTransaction(tx) 70 | await receipt.wait() // 等待链上确认交易 71 | console.log(receipt) // 打印交易详情 72 | // iv. 打印交易后余额 73 | console.log(`\niii. 发送后余额`) 74 | console.log(`钱包1: ${ethers.utils.formatEther(await wallet1WithProvider.getBalance())} ETH`) 75 | console.log(`钱包2: ${ethers.utils.formatEther(await wallet2.getBalance())} ETH`) 76 | } 77 | 78 | main() -------------------------------------------------------------------------------- /public/truffle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Truffle 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /course-5.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an example referring to https://raw.githubusercontent.com/WTFAcademy/WTF-Ethers/main/05_WriteContract/WriteContract.js 3 | */ 4 | 5 | // 声明只可写合约的规则: 6 | // const contract = new ethers.Contract(address, abi, signer); 7 | // 参数分别为合约地址`address`,合约ABI `abi`,Signer变量`signer` 8 | 9 | import { ethers } from "ethers"; 10 | // playcode免费版不能安装ethers,用这条命令,需要从网络上import包(把上面这行注释掉) 11 | // import { ethers } from "https://cdn-cors.ethers.io/lib/ethers-5.6.9.esm.min.js"; 12 | 13 | // 利用Alchemy的rpc节点连接以太坊网络 14 | // 准备 alchemy API 可以参考https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL04_Alchemy/readme.md 15 | // const ALCHEMY_GOERLI_URL = 'https://eth-goerli.alchemyapi.io/v2/GlaeWuylnNM3uuOo-SAwJxuwTdqHaY5l'; 16 | 17 | // 本代码在WTF的原始示例代码的基础上替换为如下Infura测试网endpoint 18 | const INFURA_GOERLI_URL = `https://goerli.infura.io/v3/${process.env.INFURA_API_KEY}` 19 | const provider = new ethers.providers.JsonRpcProvider(INFURA_GOERLI_URL) 20 | 21 | // 利用环境变量中的私钥和provider创建wallet对象 22 | const privateKey = process.env.WALLET_PRIVATE_KEY 23 | const wallet = new ethers.Wallet(privateKey, provider) 24 | 25 | // WETH的ABI 26 | const abiWETH = [ 27 | "function balanceOf(address) public view returns(uint)", 28 | "function deposit() public payable", 29 | "function transfer(address, uint) public returns (bool)", 30 | "function withdraw(uint) public ", 31 | ]; 32 | // WETH合约地址(Goerli测试网) 33 | const addressWETH = '0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6' // WETH Contract 34 | 35 | // 声明可写合约 36 | const contractWETH = new ethers.Contract(addressWETH, abiWETH, wallet) 37 | // 也可以声明一个只读合约,再用connect(wallet)函数转换成可写合约。 38 | // const contractWETH = new ethers.Contract(addressWETH, abiWETH, provider) 39 | // contractWETH.connect(wallet) 40 | 41 | const main = async () => { 42 | 43 | const address = await wallet.getAddress() 44 | // 1. 读取WETH合约的链上信息(WETH abi) 45 | console.log("\n1. 读取WETH余额") 46 | const balanceWETH = await contractWETH.balanceOf(address) 47 | console.log(`存款前WETH持仓: ${ethers.utils.formatEther(balanceWETH)}\n`) 48 | //读取钱包内ETH余额 49 | const balanceETH = await wallet.getBalance() 50 | 51 | // 如果钱包ETH足够 52 | if(ethers.utils.formatEther(balanceETH) > 0.0015){ 53 | 54 | // 2. 调用desposit()函数,将0.001 ETH转为WETH 55 | console.log("\n2. 调用desposit()函数,存入0.001 ETH") 56 | // 发起交易 57 | const tx = await contractWETH.deposit({value: ethers.utils.parseEther("0.001")}) 58 | // 等待交易上链 59 | await tx.wait() 60 | console.log(`交易详情:`) 61 | console.log(tx) 62 | const balanceWETH_deposit = await contractWETH.balanceOf(address) 63 | console.log(`存款后WETH持仓原始值: ${balanceWETH_deposit}\n`) 64 | console.log(`存款后WETH持仓: ${ethers.utils.formatEther(balanceWETH_deposit)}\n`) 65 | 66 | // 3. 调用transfer()函数,将0.001 WETH转账给 vitalik 67 | console.log("\n3. 调用transfer()函数,给vitalik转账0.001 WETH") 68 | // 发起交易 69 | const tx2 = await contractWETH.transfer("vitalik.eth", ethers.utils.parseEther("0.001")) 70 | // 等待交易上链 71 | await tx2.wait() 72 | const balanceWETH_transfer = await contractWETH.balanceOf(address) 73 | console.log(`转账后WETH持仓: ${ethers.utils.formatEther(balanceWETH_transfer)}\n`) 74 | 75 | }else{ 76 | // 如果ETH不足 77 | console.log("ETH不足,去水龙头领一些Goerli ETH") 78 | console.log("1. chainlink水龙头: https://faucets.chain.link/goerli") 79 | console.log("2. paradigm水龙头: https://faucet.paradigm.xyz/") 80 | } 81 | } 82 | 83 | main() 84 | -------------------------------------------------------------------------------- /course-18.mjs: -------------------------------------------------------------------------------- 1 | // 通过签名分发NFT白名单流程: 2 | // 3 | // 在服务器保管signer钱包的私钥-公钥对 4 | // -> 在服务器记录allowlist(白名单地址)和tokenId,并生成对应的msgHash, 5 | // -> 用signer钱包给msgHash签名 6 | // -> 部署NFT合约,初始化时signer的公钥保存在合约中。 7 | // -> 用户mint时填地址和tokenId,并向服务器请求签名。 8 | // -> 调用合约的mint()函数进行铸造 9 | 10 | import { ethers, utils } from "ethers"; 11 | import * as contractJson from "./ecdsa-signature.json" assert {type: "json"}; 12 | 13 | // 1. 创建provider和wallet 14 | // 准备 alchemy API 可以参考https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL04_Alchemy/readme.md 15 | // const ALCHEMY_GOERLI_URL = 'https://eth-goerli.alchemyapi.io/v2/GlaeWuylnNM3uuOo-SAwJxuwTdqHaY5l'; 16 | // const provider = new ethers.providers.JsonRpcProvider(ALCHEMY_GOERLI_URL); 17 | 18 | const INFURA_SEPOLIA_URL = `https://sepolia.infura.io/v3/${process.env.INFURA_API_KEY}` 19 | const provider = new ethers.providers.JsonRpcProvider(INFURA_SEPOLIA_URL); 20 | // 利用私钥和provider创建wallet对象 21 | // const privateKey = '0x227dbb8586117d55284e26620bc76534dfbd2394be34cf4a09cb775d593b6f2b' 22 | const privateKey = process.env.WALLET_PRIVATE_KEY 23 | const wallet = new ethers.Wallet(privateKey, provider) 24 | 25 | // 2. 根据allowlist地址和tokenId生成msgHash,并签名 26 | console.log("\n1. 生成签名") 27 | // 创建消息 28 | const account = "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4" 29 | const tokenId = "0" 30 | // 等效于Solidity中的keccak256(abi.encodePacked(account, tokenId)) 31 | const msgHash = utils.solidityKeccak256( 32 | ['address', 'uint256'], 33 | [account, tokenId]) 34 | console.log(`msgHash:${msgHash}`) 35 | 36 | const main = async () => { 37 | // 签名 38 | const messageHashBytes = ethers.utils.arrayify(msgHash) 39 | const signature = await wallet.signMessage(messageHashBytes); 40 | console.log(`签名:${signature}`) 41 | 42 | // 3. 创建合约工厂 43 | // NFT的人类可读abi 44 | const abiNFT = [ 45 | "constructor(string memory _name, string memory _symbol, address _signer)", 46 | "function name() view returns (string)", 47 | "function symbol() view returns (string)", 48 | "function mint(address _account, uint256 _tokenId, bytes memory _signature) external", 49 | "function ownerOf(uint256) view returns (address)", 50 | "function balanceOf(address) view returns (uint256)", 51 | ]; 52 | // 合约字节码,在remix中,你可以在两个地方找到Bytecode 53 | // i. 部署面板的Bytecode按钮 54 | // ii. 文件面板artifact文件夹下与合约同名的json文件中 55 | // 里面"object"字段对应的数据就是Bytecode,挺长的,608060起始 56 | // "object": "608060405260646000553480156100... 57 | const bytecodeNFT = contractJson.default.object; 58 | const factoryNFT = new ethers.ContractFactory(abiNFT, bytecodeNFT, wallet); 59 | 60 | // 读取钱包内ETH余额 61 | const balanceETH = await wallet.getBalance() 62 | 63 | // 如果钱包ETH足够 64 | if(ethers.utils.formatEther(balanceETH) > 0.002){ 65 | // 4. 利用contractFactory部署NFT合约 66 | console.log("\n2. 利用contractFactory部署NFT合约") 67 | // 部署合约,填入constructor的参数 68 | const contractNFT = await factoryNFT.deploy("WTF Signature", "WTF", wallet.address) 69 | console.log(`合约地址: ${contractNFT.address}`); 70 | // console.log("部署合约的交易详情") 71 | // console.log(contractNFT.deployTransaction) 72 | console.log("等待合约部署上链") 73 | await contractNFT.deployed() 74 | // 也可以用 contractNFT.deployTransaction.wait() 75 | console.log("合约已上链") 76 | 77 | // 5. 调用mint()函数,利用签名验证白名单,给account地址铸造NFT 78 | console.log("\n3. 调用mint()函数,利用签名验证白名单,给第一个地址铸造NFT") 79 | console.log(`NFT名称: ${await contractNFT.name()}`) 80 | console.log(`NFT代号: ${await contractNFT.symbol()}`) 81 | let tx = await contractNFT.mint(account, "1", signature) 82 | console.log("铸造中,等待交易上链") 83 | await tx.wait() 84 | console.log(`mint成功,地址${account} 的NFT余额: ${await contractNFT.balanceOf(account)}\n`) 85 | 86 | }else{ 87 | // 如果ETH不足 88 | console.log("ETH不足,去水龙头领一些Goerli ETH") 89 | console.log("1. chainlink水龙头: https://faucets.chain.link/goerli") 90 | console.log("2. paradigm水龙头: https://faucet.paradigm.xyz/") 91 | } 92 | } 93 | 94 | main() -------------------------------------------------------------------------------- /course-17.mjs: -------------------------------------------------------------------------------- 1 | import { ethers, utils } from "ethers"; 2 | import { MerkleTree } from "merkletreejs"; 3 | import * as contractJson from "./contract.json" assert {type: "json"}; 4 | 5 | // 1. 生成merkle tree 6 | console.log("\n1. 生成merkle tree") 7 | // 白名单地址 8 | const tokens = [ 9 | "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", 10 | "0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2", 11 | "0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db", 12 | "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB" 13 | ]; 14 | // leaf, merkletree, proof 15 | const leaf = tokens.map(x => utils.keccak256(x)) 16 | const merkletree = new MerkleTree(leaf, utils.keccak256, { sortPairs: true }); 17 | const proof = merkletree.getHexProof(leaf[0]); 18 | const root = merkletree.getHexRoot() 19 | console.log("Leaf:") 20 | console.log(leaf) 21 | console.log("\nMerkleTree:") 22 | console.log(merkletree.toString()) 23 | console.log("\nProof:") 24 | console.log(proof) 25 | console.log("\nRoot:") 26 | console.log(root) 27 | 28 | // 2. 创建provider和wallet 29 | // 准备 alchemy API 可以参考https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL04_Alchemy/readme.md 30 | // const ALCHEMY_GOERLI_URL = 'https://eth-goerli.alchemyapi.io/v2/GlaeWuylnNM3uuOo-SAwJxuwTdqHaY5l'; 31 | // const provider = new ethers.providers.JsonRpcProvider(ALCHEMY_GOERLI_URL); 32 | 33 | const INFURA_SEPOLIA_URL = `https://sepolia.infura.io/v3/${process.env.INFURA_API_KEY}` 34 | const provider = new ethers.providers.JsonRpcProvider(INFURA_SEPOLIA_URL); 35 | 36 | // 利用私钥和provider创建wallet对象 37 | // const privateKey = '0x227dbb8586117d55284e26620bc76534dfbd2394be34cf4a09cb775d593b6f2b' 38 | const privateKey = process.env.WALLET_PRIVATE_KEY 39 | const wallet = new ethers.Wallet(privateKey, provider) 40 | 41 | // 3. 创建合约工厂 42 | // NFT的人类可读abi 43 | const abiNFT = [ 44 | "constructor(string memory name, string memory symbol, bytes32 merkleroot)", 45 | "function name() view returns (string)", 46 | "function symbol() view returns (string)", 47 | "function mint(address account, uint256 tokenId, bytes32[] calldata proof) external", 48 | "function ownerOf(uint256) view returns (address)", 49 | "function balanceOf(address) view returns (uint256)", 50 | ]; 51 | // 合约字节码,在remix中,你可以在两个地方找到Bytecode 52 | // i. 部署面板的Bytecode按钮 53 | // ii. 文件面板artifact文件夹下与合约同名的json文件中 54 | // 里面"object"字段对应的数据就是Bytecode,挺长的,608060起始 55 | // "object": "608060405260646000553480156100... 56 | const bytecodeNFT = contractJson.default.object; 57 | const factoryNFT = new ethers.ContractFactory(abiNFT, bytecodeNFT, wallet); 58 | 59 | const main = async () => { 60 | // 读取钱包内ETH余额 61 | const balanceETH = await wallet.getBalance() 62 | 63 | console.log(`钱包余额: ${ethers.utils.formatEther(balanceETH)}`) 64 | // 如果钱包ETH足够 65 | if(ethers.utils.formatEther(balanceETH) > 0.002){ 66 | // 4. 利用contractFactory部署NFT合约 67 | console.log("\n2. 利用contractFactory部署NFT合约") 68 | // 部署合约,填入constructor的参数 69 | const contractNFT = await factoryNFT.deploy("WTF Merkle Tree", "WTF", root) 70 | console.log(`合约地址: ${contractNFT.address}`); 71 | // console.log("部署合约的交易详情") 72 | // console.log(contractNFT.deployTransaction) 73 | console.log("等待合约部署上链") 74 | await contractNFT.deployed() 75 | // 也可以用 contractNFT.deployTransaction.wait() 76 | console.log("合约已上链") 77 | 78 | // 5. 调用mint()函数,利用merkle tree验证白名单,给第0个地址铸造NFT 79 | console.log("\n3. 调用mint()函数,利用merkle tree验证白名单,给第一个地址铸造NFT") 80 | console.log(`NFT名称: ${await contractNFT.name()}`) 81 | console.log(`NFT代号: ${await contractNFT.symbol()}`) 82 | let tx = await contractNFT.mint(tokens[0], "0", proof) 83 | console.log("铸造中,等待交易上链") 84 | await tx.wait() 85 | console.log(`mint成功,地址${tokens[0]} 的NFT余额: ${await contractNFT.balanceOf(tokens[0])}\n`) 86 | 87 | }else{ 88 | // 如果ETH不足 89 | console.log("ETH不足,去水龙头领一些Goerli ETH") 90 | console.log("1. alchemy水龙头: https://goerlifaucet.com/") 91 | console.log("2. paradigm水龙头: https://faucet.paradigm.xyz/") 92 | } 93 | } 94 | 95 | main() 96 | -------------------------------------------------------------------------------- /course-16.mjs: -------------------------------------------------------------------------------- 1 | import { ethers, utils } from "ethers"; 2 | 3 | // 1. 创建provider和wallet,发送代币用 4 | // 准备 alchemy API 可以参考https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL04_Alchemy/readme.md 5 | // const ALCHEMY_GOERLI_URL = 'https://eth-goerli.alchemyapi.io/v2/GlaeWuylnNM3uuOo-SAwJxuwTdqHaY5l'; 6 | // const provider = new ethers.providers.JsonRpcProvider(ALCHEMY_GOERLI_URL); 7 | // 本代码在WTF的原始示例代码的基础上替换为如下Infura主网endpoint 8 | const INFURA_GOERLI_URL = `https://goerli.infura.io/v3/${process.env.INFURA_API_KEY}` 9 | const provider = new ethers.providers.JsonRpcProvider(INFURA_GOERLI_URL); 10 | // 利用私钥和provider创建wallet对象 11 | // const privateKey = '0x227dbb8586117d55284e26620bc76534dfbd2394be34cf4a09cb775d593b6f2b' 12 | const privateKey = process.env.WALLET_PRIVATE_KEY 13 | const wallet = new ethers.Wallet(privateKey, provider) 14 | 15 | // 2. 声明WETH合约 16 | // WETH的ABI 17 | const abiWETH = [ 18 | "function balanceOf(address) public view returns(uint)", 19 | "function transfer(address, uint) public returns (bool)", 20 | ]; 21 | // WETH合约地址(Goerli测试网) 22 | const addressWETH = '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6' // WETH Contract 23 | // 声明WETH合约 24 | const contractWETH = new ethers.Contract(addressWETH, abiWETH, wallet) 25 | 26 | // 3. 创建HD钱包 27 | console.log("\n1. 创建HD钱包") 28 | // 通过助记词生成HD钱包 29 | const mnemonic = `air organ twist rule prison symptom jazz cheap rather dizzy verb glare jeans orbit weapon universe require tired sing casino business anxiety seminar hunt` 30 | const hdNode = utils.HDNode.fromMnemonic(mnemonic) 31 | console.log(hdNode); 32 | 33 | // 4. 获得20个钱包 34 | console.log("\n2. 通过HD钱包派生20个钱包") 35 | const numWallet = 20 36 | // 派生路径:m / purpose' / coin_type' / account' / change / address_index 37 | // 我们只需要切换最后一位address_index,就可以从hdNode派生出新钱包 38 | let basePath = "m/44'/60'/0'/0"; 39 | let wallets = []; 40 | for (let i = 0; i < numWallet; i++) { 41 | let hdNodeNew = hdNode.derivePath(basePath + "/" + i); 42 | let walletNew = new ethers.Wallet(hdNodeNew.privateKey); 43 | wallets.push(walletNew); 44 | console.log(walletNew.address) 45 | } 46 | // 定义发送数额 47 | const amount = utils.parseEther("0.0001") 48 | console.log(`发送数额:${amount}`) 49 | 50 | 51 | const main = async () => { 52 | // 5. 读取一个地址的ETH和WETH余额 53 | console.log("\n3. 读取一个地址的ETH和WETH余额") 54 | //读取WETH余额 55 | const balanceWETH = await contractWETH.balanceOf(wallets[19].address) 56 | console.log(`WETH持仓: ${ethers.utils.formatEther(balanceWETH)}`) 57 | //读取ETH余额 58 | const balanceETH = await provider.getBalance(wallets[19].address) 59 | console.log(`ETH持仓: ${ethers.utils.formatEther(balanceETH)}\n`) 60 | 61 | // 如果钱包ETH足够 62 | if(ethers.utils.formatEther(balanceETH) > ethers.utils.formatEther(amount) && 63 | ethers.utils.formatEther(balanceWETH) >= ethers.utils.formatEther(amount)){ 64 | 65 | // 6. 批量归集钱包的ETH 66 | console.log("\n4. 批量归集20个钱包的ETH") 67 | const txSendETH = { 68 | to: wallet.address, 69 | value: amount 70 | } 71 | for (let i = 0; i < numWallet; i++) { 72 | // 将钱包连接到provider 73 | let walletiWithProvider = wallets[i].connect(provider) 74 | var tx = await walletiWithProvider.sendTransaction(txSendETH) 75 | console.log(`第 ${i+1} 个钱包 ${walletiWithProvider.address} ETH 归集开始`) 76 | } 77 | await tx.wait() 78 | console.log(`ETH 归集结束`) 79 | 80 | // 7. 批量归集钱包的WETH 81 | console.log("\n5. 批量归集20个钱包的WETH") 82 | for (let i = 0; i < numWallet; i++) { 83 | // 将钱包连接到provider 84 | let walletiWithProvider = wallets[i].connect(provider) 85 | // 将合约连接到新的钱包 86 | let contractConnected = contractWETH.connect(walletiWithProvider) 87 | var tx = await contractConnected.transfer(wallet.address, amount) 88 | console.log(`第 ${i+1} 个钱包 ${wallets[i].address} WETH 归集开始`) 89 | } 90 | await tx.wait() 91 | console.log(`WETH 归集结束`) 92 | 93 | // 8. 读取一个地址在归集后的ETH和WETH余额 94 | console.log("\n6. 读取一个地址在归集后的ETH和WETH余额") 95 | // 读取WETH余额 96 | const balanceWETHAfter = await contractWETH.balanceOf(wallets[19].address) 97 | console.log(`归集后WETH持仓: ${ethers.utils.formatEther(balanceWETHAfter)}`) 98 | // 读取ETH余额 99 | const balanceETHAfter = await provider.getBalance(wallets[19].address) 100 | console.log(`归集后ETH持仓: ${ethers.utils.formatEther(balanceETHAfter)}\n`) 101 | } 102 | } 103 | 104 | main() 105 | -------------------------------------------------------------------------------- /components/Wallet.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { useListen } from "../hooks/useListen"; 3 | import { useMetamask } from "../hooks/useMetamask"; 4 | import { Loading } from "./Loading"; 5 | import { ethers } from "ethers"; 6 | 7 | export default function Wallet() { 8 | const { 9 | dispatch, 10 | state: { status, isMetamaskInstalled, wallet, balance }, 11 | } = useMetamask(); 12 | const listen = useListen(); 13 | 14 | const showInstallMetamask = 15 | status !== "pageNotLoaded" && !isMetamaskInstalled; 16 | const showConnectButton = 17 | status !== "pageNotLoaded" && isMetamaskInstalled && !wallet; 18 | 19 | const isConnected = status !== "pageNotLoaded" && typeof wallet === "string"; 20 | 21 | const handleConnect = async () => { 22 | dispatch({ type: "loading" }); 23 | const accounts = await window.ethereum.request({ 24 | method: "eth_requestAccounts", 25 | }); 26 | 27 | if (accounts.length > 0) { 28 | const balance = await window.ethereum!.request({ 29 | method: "eth_getBalance", 30 | params: [accounts[0], "latest"], 31 | }); 32 | 33 | const provider = new ethers.providers.Web3Provider(window.ethereum); 34 | const signer = provider.getSigner(); 35 | console.log(signer); 36 | dispatch({ type: "connect", wallet: accounts[0], balance }); 37 | 38 | // we can register an event listener for changes to the users wallet 39 | listen(); 40 | } 41 | }; 42 | 43 | const handleDisconnect = () => { 44 | dispatch({ type: "disconnect" }); 45 | }; 46 | 47 | const handleAddUsdc = async () => { 48 | dispatch({ type: "loading" }); 49 | 50 | await window.ethereum.request({ 51 | method: "wallet_watchAsset", 52 | params: { 53 | type: "ERC20", 54 | options: { 55 | address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 56 | symbol: "USDC", 57 | decimals: 18, 58 | image: "https://cryptologos.cc/logos/usd-coin-usdc-logo.svg?v=023", 59 | }, 60 | }, 61 | }); 62 | dispatch({ type: "idle" }); 63 | }; 64 | 65 | return ( 66 |
67 |
68 |

69 | Metamask API intro 70 |

71 |

72 | Follow along with the{" "} 73 | 77 | Repo 78 | {" "} 79 | in order to learn how to use the Metamask API. 80 |

81 | 82 | {wallet && balance && ( 83 |
84 |
85 |
86 |
87 |
88 |

89 | Address: {wallet} 90 |

91 |

92 | Balance:{" "} 93 | 94 | {(parseInt(balance) / 1000000000000000000).toFixed(4)}{" "} 95 | ETH 96 | 97 |

98 |
99 |
100 |
101 |
102 |
103 | )} 104 | 105 | {showConnectButton && ( 106 | 112 | )} 113 | 114 | {showInstallMetamask && ( 115 | 116 | 117 | Install Metamask 118 | 119 | 120 | )} 121 | 122 | {isConnected && ( 123 |
124 | 130 | 136 |
137 | )} 138 |
139 |
140 | ); 141 | } 142 | -------------------------------------------------------------------------------- /course-15.mjs: -------------------------------------------------------------------------------- 1 | import { ethers, utils } from "ethers"; 2 | import { exit } from "process"; 3 | 4 | // 1. 创建HD钱包 5 | console.log("\n1. 创建HD钱包") 6 | // 通过助记词生成HD钱包 7 | const mnemonic = `air organ twist rule prison symptom jazz cheap rather dizzy verb glare jeans orbit weapon universe require tired sing casino business anxiety seminar hunt` 8 | const hdNode = utils.HDNode.fromMnemonic(mnemonic) 9 | console.log(hdNode); 10 | 11 | // 2. 获得20个钱包的地址 12 | console.log("\n2. 通过HD钱包派生20个钱包") 13 | const numWallet = 20 14 | // 派生路径:m / purpose' / coin_type' / account' / change / address_index 15 | // 我们只需要切换最后一位address_index,就可以从hdNode派生出新钱包 16 | let basePath = "m/44'/60'/0'/0"; 17 | let addresses = []; 18 | for (let i = 0; i < numWallet; i++) { 19 | let hdNodeNew = hdNode.derivePath(basePath + "/" + i); 20 | let walletNew = new ethers.Wallet(hdNodeNew.privateKey); 21 | addresses.push(walletNew.address); 22 | } 23 | console.log(addresses) 24 | const amounts = Array(20).fill(utils.parseEther("0.0001")) 25 | console.log(`发送数额:${amounts}`) 26 | 27 | // 3. 创建provider和wallet,发送代币用 28 | //准备 alchemy API 可以参考https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL04_Alchemy/readme.md 29 | // const ALCHEMY_GOERLI_URL = 'https://eth-goerli.alchemyapi.io/v2/GlaeWuylnNM3uuOo-SAwJxuwTdqHaY5l'; 30 | // const provider = new ethers.providers.JsonRpcProvider(ALCHEMY_GOERLI_URL); 31 | 32 | // 本代码在WTF的原始示例代码的基础上替换为如下Infura主网endpoint 33 | const INFURA_GOERLI_URL = `https://goerli.infura.io/v3/${process.env.INFURA_API_KEY}` 34 | const provider = new ethers.providers.JsonRpcProvider(INFURA_GOERLI_URL); 35 | 36 | // 利用私钥和provider创建wallet对象 37 | // 如果这个钱包没goerli测试网ETH了,去水龙头领一些,钱包地址: 0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2 38 | // const privateKey = '0x227dbb8586117d55284e26620bc76534dfbd2394be34cf4a09cb775d593b6f2b' 39 | const privateKey = process.env.WALLET_PRIVATE_KEY 40 | const wallet = new ethers.Wallet(privateKey, provider) 41 | 42 | // 4. 声明Airdrop合约 43 | // Airdrop的ABI 44 | const abiAirdrop = [ 45 | "function multiTransferToken(address,address[],uint256[]) external", 46 | "function multiTransferETH(address[],uint256[]) public payable", 47 | ]; 48 | // Airdrop合约地址(Goerli测试网) 49 | const addressAirdrop = '0x71C2aD976210264ff0468d43b198FD69772A25fa' // Airdrop Contract 50 | 51 | // Airdrop合约地址(Sepolia测试网) 52 | // ERC20 WTFA (Sepolia) contract address 0x0FF22707d8Bd9a5860391bea809292B0e621DFad 53 | // const addressAirdrop = '0x0D9fEce066EbFA7e71c82E7FFa542861509a603A' // Airdrop Contract 54 | 55 | // 声明Airdrop合约 56 | const contractAirdrop = new ethers.Contract(addressAirdrop, abiAirdrop, wallet) 57 | 58 | // 5. 声明WETH合约 59 | // WETH的ABI 60 | const abiWETH = [ 61 | "function balanceOf(address) public view returns(uint)", 62 | "function transfer(address, uint) public returns (bool)", 63 | "function approve(address, uint256) public returns (bool)" 64 | ]; 65 | // WETH合约地址(Goerli测试网) 66 | const addressWETH = '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6' // WETH Contract 67 | 68 | // WETH合约地址(Sepolia测试网) 69 | // const addressWETH = '0x389ecC727d6eE81777221143762B162Ae82D548e' // WETH Contract 70 | 71 | // 声明WETH合约 72 | const contractWETH = new ethers.Contract(addressWETH, abiWETH, wallet) 73 | 74 | 75 | const main = async () => { 76 | 77 | // 6. 读取一个地址的ETH和WETH余额 78 | console.log("\n3. 读取一个地址的ETH和WETH余额") 79 | //读取WETH余额 80 | const balanceWETH = await contractWETH.balanceOf(addresses[10]) 81 | console.log(`WETH持仓: ${ethers.utils.formatEther(balanceWETH)}\n`) 82 | //读取ETH余额 83 | const balanceETH = await provider.getBalance(addresses[10]) 84 | console.log(`ETH持仓: ${ethers.utils.formatEther(balanceETH)}\n`) 85 | 86 | const myETH = await wallet.getBalance() 87 | const myToken = await contractWETH.balanceOf(wallet.getAddress()) 88 | console.log(`My ETH: ${ethers.utils.formatEther(myETH)}`) 89 | console.log(`My Token: ${ethers.utils.formatEther(myToken)}`) 90 | 91 | // 如果钱包ETH足够和WETH足够 92 | if(ethers.utils.formatEther(myETH) > 0.002 && ethers.utils.formatEther(myToken) >= 0.002){ 93 | 94 | // 7. 调用multiTransferETH()函数,给每个钱包转 0.0001 ETH 95 | console.log("\n4. 调用multiTransferETH()函数,给每个钱包转 0.0001 ETH") 96 | // 发起交易 97 | const tx = await contractAirdrop.multiTransferETH(addresses, amounts, {value: ethers.utils.parseEther("0.002")}) 98 | // 等待交易上链 99 | await tx.wait() 100 | // console.log(`交易详情:`) 101 | // console.log(tx) 102 | const balanceETH2 = await provider.getBalance(addresses[10]) 103 | console.log(`发送后该钱包ETH持仓: ${ethers.utils.formatEther(balanceETH2)}\n`) 104 | 105 | // 8. 调用multiTransferToken()函数,给每个钱包转 0.0001 WETH 106 | console.log("\n5. 调用multiTransferToken()函数,给每个钱包转 0.0001 WETH") 107 | // 先approve WETH给Airdrop合约 108 | const txApprove = await contractWETH.approve(addressAirdrop, utils.parseEther("1")) 109 | await txApprove.wait() 110 | // 发起交易 111 | const tx2 = await contractAirdrop.multiTransferToken(addressWETH, addresses, amounts) 112 | // 等待交易上链 113 | await tx2.wait() 114 | // console.log(`交易详情:`) 115 | // console.log(tx2) 116 | // 读取WETH余额 117 | const balanceWETH2 = await contractWETH.balanceOf(addresses[10]) 118 | console.log(`发送后该钱包WETH持仓: ${ethers.utils.formatEther(balanceWETH2)}\n`) 119 | 120 | }else{ 121 | // 如果ETH和WETH不足 122 | console.log("ETH不足,去水龙头领一些Goerli ETH,并兑换一些WETH") 123 | console.log("1. chainlink水龙头: https://faucets.chain.link/goerli") 124 | console.log("2. paradigm水龙头: https://faucet.paradigm.xyz/") 125 | } 126 | } 127 | 128 | main() 129 | -------------------------------------------------------------------------------- /course-3.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an example referring to https://github.com/WTFAcademy/WTF-Ethers/tree/main/03_ReadContract 3 | * with the following changes: 4 | * 1. An extra showcase on how to fetch WTF Academy contract information 5 | * 2. Changed to Infura endpoint for JSON RPC provider. 6 | */ 7 | 8 | // 声明只读合约的规则: 9 | // 参数分别为合约地址`address`,合约ABI `abi`,Provider变量`provider` 10 | // const contract = new ethers.Contract(`address`, `abi`, `provider`); 11 | 12 | import { ethers } from "ethers"; 13 | // playcode免费版不能安装ethers,用这条命令,需要从网络上import包(把上面这行注释掉) 14 | // import { ethers } from "https://cdn-cors.ethers.io/lib/ethers-5.6.9.esm.min.js"; 15 | 16 | // 利用Alchemy的rpc节点连接以太坊网络 17 | // 准备 alchemy API 可以参考https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL04_Alchemy/readme.md 18 | // const ALCHEMY_MAINNET_URL = 'https://eth-mainnet.g.alchemy.com/v2/oKmOQKbneVkxgHZfibs-iFhIlIAl6HDN'; 19 | const INFURA_MAINNET_URL = `https://mainnet.infura.io/v3/${process.env.INFURA_API_KEY}`; 20 | const INFURA_GOERLI_URL = `https://goerli.infura.io/v3/${process.env.INFURA_API_KEY}`; 21 | const provider = new ethers.providers.JsonRpcProvider(INFURA_MAINNET_URL); 22 | 23 | // 第1种输入abi的方式: 复制abi全文 24 | // WETH的abi可以在这里复制:https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2#code 25 | const abiWETH = '[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"guy","type":"address"},{"name":"wad","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"src","type":"address"},{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"name":"wad","type":"uint256"}],"name":"withdraw","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"dst","type":"address"},{"name":"wad","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[],"name":"deposit","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"},{"name":"","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"guy","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":true,"name":"dst","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"dst","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"src","type":"address"},{"indexed":false,"name":"wad","type":"uint256"}],"name":"Withdrawal","type":"event"}]'; 26 | const addressWETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' // WETH Contract 27 | const contractWETH = new ethers.Contract(addressWETH, abiWETH, provider) 28 | 29 | // 第2种输入abi的方式:输入程序需要用到的函数,逗号分隔,ethers会自动帮你转换成相应的abi 30 | // 人类可读abi,以ERC20合约为例 31 | const abiERC20 = [ 32 | "function name() view returns (string)", 33 | "function symbol() view returns (string)", 34 | "function totalSupply() view returns (uint256)", 35 | "function balanceOf(address) view returns (uint)", 36 | ]; 37 | const addressDAI = '0x6B175474E89094C44Da98b954EedeAC495271d0F' // DAI Contract 38 | const contractDAI = new ethers.Contract(addressDAI, abiERC20, provider) 39 | 40 | const displayWTFContractInfo = async () => { 41 | const contractAddress = '0xdf9c19ceadf7e4a9db07a57fc0bfa246938e3bca'; 42 | const abi = [ 43 | "function name() view returns (string)", 44 | "function symbol() view returns (string)", 45 | "function balanceOf(address,uint256) external view returns (uint256)" 46 | ] 47 | const wtfContract = new ethers.Contract(contractAddress, abi, new ethers.providers.JsonRpcProvider(INFURA_GOERLI_URL)); 48 | const name = await wtfContract.name() 49 | const symbol = await wtfContract.symbol() 50 | const veryBigWoodsBalance = await wtfContract.balanceOf('0x0f46E7FF0Df5CFcE65d3aACf5a26268787aa8b79', 0) 51 | console.log("3. 读取WTF合约信息") 52 | console.log(`合约地址: ${contractAddress}`) 53 | console.log(`名称: ${name}`) 54 | console.log(`代号: ${symbol}`) 55 | const passed = veryBigWoodsBalance > 0 ? 'yes' : 'no' 56 | console.log(`VeryBigWoods Passed: ${passed}`) 57 | } 58 | 59 | const main = async () => { 60 | // 1. 读取WETH合约的链上信息(WETH abi) 61 | const nameWETH = await contractWETH.name() 62 | const symbolWETH = await contractWETH.symbol() 63 | const totalSupplyWETH = await contractWETH.totalSupply() 64 | console.log("1. 读取WETH合约信息") 65 | console.log(`合约地址: ${addressWETH}`) 66 | console.log(`名称: ${nameWETH}`) 67 | console.log(`代号: ${symbolWETH}`) 68 | console.log(`总供给: ${ethers.utils.formatEther(totalSupplyWETH)}`) 69 | const balanceWETH = await contractWETH.balanceOf('vitalik.eth') 70 | console.log(`Vitalik持仓: ${ethers.utils.formatEther(balanceWETH)}\n`) 71 | 72 | // 2. 读取DAI合约的链上信息(IERC20接口合约) 73 | const nameDAI = await contractDAI.name() 74 | const symbolDAI = await contractDAI.symbol() 75 | const totalSupplDAI = await contractDAI.totalSupply() 76 | console.log("2. 读取DAI合约信息") 77 | console.log(`合约地址: ${addressDAI}`) 78 | console.log(`名称: ${nameDAI}`) 79 | console.log(`代号: ${symbolDAI}`) 80 | console.log(`总供给: ${ethers.utils.formatEther(totalSupplDAI)}`) 81 | const balanceDAI = await contractDAI.balanceOf('vitalik.eth') 82 | console.log(`Vitalik持仓: ${ethers.utils.formatEther(balanceDAI)}\n`) 83 | 84 | displayWTFContractInfo() 85 | } 86 | 87 | main() 88 | -------------------------------------------------------------------------------- /course-6.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This is an example referring to https://github.com/WTFAcademy/WTF-Ethers/blob/main/06_DeployContract/DeployContract.js 3 | */ 4 | 5 | // 创建合约工厂实例的规则: 6 | // const contractFactory = new ethers.ContractFactory(abi, bytecode, signer); 7 | // 参数分别为合约ABI`abi`,合约字节码`bytecode`,Signer变量`signer` 8 | 9 | // 利用合约工厂部署合约: 10 | // contractFactory.deploy(args) 11 | // 其中args为合约构造函数的参数 12 | 13 | import { ethers } from "ethers"; 14 | 15 | // 利用Alchemy的rpc节点连接以太坊网络 16 | // 准备 alchemy API 可以参考https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL04_Alchemy/readme.md 17 | // const ALCHEMY_GOERLI_URL = 'https://eth-goerli.alchemyapi.io/v2/GlaeWuylnNM3uuOo-SAwJxuwTdqHaY5l'; 18 | // const provider = new ethers.providers.JsonRpcProvider(ALCHEMY_GOERLI_URL); 19 | 20 | // 本代码在WTF的原始示例代码的基础上替换为如下Infura测试网endpoint 21 | const INFURA_GOERLI_URL = `https://goerli.infura.io/v3/${process.env.INFURA_API_KEY}` 22 | const provider = new ethers.providers.JsonRpcProvider(INFURA_GOERLI_URL) 23 | 24 | // 利用私钥和provider创建wallet对象 25 | const privateKey = process.env.WALLET_PRIVATE_KEY 26 | const wallet = new ethers.Wallet(privateKey, provider) 27 | 28 | // ERC20的人类可读abi 29 | const abiERC20 = [ 30 | "constructor(string memory name_, string memory symbol_)", 31 | "function name() view returns (string)", 32 | "function symbol() view returns (string)", 33 | "function totalSupply() view returns (uint256)", 34 | "function balanceOf(address) view returns (uint)", 35 | "function transfer(address to, uint256 amount) external returns (bool)", 36 | "function mint(uint256 amount) external", 37 | ]; 38 | 39 | // 合约字节码,在remix中,你可以在两个地方找到Bytecode 40 | // 1. 部署面板的Bytecode按钮 41 | // 2. 文件面板artifact文件夹下与合约同名的json文件中 42 | // 里面"object"字段对应的数据就是Bytecode,挺长的,608060起始 43 | // "object": "608060405260646000553480156100... 44 | const bytecodeERC20 = "60806040526012600560006101000a81548160ff021916908360ff1602179055503480156200002d57600080fd5b5060405162001166380380620011668339818101604052810190620000539190620001bb565b81600390805190602001906200006b9291906200008d565b508060049080519060200190620000849291906200008d565b505050620003c4565b8280546200009b90620002d5565b90600052602060002090601f016020900481019282620000bf57600085556200010b565b82601f10620000da57805160ff19168380011785556200010b565b828001600101855582156200010b579182015b828111156200010a578251825591602001919060010190620000ed565b5b5090506200011a91906200011e565b5090565b5b80821115620001395760008160009055506001016200011f565b5090565b6000620001546200014e8462000269565b62000240565b905082815260208101848484011115620001735762000172620003a4565b5b620001808482856200029f565b509392505050565b600082601f830112620001a0576200019f6200039f565b5b8151620001b28482602086016200013d565b91505092915050565b60008060408385031215620001d557620001d4620003ae565b5b600083015167ffffffffffffffff811115620001f657620001f5620003a9565b5b620002048582860162000188565b925050602083015167ffffffffffffffff811115620002285762000227620003a9565b5b620002368582860162000188565b9150509250929050565b60006200024c6200025f565b90506200025a82826200030b565b919050565b6000604051905090565b600067ffffffffffffffff82111562000287576200028662000370565b5b6200029282620003b3565b9050602081019050919050565b60005b83811015620002bf578082015181840152602081019050620002a2565b83811115620002cf576000848401525b50505050565b60006002820490506001821680620002ee57607f821691505b6020821081141562000305576200030462000341565b5b50919050565b6200031682620003b3565b810181811067ffffffffffffffff8211171562000338576200033762000370565b5b80604052505050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b600080fd5b600080fd5b600080fd5b600080fd5b6000601f19601f8301169050919050565b610d9280620003d46000396000f3fe608060405234801561001057600080fd5b50600436106100a95760003560e01c806342966c681161007157806342966c681461016857806370a082311461018457806395d89b41146101b4578063a0712d68146101d2578063a9059cbb146101ee578063dd62ed3e1461021e576100a9565b806306fdde03146100ae578063095ea7b3146100cc57806318160ddd146100fc57806323b872dd1461011a578063313ce5671461014a575b600080fd5b6100b661024e565b6040516100c39190610b02565b60405180910390f35b6100e660048036038101906100e19190610a14565b6102dc565b6040516100f39190610ae7565b60405180910390f35b6101046103ce565b6040516101119190610b24565b60405180910390f35b610134600480360381019061012f91906109c1565b6103d4565b6040516101419190610ae7565b60405180910390f35b610152610583565b60405161015f9190610b3f565b60405180910390f35b610182600480360381019061017d9190610a54565b610596565b005b61019e60048036038101906101999190610954565b61066d565b6040516101ab9190610b24565b60405180910390f35b6101bc610685565b6040516101c99190610b02565b60405180910390f35b6101ec60048036038101906101e79190610a54565b610713565b005b61020860048036038101906102039190610a14565b6107ea565b6040516102159190610ae7565b60405180910390f35b61023860048036038101906102339190610981565b610905565b6040516102459190610b24565b60405180910390f35b6003805461025b90610c88565b80601f016020809104026020016040519081016040528092919081815260200182805461028790610c88565b80156102d45780601f106102a9576101008083540402835291602001916102d4565b820191906000526020600020905b8154815290600101906020018083116102b757829003601f168201915b505050505081565b600081600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040516103bc9190610b24565b60405180910390a36001905092915050565b60025481565b600081600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546104629190610bcc565b92505081905550816000808673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546104b79190610bcc565b92505081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825461050c9190610b76565b925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040516105709190610b24565b60405180910390a3600190509392505050565b600560009054906101000a900460ff1681565b806000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546105e49190610bcc565b9250508190555080600260008282546105fd9190610bcc565b92505081905550600073ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040516106629190610b24565b60405180910390a350565b60006020528060005260406000206000915090505481565b6004805461069290610c88565b80601f01602080910402602001604051908101604052809291908181526020018280546106be90610c88565b801561070b5780601f106106e05761010080835404028352916020019161070b565b820191906000526020600020905b8154815290600101906020018083116106ee57829003601f168201915b505050505081565b806000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546107619190610b76565b92505081905550806002600082825461077a9190610b76565b925050819055503373ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040516107df9190610b24565b60405180910390a350565b6000816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825461083a9190610bcc565b92505081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825461088f9190610b76565b925050819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040516108f39190610b24565b60405180910390a36001905092915050565b6001602052816000526040600020602052806000526040600020600091509150505481565b60008135905061093981610d2e565b92915050565b60008135905061094e81610d45565b92915050565b60006020828403121561096a57610969610d18565b5b60006109788482850161092a565b91505092915050565b6000806040838503121561099857610997610d18565b5b60006109a68582860161092a565b92505060206109b78582860161092a565b9150509250929050565b6000806000606084860312156109da576109d9610d18565b5b60006109e88682870161092a565b93505060206109f98682870161092a565b9250506040610a0a8682870161093f565b9150509250925092565b60008060408385031215610a2b57610a2a610d18565b5b6000610a398582860161092a565b9250506020610a4a8582860161093f565b9150509250929050565b600060208284031215610a6a57610a69610d18565b5b6000610a788482850161093f565b91505092915050565b610a8a81610c12565b82525050565b6000610a9b82610b5a565b610aa58185610b65565b9350610ab5818560208601610c55565b610abe81610d1d565b840191505092915050565b610ad281610c3e565b82525050565b610ae181610c48565b82525050565b6000602082019050610afc6000830184610a81565b92915050565b60006020820190508181036000830152610b1c8184610a90565b905092915050565b6000602082019050610b396000830184610ac9565b92915050565b6000602082019050610b546000830184610ad8565b92915050565b600081519050919050565b600082825260208201905092915050565b6000610b8182610c3e565b9150610b8c83610c3e565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03821115610bc157610bc0610cba565b5b828201905092915050565b6000610bd782610c3e565b9150610be283610c3e565b925082821015610bf557610bf4610cba565b5b828203905092915050565b6000610c0b82610c1e565b9050919050565b60008115159050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b600060ff82169050919050565b60005b83811015610c73578082015181840152602081019050610c58565b83811115610c82576000848401525b50505050565b60006002820490506001821680610ca057607f821691505b60208210811415610cb457610cb3610ce9565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600080fd5b6000601f19601f8301169050919050565b610d3781610c00565b8114610d4257600080fd5b50565b610d4e81610c3e565b8114610d5957600080fd5b5056fea2646970667358221220174a348da25abcf2ad113110f9e5a675093a6b3325908b8ab2fddb0cdb6aaab064736f6c63430008070033"; 45 | 46 | const factoryERC20 = new ethers.ContractFactory(abiERC20, bytecodeERC20, wallet); 47 | 48 | const main = async () => { 49 | // 读取钱包内ETH余额 50 | const balanceETH = await wallet.getBalance() 51 | 52 | // 如果钱包ETH足够 53 | if(ethers.utils.formatEther(balanceETH) > 0.002){ 54 | // 1. 利用contractFactory部署ERC20代币合约 55 | console.log("\n1. 利用contractFactory部署ERC20代币合约") 56 | // 部署合约,填入constructor的参数 57 | const contractERC20 = await factoryERC20.deploy("WTF Token 1", "WTF1") 58 | console.log(`合约地址: ${contractERC20.address}`); 59 | console.log("部署合约的交易详情") 60 | console.log(contractERC20.deployTransaction) 61 | console.log("\n等待合约部署上链") 62 | await contractERC20.deployed() 63 | // 也可以用 contractERC20.deployTransaction.wait() 64 | console.log("合约已上链") 65 | 66 | // 2. 打印合约的name()和symbol(),然后调用mint()函数,给自己地址mint 10,000代币 67 | console.log("\n2. 调用mint()函数,给自己地址mint 10,000代币") 68 | console.log(`合约名称: ${await contractERC20.name()}`) 69 | console.log(`合约代号: ${await contractERC20.symbol()}`) 70 | let tx = await contractERC20.mint("10000") 71 | console.log("等待交易上链") 72 | await tx.wait() 73 | console.log(`mint后地址中代币余额: ${await contractERC20.balanceOf(wallet.address)}`) 74 | console.log(`代币总供给: ${await contractERC20.totalSupply()}`) 75 | 76 | // 3. 调用transfer()函数,给V神转账1000代币 77 | console.log("\n3. 调用transfer()函数,给V神转账1,000代币") 78 | tx = await contractERC20.transfer("vitalik.eth", "1000") 79 | console.log("等待交易上链") 80 | await tx.wait() 81 | console.log(`V神钱包中的代币余额: ${await contractERC20.balanceOf("vitalik.eth")}`) 82 | 83 | }else{ 84 | // 如果ETH不足 85 | console.log("ETH不足,去水龙头领一些Goerli ETH") 86 | console.log("1. chainlink水龙头: https://faucets.chain.link/goerli") 87 | console.log("2. paradigm水龙头: https://faucet.paradigm.xyz/") 88 | } 89 | } 90 | 91 | main() -------------------------------------------------------------------------------- /instructions.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | In this edition of [Web3 Unleashed](https://trufflesuite.com/unleashed/), we're interviewing the MetaMask DevRel team [Gui Bibeau](https://twitter.com/guibibeau) and [Eric Bishard](https://twitter.com/httpjunkie) about the MetaMask API and other tools and features on the horizon like their MetaMask SDK. We'll will be building a dapp live with them to custom integrate MetaMask into a NextJS application! 4 | 5 | [![Web3 Unleashed Ep 7](https://i.ytimg.com/vi/vQVletnhLVk/maxresdefault.jpg)](http://www.youtube.com/watch?v=vQVletnhLVk "Web3 Unleashed Ep 7: Build a dapp with Next.js and the MetaMaskAPI") 6 | 7 | If you would like to follow along with the Web3 Unleashed Episode #7 demo during or after the stream, below are the steps we walked through to build our dapp integration with MetaMask API. 8 | 9 | ## Prerequisites 10 | 11 | - [MetaMask Extension](https://metamask.io/download/) 12 | - NodeJS 13 | - NPM 14 | - Git 15 | 16 | ## Clone and Install Dependencies 17 | 18 | Clone the [MetaMask API Web3 Unleashed repo](https://github.com/GuiBibeau/web3-unleashed-demo) and once in the directory we want to ensure we are on the main branch and run: 19 | 20 | ```bash 21 | npm i && npm run dev 22 | ``` 23 | 24 | This will give us a starting point in a NextJS application to build our demo. 25 | 26 | ### Connecting the User 27 | 28 | We will start by updating the `hooks/useMetamask.tsx` file. This is our global app context that utilizes "out of the box" Context API in React. 29 | 30 | Update `hooks/useMetamask.tsx` file: 31 | 32 | ```typescript 33 | import React, { useEffect, type PropsWithChildren } from "react"; 34 | 35 | type ConnectAction = { type: "connect"; wallet: string }; 36 | type DisconnectAction = { type: "disconnect" }; 37 | type PageLoadedAction = { type: "pageLoaded"; isMetamaskInstalled: boolean }; 38 | type LoadingAction = { type: "loading" }; 39 | 40 | type Action = 41 | | ConnectAction 42 | | DisconnectAction 43 | | PageLoadedAction 44 | | LoadingAction; 45 | 46 | type Dispatch = (action: Action) => void; 47 | 48 | type Status = "loading" | "idle" | "pageNotLoaded"; 49 | 50 | type State = { 51 | wallet: string | null; 52 | isMetamaskInstalled: boolean; 53 | status: Status; 54 | }; 55 | 56 | const MetamaskContext = React.createContext< 57 | { state: State; dispatch: Dispatch } | undefined 58 | >(undefined); 59 | 60 | const initialState: State = { 61 | wallet: null, 62 | isMetamaskInstalled: false, 63 | status: "loading", 64 | } as const; 65 | 66 | function metamaskReducer(state: State, action: Action): State { 67 | switch (action.type) { 68 | case "connect": { 69 | const { wallet } = action; 70 | return { ...state, wallet, status: "idle" }; 71 | } 72 | case "disconnect": { 73 | return { ...state, wallet: null }; 74 | } 75 | case "pageLoaded": { 76 | const { isMetamaskInstalled } = action; 77 | return { ...state, isMetamaskInstalled, status: "idle" }; 78 | } 79 | case "loading": { 80 | return { ...state, status: "loading" }; 81 | } 82 | default: { 83 | throw new Error("Unhandled action type"); 84 | } 85 | } 86 | } 87 | 88 | function MetamaskProvider({ children }: PropsWithChildren) { 89 | const [state, dispatch] = React.useReducer(metamaskReducer, initialState); 90 | const value = { state, dispatch }; 91 | 92 | useEffect(() => { 93 | if (typeof window !== undefined) { 94 | // start by checking if window.ethereum is present, indicating a wallet extension 95 | const ethereumProviderInjected = typeof window.ethereum !== "undefined"; 96 | // this could be other wallets so we can verify if we are dealing with metamask 97 | // using the boolean constructor to be explicit and not let this be used as a falsy value (optional) 98 | const isMetamaskInstalled = 99 | ethereumProviderInjected && Boolean(window.ethereum.isMetaMask); 100 | 101 | dispatch({ type: "pageLoaded", isMetamaskInstalled }); 102 | } 103 | }, []); 104 | 105 | return ( 106 | 107 | {children} 108 | 109 | ); 110 | } 111 | 112 | function useMetamask() { 113 | const context = React.useContext(MetamaskContext); 114 | if (context === undefined) { 115 | throw new Error("useMetamask must be used within a MetamaskProvider"); 116 | } 117 | return context; 118 | } 119 | 120 | export { MetamaskProvider, useMetamask }; 121 | ``` 122 | 123 | The above change is by far one of our largest changes that we will do at one time but this file is in charge of helping us keep our application state in sync with the wallet state and is crucial so that we can build the components and features that we want. 124 | 125 | After this change you will might notice red squiggly lines under the `window.ethereum` object, this is only because if we want TypeScript to stop yelling at us in our code editor, we need to tell it what `window.ethereum` is type-wise. 126 | 127 | Add the file `types.d.ts` to the app root: 128 | 129 | ```typescript 130 | type InjectedProviders = { 131 | isMetaMask?: true; 132 | }; 133 | 134 | interface Window { 135 | ethereum: InjectedProviders & { 136 | on: (...args: any[]) => void; 137 | removeListener?: (...args: any[]) => void; 138 | request(args: any): Promise; 139 | }; 140 | } 141 | ``` 142 | 143 | You should no longer see those warnings in your `hooks/useMetamask.tsx` file. 144 | 145 | Create a `components/Loading.tsx` file: 146 | 147 | ```typescript 148 | import { type FC } from "react"; 149 | 150 | const dot = `rounded-full h-2 w-2 mx-0.5 bg-current animate-[blink_1s_ease_0s_infinite_normal_both]"`; 151 | 152 | export const Loading: FC = () => { 153 | return ( 154 | 155 | 156 | 157 | 158 | 159 | ); 160 | }; 161 | ``` 162 | 163 | With our Type Definitions added, our Context Provider updated, and our `Loading.tsx` in place, we can now make changes to our `components/Wallet.tsx` file and add a loading state for our app. 164 | 165 | Update the `components/Wallet.tsx` file to: 166 | 167 | ```typescript 168 | import Image from "next/future/image"; 169 | import Link from "next/link"; 170 | import { useMetamask } from "../hooks/useMetamask"; 171 | import { Loading } from "./Loading"; 172 | 173 | export default function Wallet() { 174 | const { 175 | dispatch, 176 | state: { status, isMetamaskInstalled }, 177 | } = useMetamask(); 178 | 179 | const showInstallMetamask = status !== "pageNotLoaded" && !isMetamaskInstalled; 180 | const showConnectButton = status !== "pageNotLoaded" && isMetamaskInstalled; 181 | 182 | const handleConnect = async () => { 183 | dispatch({ type: "loading" }); 184 | const accounts = await window.ethereum.request({ 185 | method: "eth_requestAccounts", 186 | }); 187 | 188 | if (accounts.length > 0) { 189 | dispatch({ type: "connect", wallet: accounts[0] }); 190 | } 191 | }; 192 | 193 | return ( 194 |
195 |
196 |

197 | Metamask API intro 198 |

199 |

200 | Follow along with the{" "} 201 | 205 | Repo 206 | {" "} 207 | in order to learn how to use the Metamask API. 208 |

209 | {showConnectButton && ( 210 | 216 | )} 217 | 218 | {showInstallMetamask && ( 219 | 220 | 221 | Connect Wallet 222 | 223 | 224 | )} 225 |
226 |
227 | ); 228 | } 229 | ``` 230 | 231 | This imports the loading component, further destructures the return value of our `useMetaMask()` custom hook, sets up variables to track if MetaMask is installed or connected for conditional rendering and gives us a `handleConnect()` function for dispatching changes to our state reducer. 232 | 233 | If we are tracking our changes we can see that we have touched 4 files by creating or updating/refactoring. At this point we should be able to connect a user to our dapp with MetaMask. 234 | 235 | Run the project and attempt to connect to your MetaMask wallet. 236 | 237 | ```bash 238 | npm run dev 239 | ``` 240 | 241 | Two things are happening now: 242 | 243 | 1. If a user does not have MetaMask installed they will get a "Connect Wallet" button that simply takes you to download MetaMask. 244 | 2. If MetaMask is installed they will see a "Connect Wallet" button that actually connects their wallet to the dapp. 245 | 246 | > We are not yet hiding the button once connected or displaying any wallet information. As well, you will notice in MetaMask that you are connected to the dapp. To test the Install link you can go into your extension manager and disable MetaMask temporarily. 247 | 248 | [Checkout the Diff to see what changed](https://github.com/GuiBibeau/web3-unleashed-demo/pull/1/files) 249 | 250 | ### Use the MetaMask API to get User Info 251 | 252 | We want to display the a balance from our wallet, and the public address of the wallet account that is connected to our dapp. For this we need to make a few changes again to the `hooks/useMetamask.tsx` and add the logic and JSX/HTML in our `components/Wallet.tsx`. 253 | 254 | Update `hooks/useMetamask.tsx` to: 255 | 256 | ```typescript 257 | import React, { useEffect, type PropsWithChildren } from "react"; 258 | 259 | type ConnectAction = { type: "connect"; wallet: string; balance: string }; 260 | type DisconnectAction = { type: "disconnect" }; 261 | type PageLoadedAction = { type: "pageLoaded"; isMetamaskInstalled: boolean }; 262 | type LoadingAction = { type: "loading" }; 263 | 264 | type Action = 265 | | ConnectAction 266 | | DisconnectAction 267 | | PageLoadedAction 268 | | LoadingAction; 269 | 270 | type Dispatch = (action: Action) => void; 271 | 272 | type Status = "loading" | "idle" | "pageNotLoaded"; 273 | 274 | type State = { 275 | wallet: string | null; 276 | isMetamaskInstalled: boolean; 277 | status: Status; 278 | balance: string | null; 279 | }; 280 | 281 | const initialState: State = { 282 | wallet: null, 283 | isMetamaskInstalled: false, 284 | status: "loading", 285 | balance: null, 286 | } as const; 287 | 288 | function metamaskReducer(state: State, action: Action): State { 289 | switch (action.type) { 290 | case "connect": { 291 | const { wallet, balance } = action; 292 | return { ...state, wallet, balance, status: "idle" }; 293 | } 294 | case "disconnect": { 295 | return { ...state, wallet: null }; 296 | } 297 | case "pageLoaded": { 298 | const { isMetamaskInstalled } = action; 299 | return { ...state, isMetamaskInstalled, status: "idle" }; 300 | } 301 | case "loading": { 302 | return { ...state, status: "loading" }; 303 | } 304 | default: { 305 | throw new Error("Unhandled action type"); 306 | } 307 | } 308 | } 309 | 310 | const MetamaskContext = React.createContext< 311 | { state: State; dispatch: Dispatch } | undefined 312 | >(undefined); 313 | 314 | function MetamaskProvider({ children }: PropsWithChildren) { 315 | const [state, dispatch] = React.useReducer(metamaskReducer, initialState); 316 | const value = { state, dispatch }; 317 | 318 | useEffect(() => { 319 | if (typeof window !== undefined) { 320 | // start by checking if window.ethereum is present, indicating a wallet extension 321 | const ethereumProviderInjected = typeof window.ethereum !== "undefined"; 322 | // this could be other wallets so we can verify if we are dealing with metamask 323 | // using the boolean constructor to be explecit and not let this be used as a falsy value (optional) 324 | const isMetamaskInstalled = 325 | ethereumProviderInjected && Boolean(window.ethereum.isMetaMask); 326 | 327 | dispatch({ type: "pageLoaded", isMetamaskInstalled }); 328 | } 329 | }, []); 330 | 331 | return ( 332 | 333 | {children} 334 | 335 | ); 336 | } 337 | 338 | function useMetamask() { 339 | const context = React.useContext(MetamaskContext); 340 | if (context === undefined) { 341 | throw new Error("useMetamask must be used within a MetamaskProvider"); 342 | } 343 | return context; 344 | } 345 | 346 | export { MetamaskProvider, useMetamask }; 347 | ``` 348 | 349 | We have done some slight refactoring to account for the ability to track the state of the wallet balance, added **balance** to our **initialState**, and updated our **connect** action in our reducer 350 | 351 | Update `components/Wallet.tsx` to: 352 | 353 | ```typescript 354 | import Image from "next/future/image"; 355 | import Link from "next/link"; 356 | import { useMetamask } from "../hooks/useMetamask"; 357 | import { Loading } from "./Loading"; 358 | 359 | export default function Wallet() { 360 | const { 361 | dispatch, 362 | state: { status, isMetamaskInstalled, wallet, balance }, 363 | } = useMetamask(); 364 | 365 | const showInstallMetamask = status !== "pageNotLoaded" && !isMetamaskInstalled; 366 | const showConnectButton = status !== "pageNotLoaded" && isMetamaskInstalled && !wallet; 367 | 368 | const handleConnect = async () => { 369 | dispatch({ type: "loading" }); 370 | const accounts = await window.ethereum.request({ 371 | method: "eth_requestAccounts", 372 | }); 373 | 374 | if (accounts.length > 0) { 375 | const balance = await window.ethereum!.request({ 376 | method: "eth_getBalance", 377 | params: [accounts[0], "latest"], 378 | }); 379 | 380 | dispatch({ type: "connect", wallet: accounts[0], balance }); 381 | } 382 | }; 383 | 384 | return ( 385 |
386 |
387 |

388 | Metamask API intro 389 |

390 |

391 | Follow along with the{" "} 392 | 396 | Repo 397 | {" "} 398 | in order to learn how to use the Metamask API. 399 |

400 | 401 | {wallet && ( 402 |
403 |
404 |
405 |
406 |
407 |

408 | Address: {wallet} 409 |

410 |

411 | Balance: {balance} 412 |

413 |
414 |
415 |
416 |
417 |
418 | )} 419 | 420 | {showConnectButton && ( 421 | 427 | )} 428 | 429 | {showInstallMetamask && ( 430 | 431 | 432 | Connect Wallet 433 | 434 | 435 | )} 436 |
437 |
438 | ); 439 | } 440 | ``` 441 | 442 | As well we have added balance to our destructured object so that we have access to it in our component, updated the **showConnectButton** logic and requested the balance using the `eth_getBalance` MetaMask (RPC API) method. 443 | 444 | We have also updated our JSX/HTML to include an **address** and **balance**. 445 | 446 | This is a great start, but our UI is still lacking and there is more logic we need to properly track our wallet state and update the page because if we connect to our wallet we get a funny display for our balance and if we refresh our page, we don't see our address and balance. But we will now fix those issues. 447 | 448 | [Checkout the Diff to see what changed](https://github.com/GuiBibeau/web3-unleashed-demo/pull/2/files) 449 | 450 | ### Two Way Communication with MetaMask 451 | 452 | Again we will be updating the `hooks/useMetamask.tsx` and `components/Wallet.tsx` files. The idea will be to add a few more reducer actions including **Loading** and **Idle** states for the page, we will fix our button to say **"Install MetaMask"** instead of **"Connect MetaMask"** and we will parse the **balance** to display a readable number. 453 | 454 | Finally, we will add some code that uses the `wallet_watchAsset` MetaMask (RPC API) method to add **$USDC** token to our MetaMask wallet. This will enable our users to see those tokens in their wallet if they have them. If a dApp uses a particular token, we can programmatically do this for them rather than expecting to do it themselves manually through the MetMask UI. 455 | 456 | Update `hooks/useMetamask.tsx` to: 457 | 458 | ```typescript 459 | import React, { useEffect, type PropsWithChildren } from "react"; 460 | 461 | type ConnectAction = { type: "connect"; wallet: string; balance: string }; 462 | type DisconnectAction = { type: "disconnect" }; 463 | type PageLoadedAction = { type: "pageLoaded"; isMetamaskInstalled: boolean }; 464 | type LoadingAction = { type: "loading" }; 465 | type IdleAction = { type: "idle" }; 466 | 467 | type Action = 468 | | ConnectAction 469 | | DisconnectAction 470 | | PageLoadedAction 471 | | LoadingAction 472 | | IdleAction; 473 | 474 | type Dispatch = (action: Action) => void; 475 | 476 | type Status = "loading" | "idle" | "pageNotLoaded"; 477 | 478 | type State = { 479 | wallet: string | null; 480 | isMetamaskInstalled: boolean; 481 | status: Status; 482 | balance: string | null; 483 | }; 484 | 485 | const initialState: State = { 486 | wallet: null, 487 | isMetamaskInstalled: false, 488 | status: "loading", 489 | balance: null, 490 | } as const; 491 | 492 | function metamaskReducer(state: State, action: Action): State { 493 | switch (action.type) { 494 | case "connect": { 495 | const { wallet, balance } = action; 496 | return { ...state, wallet, balance, status: "idle" }; 497 | } 498 | case "disconnect": { 499 | return { ...state, wallet: null, balance: null }; 500 | } 501 | case "pageLoaded": { 502 | const { isMetamaskInstalled } = action; 503 | return { ...state, isMetamaskInstalled, status: "idle" }; 504 | } 505 | case "loading": { 506 | return { ...state, status: "loading" }; 507 | } 508 | case "idle": { 509 | return { ...state, status: "idle" }; 510 | } 511 | default: { 512 | throw new Error("Unhandled action type"); 513 | } 514 | } 515 | } 516 | 517 | const MetamaskContext = React.createContext< 518 | { state: State; dispatch: Dispatch } | undefined 519 | >(undefined); 520 | 521 | function MetamaskProvider({ children }: PropsWithChildren) { 522 | const [state, dispatch] = React.useReducer(metamaskReducer, initialState); 523 | const value = { state, dispatch }; 524 | 525 | useEffect(() => { 526 | if (typeof window !== undefined) { 527 | // start by checking if window.ethereum is present, indicating a wallet extension 528 | const ethereumProviderInjected = typeof window.ethereum !== "undefined"; 529 | // this could be other wallets so we can verify if we are dealing with metamask 530 | // using the boolean constructor to be explecit and not let this be used as a falsy value (optional) 531 | const isMetamaskInstalled = 532 | ethereumProviderInjected && Boolean(window.ethereum.isMetaMask); 533 | 534 | dispatch({ type: "pageLoaded", isMetamaskInstalled }); 535 | } 536 | }, []); 537 | 538 | return ( 539 | 540 | {children} 541 | 542 | ); 543 | } 544 | 545 | function useMetamask() { 546 | const context = React.useContext(MetamaskContext); 547 | if (context === undefined) { 548 | throw new Error("useMetamask must be used within a MetamaskProvider"); 549 | } 550 | return context; 551 | } 552 | 553 | export { MetamaskProvider, useMetamask }; 554 | ``` 555 | 556 | Update `components/Wallet.tsx` to: 557 | 558 | ```typescript 559 | import Link from "next/link"; 560 | import { useMetamask } from "../hooks/useMetamask"; 561 | import { Loading } from "./Loading"; 562 | 563 | export default function Wallet() { 564 | const { 565 | dispatch, 566 | state: { status, isMetamaskInstalled, wallet, balance }, 567 | } = useMetamask(); 568 | 569 | const showInstallMetamask = status !== "pageNotLoaded" && !isMetamaskInstalled; 570 | const showConnectButton = status !== "pageNotLoaded" && isMetamaskInstalled && !wallet; 571 | 572 | const showAddToken = status !== "pageNotLoaded" && typeof wallet === "string"; 573 | 574 | const handleConnect = async () => { 575 | dispatch({ type: "loading" }); 576 | const accounts = await window.ethereum.request({ 577 | method: "eth_requestAccounts", 578 | }); 579 | 580 | if (accounts.length > 0) { 581 | const balance = await window.ethereum!.request({ 582 | method: "eth_getBalance", 583 | params: [accounts[0], "latest"], 584 | }); 585 | 586 | dispatch({ type: "connect", wallet: accounts[0], balance }); 587 | 588 | // we can register an event listener for changes to the users wallet 589 | window.ethereum.on("accountsChanged", async (newAccounts: string[]) => { 590 | if (newAccounts.length > 0) { 591 | // uppon receiving a new wallet, we'll request again the balance to synchronize the UI. 592 | const newBalance = await window.ethereum!.request({ 593 | method: "eth_getBalance", 594 | params: [newAccounts[0], "latest"], 595 | }); 596 | 597 | dispatch({ 598 | type: "connect", 599 | wallet: newAccounts[0], 600 | balance: newBalance, 601 | }); 602 | } else { 603 | // if the length is 0, then the user has disconnected from the wallet UI 604 | dispatch({ type: "disconnect" }); 605 | } 606 | }); 607 | } 608 | }; 609 | 610 | const handleAddUsdc = async () => { 611 | dispatch({ type: "loading" }); 612 | 613 | await window.ethereum.request({ 614 | method: "wallet_watchAsset", 615 | params: { 616 | type: "ERC20", 617 | options: { 618 | address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 619 | symbol: "USDC", 620 | decimals: 18, 621 | image: "https://cryptologos.cc/logos/usd-coin-usdc-logo.svg?v=023", 622 | }, 623 | }, 624 | }); 625 | dispatch({ type: "idle" }); 626 | }; 627 | 628 | return ( 629 |
630 |
631 |

632 | Metamask API intro 633 |

634 |

635 | Follow along with the{" "} 636 | 640 | Repo 641 | {" "} 642 | in order to learn how to use the Metamask API. 643 |

644 | 645 | {wallet && balance && ( 646 |
647 |
648 |
649 |
650 |
651 |

652 | Address: {wallet} 653 |

654 |

655 | Balance:{" "} 656 | 657 | {(parseInt(balance) / 1000000000000000000).toFixed(4)}{" "} ETH 658 | 659 |

660 |
661 |
662 |
663 |
664 |
665 | )} 666 | 667 | {showConnectButton && ( 668 | 674 | )} 675 | 676 | {showInstallMetamask && ( 677 | 678 | 679 | Install Metamask 680 | 681 | 682 | )} 683 | 684 | {showAddToken && ( 685 | 691 | )} 692 |
693 |
694 | ); 695 | } 696 | ``` 697 | 698 | With those changes in place we can now install, connect to and view information from our MetaMask wallet. We can also see a nicely formatted version of our ETH balance and we can see **$USDC** tokens in our wallet. 699 | 700 | [Checkout the Diff to see what changed](https://github.com/GuiBibeau/web3-unleashed-demo/pull/3/files) 701 | 702 | We have one more UX improvement to push our dapp. 703 | 704 | ### More UX Goodies 705 | 706 | We'd like to store some MetaMask state in the browser's local storage to help us create a Disconnect button, something that we feel makes the UX better in a dapp. We will register an event listener for changes to the users wallet, so that when connecting and disconnecting the UX is just a little bit better. We will add a custom React Hook called `useListen` to help us achieve this and to co-locate some code that would otherwise be added in two different components so that our final code is a bit cleaner. We do a small refactor to get rid of a `useEffect` and we will display our buttons side by side when we have more than one showing on the page (Disconnect & Add Tokens) and we will use Tailwind's flex-box options to make this easy. 707 | 708 | 709 | Update `hooks/useMetamask.tsx` 710 | 711 | ```typescript 712 | import React, { useEffect, type PropsWithChildren } from "react"; 713 | 714 | type ConnectAction = { type: "connect"; wallet: string; balance: string }; 715 | type DisconnectAction = { type: "disconnect" }; 716 | type PageLoadedAction = { 717 | type: "pageLoaded"; 718 | isMetamaskInstalled: boolean; 719 | wallet: string | null; 720 | balance: string | null; 721 | }; 722 | type LoadingAction = { type: "loading" }; 723 | type IdleAction = { type: "idle" }; 724 | 725 | type Action = 726 | | ConnectAction 727 | | DisconnectAction 728 | | PageLoadedAction 729 | | LoadingAction 730 | | IdleAction; 731 | 732 | type Dispatch = (action: Action) => void; 733 | 734 | type Status = "loading" | "idle" | "pageNotLoaded"; 735 | 736 | type State = { 737 | wallet: string | null; 738 | isMetamaskInstalled: boolean; 739 | status: Status; 740 | balance: string | null; 741 | }; 742 | 743 | const initialState: State = { 744 | wallet: null, 745 | isMetamaskInstalled: false, 746 | status: "loading", 747 | balance: null, 748 | } as const; 749 | 750 | function metamaskReducer(state: State, action: Action): State { 751 | switch (action.type) { 752 | case "connect": { 753 | const { wallet, balance } = action; 754 | const newState = { ...state, wallet, balance, status: "idle" } as State; 755 | const info = JSON.stringify(newState); 756 | window.localStorage.setItem("metamaskState", info); 757 | 758 | return newState; 759 | } 760 | case "disconnect": { 761 | window.localStorage.removeItem("metamaskState"); 762 | return { ...state, wallet: null, balance: null }; 763 | } 764 | case "pageLoaded": { 765 | const { isMetamaskInstalled, balance, wallet } = action; 766 | return { ...state, isMetamaskInstalled, status: "idle", wallet, balance }; 767 | } 768 | case "loading": { 769 | return { ...state, status: "loading" }; 770 | } 771 | case "idle": { 772 | return { ...state, status: "idle" }; 773 | } 774 | default: { 775 | throw new Error("Unhandled action type"); 776 | } 777 | } 778 | } 779 | 780 | const MetamaskContext = React.createContext< 781 | { state: State; dispatch: Dispatch } | undefined 782 | >(undefined); 783 | 784 | function MetamaskProvider({ children }: PropsWithChildren) { 785 | const [state, dispatch] = React.useReducer(metamaskReducer, initialState); 786 | const value = { state, dispatch }; 787 | 788 | return ( 789 | 790 | {children} 791 | 792 | ); 793 | } 794 | 795 | function useMetamask() { 796 | const context = React.useContext(MetamaskContext); 797 | if (context === undefined) { 798 | throw new Error("useMetamask must be used within a MetamaskProvider"); 799 | } 800 | return context; 801 | } 802 | 803 | export { MetamaskProvider, useMetamask }; 804 | ``` 805 | 806 | Create `hooks/useListen.tsx` 807 | 808 | ```typescript 809 | import { useMetamask } from "./useMetamask"; 810 | 811 | export const useListen = () => { 812 | const { dispatch } = useMetamask(); 813 | 814 | return () => { 815 | window.ethereum.on("accountsChanged", async (newAccounts: string[]) => { 816 | if (newAccounts.length > 0) { 817 | // uppon receiving a new wallet, we'll request again the balance to synchronize the UI. 818 | const newBalance = await window.ethereum!.request({ 819 | method: "eth_getBalance", 820 | params: [newAccounts[0], "latest"], 821 | }); 822 | 823 | dispatch({ 824 | type: "connect", 825 | wallet: newAccounts[0], 826 | balance: newBalance, 827 | }); 828 | } else { 829 | // if the length is 0, then the user has disconnected from the wallet UI 830 | dispatch({ type: "disconnect" }); 831 | } 832 | }); 833 | }; 834 | }; 835 | ``` 836 | 837 | Update `components/Wallet.tsx` 838 | 839 | ```typescript 840 | import Link from "next/link"; 841 | import { useListen } from "../hooks/useListen"; 842 | import { useMetamask } from "../hooks/useMetamask"; 843 | import { Loading } from "./Loading"; 844 | 845 | export default function Wallet() { 846 | const { 847 | dispatch, 848 | state: { status, isMetamaskInstalled, wallet, balance }, 849 | } = useMetamask(); 850 | const listen = useListen(); 851 | 852 | const showInstallMetamask = status !== "pageNotLoaded" && !isMetamaskInstalled; 853 | const showConnectButton = status !== "pageNotLoaded" && isMetamaskInstalled && !wallet; 854 | 855 | const isConnected = status !== "pageNotLoaded" && typeof wallet === "string"; 856 | 857 | const handleConnect = async () => { 858 | dispatch({ type: "loading" }); 859 | const accounts = await window.ethereum.request({ 860 | method: "eth_requestAccounts", 861 | }); 862 | 863 | if (accounts.length > 0) { 864 | const balance = await window.ethereum!.request({ 865 | method: "eth_getBalance", 866 | params: [accounts[0], "latest"], 867 | }); 868 | 869 | dispatch({ type: "connect", wallet: accounts[0], balance }); 870 | 871 | // we can register an event listener for changes to the users wallet 872 | listen(); 873 | } 874 | }; 875 | 876 | const handleDisconnect = () => { 877 | dispatch({ type: "disconnect" }); 878 | }; 879 | 880 | const handleAddUsdc = async () => { 881 | dispatch({ type: "loading" }); 882 | 883 | await window.ethereum.request({ 884 | method: "wallet_watchAsset", 885 | params: { 886 | type: "ERC20", 887 | options: { 888 | address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 889 | symbol: "USDC", 890 | decimals: 18, 891 | image: "https://cryptologos.cc/logos/usd-coin-usdc-logo.svg?v=023", 892 | }, 893 | }, 894 | }); 895 | dispatch({ type: "idle" }); 896 | }; 897 | 898 | 899 | return ( 900 |
901 |
902 |

903 | Metamask API intro 904 |

905 |

906 | Follow along with the{" "} 907 | 911 | Repo 912 | {" "} 913 | in order to learn how to use the Metamask API. 914 |

915 | 916 | {wallet && balance && ( 917 |
918 |
919 |
920 |
921 |
922 |

923 | Address: {wallet} 924 |

925 |

926 | Balance:{" "} 927 | 928 | {(parseInt(balance) / 1000000000000000000).toFixed(4)}{" "} ETH 929 | 930 |

931 |
932 |
933 |
934 |
935 |
936 | )} 937 | 938 | {showConnectButton && ( 939 | 945 | )} 946 | 947 | {showInstallMetamask && ( 948 | 949 | 950 | Install Metamask 951 | 952 | 953 | )} 954 | 955 | {isConnected && ( 956 |
957 | 963 | 969 |
970 | )} 971 | 972 |
973 |
974 | ); 975 | } 976 | ``` 977 | 978 | Finally, we will update our `pages/index.tsx` file with a `useEffect` to wrap all of these final changes up. 979 | 980 | Update `pages/index/tsx` 981 | 982 | ```typescript 983 | import type { NextPage } from "next"; 984 | import { useEffect } from "react"; 985 | import Wallet from "../components/Wallet"; 986 | import { useListen } from "../hooks/useListen"; 987 | import { useMetamask } from "../hooks/useMetamask"; 988 | 989 | const Home: NextPage = () => { 990 | const { dispatch } = useMetamask(); 991 | const listen = useListen(); 992 | 993 | useEffect(() => { 994 | if (typeof window !== undefined) { 995 | // start by checking if window.ethereum is present, indicating a wallet extension 996 | const ethereumProviderInjected = typeof window.ethereum !== "undefined"; 997 | // this could be other wallets so we can verify if we are dealing with metamask 998 | // using the boolean constructor to be explecit and not let this be used as a falsy value (optional) 999 | const isMetamaskInstalled = 1000 | ethereumProviderInjected && Boolean(window.ethereum.isMetaMask); 1001 | 1002 | const local = window.localStorage.getItem("metamaskState"); 1003 | 1004 | // user was previously connected, start listening to MM 1005 | if (local) { 1006 | listen(); 1007 | } 1008 | 1009 | // local could be null if not present in LocalStorage 1010 | const { wallet, balance } = local 1011 | ? JSON.parse(local) 1012 | : // backup if local storage is empty 1013 | { wallet: null, balance: null }; 1014 | 1015 | dispatch({ type: "pageLoaded", isMetamaskInstalled, wallet, balance }); 1016 | } 1017 | }, []); 1018 | 1019 | return ( 1020 | <> 1021 | 1022 | 1023 | ); 1024 | }; 1025 | 1026 | export default Home; 1027 | ``` 1028 | 1029 | In this last page update to `pages/index.tsx` we have relocated the `useEffect` from the `hooks/useMetaMask.tsx` page for separation of concerns since the hook is consuming **dispatch** as a proper next step would to be creating a layout page with NextJS, but since we only have one page we simply added this code here. 1030 | 1031 | We have updated our `hooks/useMetamask.tsx` page's **PageLoadAction** to include **wallet** and **balance** as well as the code required to access our local storage and rehydrate our app. 1032 | 1033 | With those changes in place we have also updated our `components/Wallet.tsx` page to use our `useListen` hook since we are using that code in multiple places now, updated the `showAddToken` variable to a more descriptive name of `isConnected`, and added a `handleDisconnect()` function to dispatch an action clearing local storage in our browser. 1034 | 1035 | This also required a slight update to our JSX/HTML to display our buttons more neatly. 1036 | 1037 | [Checkout the Diff to see what changed](https://github.com/GuiBibeau/web3-unleashed-demo/pull/4/files) 1038 | 1039 | ## Remove Listeners after Disconnect 1040 | 1041 | We have one final change we want to make to ensure that we stop listening to changes once the user has disconnected their wallet. 1042 | 1043 | We will update the `` and `` files. This will make TypeScript definitions file aware of the `removeAllListeners()` method we will be using as well add necessary code to the `disconnect` case inside the `metamaskReducer`. 1044 | 1045 | Update `types.d.ts` file: 1046 | 1047 | ```typescript 1048 | type InjectedProviders = { 1049 | isMetaMask?: true; 1050 | }; 1051 | 1052 | interface Window { 1053 | ethereum: InjectedProviders & { 1054 | on: (...args: any[]) => void; 1055 | removeListener: (...args: any[]) => void; 1056 | removeAllListeners: (...args: any[]) => void; 1057 | request(args: any): Promise; 1058 | }; 1059 | } 1060 | ``` 1061 | 1062 | Update the case statement in the `useMetamask.tsx` file to: 1063 | 1064 | ```typescript 1065 | case "disconnect": { 1066 | window.localStorage.removeItem("metamaskState"); 1067 | if (typeof window.ethereum !== undefined) { 1068 | window.ethereum.removeAllListeners(["accountsChanged"]); 1069 | } 1070 | return { ...state, wallet: null, balance: null }; 1071 | } 1072 | ``` 1073 | 1074 | Again, here we have simply ensured that all listeners added after connecting the wallet stop listening once the user is disconnected. 1075 | 1076 | This concludes the demo! But you're just getting started, for a challenge, try updating the UI, try to add functionality to switch chains and overall have fun, if you have any questions or need help with MetaMask, reach out to our DevRel team on Twitter. You can contact [Gui Bibeau](https://twitter.com/guibibeau) and [Eric Bishard](https://twitter.com/httpjunkie) with any questions or feedback. 1077 | 1078 | > One final note, Gui has a great resource and blog called [web3-fullstack](https://www.web3-fullstack.com/) where he waxes poetically about Web3, full stack development and UX which is a great resource for Web2 developers getting into Web3 as well as seasoned veterans of the space! --------------------------------------------------------------------------------