├── .gitignore ├── 01_QuickStart ├── img │ ├── dev-success.png │ ├── init-next.png │ ├── next-init-page.png │ └── nft-card.png ├── readme.md └── web3.tsx ├── 02_ConnectWallet ├── img │ ├── connect.png │ ├── metamask.png │ └── metamask2.png ├── readme.md └── web3.tsx ├── 03_NodeService ├── img │ ├── faucet.png │ ├── sepoliaeth.png │ └── zan-service.png ├── readme.md └── web3.tsx ├── 04_CallContract ├── img │ ├── call-contract.png │ ├── call-read-contract.png │ ├── metamask.png │ ├── metamask3.png │ ├── no-gas.png │ └── zan-api-doc.png ├── readme.md └── web3.tsx ├── 05_Events ├── readme.md └── web3.tsx ├── 06_NextJS ├── img │ ├── createnew.png │ ├── deploy.png │ ├── domain.png │ ├── editroot.png │ ├── import.png │ └── nextbuild.png └── readme.md ├── 07_ContractDev ├── MyToken.sol ├── img │ ├── chai.png │ ├── create.png │ ├── createBtn.png │ ├── generate.png │ ├── initCode.png │ ├── mintable.png │ ├── more.png │ ├── remix.png │ ├── run.png │ ├── slide.png │ ├── unitTest.png │ └── unitTest1.png └── readme.md ├── 08_ContractDeploy ├── demo │ └── dapp.tsx ├── img │ ├── call-in-ide.png │ ├── changeNode.png │ ├── compile.png │ ├── connect1.png │ ├── connect2.png │ ├── copyABI.png │ ├── json.png │ ├── mint-test-net.png │ ├── sendTrans.png │ └── transInfo.png └── readme.md ├── 09_EIP1193 ├── img │ └── demo.png ├── readme.md └── web3.tsx ├── 10_WalletConnect ├── img │ ├── walletconnect.png │ ├── walletnetwork.png │ └── walletqrcode.png ├── readme.md └── web3.tsx ├── 11_MultipleChain ├── img │ └── image.png ├── readme.md └── web3.tsx ├── 12_Signature ├── img │ └── demo.png └── readme.md ├── 13_Payment ├── img │ └── send.png └── readme.md ├── 14_LocalDev ├── img │ ├── addnetwork.png │ ├── hardhat.png │ ├── initfiles.png │ ├── localnode.png │ └── minttest.png └── readme.md ├── 15_WagmiCli └── readme.md ├── CONTRIBUTING.md ├── LICENSE ├── P000_WhyDEX └── readme.md ├── P001_WhatIsDEX ├── img │ ├── AMM.png │ ├── binance.jpeg │ └── uniswap.png └── readme.md ├── P002_WhatIsUniswap ├── img │ ├── liquidity.webp │ ├── liquidityv3.webp │ ├── poolv3.png │ ├── tick.webp │ └── uniswapv3.jpg └── readme.md ├── P003_OverallDesign ├── img │ ├── add.png │ ├── pool.png │ ├── positions.png │ └── swap.png └── readme.md ├── P101_ContractsDesign ├── img │ └── uml.png └── readme.md ├── P102_InitContracts ├── code │ ├── Factory.sol │ ├── Pool.sol │ ├── PoolManager.sol │ ├── PositionManager.sol │ └── SwapRouter.sol ├── img │ └── deploy.png └── readme.md ├── P103_Factory ├── img │ └── factory.png └── readme.md ├── P104_PoolManager ├── img │ └── poolManager.png └── readme.md ├── P105_PoolLP └── readme.md ├── P106_PoolSwap └── readme.md ├── P107_PoolFee └── readme.md ├── P108_PositionManager └── readme.md ├── P109_SwapRouter └── readme.md ├── P201_InitFrontend ├── code │ ├── components │ │ └── WtfLayout │ │ │ ├── Header.tsx │ │ │ └── index.tsx │ └── pages │ │ └── wtfswap │ │ ├── index.tsx │ │ ├── pool.tsx │ │ └── positions.tsx ├── img │ └── initlayout.jpeg └── readme.md ├── P202_HeadUI ├── code │ ├── Header.tsx │ ├── index.tsx │ └── styles.module.css ├── img │ └── ui.png └── readme.md ├── P203_Connect ├── code │ ├── Header.tsx │ └── index.tsx └── readme.md ├── P204_SwapUI ├── code │ ├── index.tsx │ └── swap.module.css ├── img │ ├── swap1.png │ ├── swap2.png │ ├── swap3.png │ ├── swap4.png │ └── ui.png └── readme.md ├── P205_PoolListUI ├── code │ ├── pool.module.css │ └── pool.tsx ├── img │ ├── pool-1.png │ ├── pool-2.png │ ├── pool-3.png │ └── pool.png └── readme.md ├── P206_AddPoolUI ├── img │ ├── add.png │ └── create-pool.png └── readme.md ├── P207_PositionList ├── code │ ├── positions.module.css │ └── positions.tsx ├── img │ ├── position-1.png │ ├── position-2.png │ └── position.png └── readme.md ├── P208_AddPositionUI ├── img │ ├── add-position.png │ └── add.png └── readme.md ├── P209_DebugWithChain ├── img │ └── debugtoken.png └── readme.md ├── P210_DebugPool ├── img │ └── pool.png └── readme.md ├── P211_DebugPosition ├── img │ └── positions.png └── readme.md ├── P212_DebugSwap ├── img │ └── swap.png └── readme.md ├── P301_ContractOptimize ├── bugs │ ├── 001.md │ └── 002.md ├── img │ ├── aiscan.png │ ├── payable.png │ └── reentrancy.png └── readme.md ├── P302_Deploy ├── img │ ├── contract.png │ └── verify.png └── readme.md ├── README.md ├── T001_WtfswapDemo ├── img │ ├── faucet.png │ ├── swap.png │ └── testtoken.png └── readme.md ├── demo-contract ├── .gitignore ├── README.md ├── contracts │ ├── Lock.sol │ ├── MyToken.sol │ └── wtfswap │ │ ├── Factory.sol │ │ ├── Pool.sol │ │ ├── PoolManager.sol │ │ ├── PositionManager.sol │ │ ├── SwapRouter.sol │ │ ├── interfaces │ │ ├── IFactory.sol │ │ ├── IPool.sol │ │ ├── IPoolManager.sol │ │ ├── IPositionManager.sol │ │ └── ISwapRouter.sol │ │ ├── libraries │ │ ├── BitMath.sol │ │ ├── CustomRevert.sol │ │ ├── FixedPoint128.sol │ │ ├── FixedPoint96.sol │ │ ├── FullMath.sol │ │ ├── LiquidityAmounts.sol │ │ ├── LiquidityMath.sol │ │ ├── LowGasSafeMath.sol │ │ ├── SafeCast.sol │ │ ├── SqrtPriceMath.sol │ │ ├── SwapMath.sol │ │ ├── TickMath.sol │ │ ├── TransferHelper.sol │ │ └── UnsafeMath.sol │ │ └── test-contracts │ │ ├── DebugToken.sol │ │ ├── TestLP.sol │ │ ├── TestSwap.sol │ │ └── TestToken.sol ├── hardhat.config.ts ├── ignition │ └── modules │ │ ├── DebugToken.ts │ │ ├── Lock.ts │ │ ├── MyToken.ts │ │ └── Wtfswap.ts ├── package-lock.json ├── package.json ├── test │ ├── Lock.ts │ ├── MyToken.ts │ └── wtfswap │ │ ├── Factory.ts │ │ ├── Pool.ts │ │ ├── PoolManager.ts │ │ ├── PositionManager.ts │ │ └── SwapRouter.ts └── tsconfig.json └── demo ├── .eslintrc.json ├── README.md ├── components ├── AddPoolModal │ └── index.tsx ├── AddPositionModal │ └── index.tsx ├── Balance.tsx ├── Faucet.tsx ├── SendEth │ └── index.tsx ├── SignDemo │ └── index.tsx └── WtfLayout │ ├── Header.tsx │ ├── index.tsx │ └── styles.module.css ├── hooks └── useTokenAddress.tsx ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── api │ ├── hello.ts │ └── signatureCheck.ts ├── index.tsx ├── sign │ └── index.tsx ├── test.tsx ├── transaction │ └── index.tsx ├── web3.tsx └── wtfswap │ ├── debug.tsx │ ├── index.tsx │ ├── pool.module.css │ ├── pool.tsx │ ├── positions.module.css │ ├── positions.tsx │ └── swap.module.css ├── public ├── favicon.ico ├── next.svg ├── vercel.svg ├── wtf-antdweb3.png └── wtf.png ├── styles ├── Home.module.css └── globals.css ├── tsconfig.json ├── utils ├── common.ts └── contracts.ts └── wagmi.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .next 4 | next-env.d.ts 5 | .vscode -------------------------------------------------------------------------------- /01_QuickStart/img/dev-success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/01_QuickStart/img/dev-success.png -------------------------------------------------------------------------------- /01_QuickStart/img/init-next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/01_QuickStart/img/init-next.png -------------------------------------------------------------------------------- /01_QuickStart/img/next-init-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/01_QuickStart/img/next-init-page.png -------------------------------------------------------------------------------- /01_QuickStart/img/nft-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/01_QuickStart/img/nft-card.png -------------------------------------------------------------------------------- /01_QuickStart/web3.tsx: -------------------------------------------------------------------------------- 1 | import { http } from "wagmi"; 2 | import { Mainnet, WagmiWeb3ConfigProvider } from '@ant-design/web3-wagmi'; 3 | import { Address, NFTCard} from "@ant-design/web3"; 4 | 5 | export default function Web3() { 6 | return ( 7 | 13 |
14 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /02_ConnectWallet/img/connect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/02_ConnectWallet/img/connect.png -------------------------------------------------------------------------------- /02_ConnectWallet/img/metamask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/02_ConnectWallet/img/metamask.png -------------------------------------------------------------------------------- /02_ConnectWallet/img/metamask2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/02_ConnectWallet/img/metamask2.png -------------------------------------------------------------------------------- /02_ConnectWallet/readme.md: -------------------------------------------------------------------------------- 1 | 本节作者:[@愚指导](https://x.com/yudao1024)、[@小符](https://x.com/smallfu666)、[@simple](https://x.com/0xsimpleth) 2 | 3 | 连接钱包是 DApp 中最重要的交互,这一讲我们会引导你通过 [wagmi](https://wagmi.sh) 和 [Ant Design Web3](https://web3.ant.design) 来实现连接钱包的功能。 4 | 5 | ## DApp 如何连接钱包 6 | 7 | 在 DApp 中,我们需要连接钱包来获取用户的钱包地址,以及进行一些需要用户签名的操作,比如发送交易、签名消息等。连接钱包有多种方式,通常在以太坊中有如下三种方式: 8 | 9 | - 通过浏览器插件建立连接。 10 | - 通过在钱包 App 中访问 DApp 建立连接。 11 | - 通过 WalletConnect 协议建立连接。 12 | 13 | 其中前面两种对于 DApp 来说都是通过钱包注入到浏览器运行环境中的接口来实现的,而 WalletConnect 则是通过服务端中转的方式来实现的。而钱包注入接口也有两种方式,一种是通过 [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) 来实现的,另一种是通过 [EIP-6963](https://eips.ethereum.org/EIPS/eip-6963) 来实现的。EIP-1193 是一个早期的协议,也比较简单,接下来我们先尝试用这种方式来和钱包建立连接。 14 | 15 | ## 注册 MetaMask 钱包 16 | 17 | [MetaMask](https://metamask.io/) 是目前以太坊生态中用户最多的钱包插件,它提供了一个简单的方式,让用户在浏览器中管理自己的以太坊资产,同时也是 DApp 与以太坊网络交互的桥梁。如果你还没有使用过,可以在[这里下载](https://metamask.io/download/)安装,并参考官网的教程完成初始的配置。当然,你也可以使用其它钱包。比如 [TokenPocket](https://www.tokenpocket.pro/)、[imToken](https://token.im/) 等。 18 | 19 | 完成安装钱包后,可以看到如下的页面: 20 | 21 | ![](./img/metamask.png) 22 | 23 | ## 配置 MetaMask 钱包 24 | 25 | 我们以 [MetaMask](https://metamask.io/) 为例,看一下如何和 MetaMask 钱包建立连接。 26 | 27 | ```diff 28 | import { http } from "wagmi"; 29 | - import { Mainnet, WagmiWeb3ConfigProvider } from '@ant-design/web3-wagmi'; 30 | + import { Mainnet, WagmiWeb3ConfigProvider, MetaMask } from '@ant-design/web3-wagmi'; 31 | - import { Address, NFTCard } from "@ant-design/web3"; 32 | + import { Address, NFTCard, Connector, ConnectButton } from "@ant-design/web3"; 33 | 34 | export default function Web3() { 35 | return ( 36 | 43 |
44 | 48 | + 49 | + 50 | + 51 | 52 | ); 53 | }; 54 | ``` 55 | 56 | 其中引入的内容说明如下: 57 | 58 | - MetaMask:代表小狐狸钱包,Ant Design Web3 支持多款[钱包](https://github.com/ant-design/ant-design-web3/blob/main/packages/wagmi/src/wallets/index.ts),方便根据需要进行配置。 59 | - [Connector](https://web3.ant.design/components/connector-cn):连接器,Connector 提供了一个完整的连接钱包的 UI 。 60 | - [ConnectButton](https://web3.ant.design/components/connect-button-cn):连接区块链钱包的按钮,配合 `Connector` 组件一起使用。 61 | 62 | 这样就完成了连接钱包的功能,点击 Connect Wallet,可以看到如下的页面: 63 | 64 | ![](./img/connect.png) 65 | 66 | 连接完成后,可以看到如下的页面: 67 | 68 | ![](./img/metamask2.png) 69 | 70 | 71 | 使用 Ant Design Web3 提供的组件可以快速的实现 DApp 的基础功能,恭喜你,我们已经实现了连接钱包的功能。 72 | 73 | -------------------------------------------------------------------------------- /02_ConnectWallet/web3.tsx: -------------------------------------------------------------------------------- 1 | import { http } from "wagmi"; 2 | import { Mainnet, WagmiWeb3ConfigProvider, MetaMask } from '@ant-design/web3-wagmi'; 3 | import { Address, NFTCard, ConnectButton, Connector } from "@ant-design/web3"; 4 | 5 | export default function Web3() { 6 | return ( 7 | 14 |
15 | 19 | 20 | 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /03_NodeService/img/faucet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/03_NodeService/img/faucet.png -------------------------------------------------------------------------------- /03_NodeService/img/sepoliaeth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/03_NodeService/img/sepoliaeth.png -------------------------------------------------------------------------------- /03_NodeService/img/zan-service.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/03_NodeService/img/zan-service.png -------------------------------------------------------------------------------- /03_NodeService/readme.md: -------------------------------------------------------------------------------- 1 | 本节作者:[@愚指导](https://x.com/yudao1024)、[@小符](https://x.com/smallfu666) 2 | 3 | 节点服务是 DApp 开发必不可少的服务。这一讲,我们介绍节点服务的概念,并引导你在项目中配置好节点服务,以及通过水龙头准备好一些 Sepolia 测试网的 ETH。 4 | 5 | ## 什么是节点服务 6 | 7 | 节点服务是 DApp 开发必不可少的服务。它是一个运行在区块链网络上的服务,它可以帮助你与区块链网络进行交互。在 DApp 开发中,我们需要通过节点服务来获取区块链的数据,发送交易等。 8 | 9 | 在以太坊网络中,我们可以通过 [ZAN](https://zan.top?chInfo=wtf)、[Infura](https://infura.io/)、[Alchemy](https://www.alchemy.com/) 等服务商来获取节点服务。这些服务商都提供了免费的节点服务,当然,它们也提供了付费的服务,如果你的 DApp 需要更高的性能,你可以考虑使用它们的付费服务。 10 | 11 | ## 配置节点服务 12 | 13 | 这里以 [ZAN 的节点服务](https://zan.top/home/node-service?chInfo=wtf)为例,指引你如何配置节点服务。 14 | 15 | 首先注册并登录 [https://zan.top](https://zan.top?chInfo=wtf) 之后进入到节点服务的控制台 [https://zan.top/service/apikeys](https://zan.top/service/apikeys?chInfo=wtf) 创建一个 Key,每个 Key 都有默认的免费额度,对于微型项目来说够用了,但是对于生产环境的项目来说,请结合实际情况购买节点服务。 16 | 17 | 创建成功后你会看到如下的页面: 18 | 19 | ![](./img/zan-service.png) 20 | 21 | 选择以太坊主网的节点服务地址,并将复制的地址添加到 `WagmiWeb3ConfigProvider` 的 `http()` 方法中。如下: 22 | 23 | ```diff 24 | 31 | ``` 32 | 33 | 上面代码中的 `YourZANApiKey` 需要替换成你自己的 Key。另外在实际的项目中,为了避免你的 Key 被滥用,建议你将 Key 放到后端服务中,然后通过后端服务来调用节点服务,或者在 ZAN 的控制台中设置域名白名单来降低被滥用的风险。当然,在教程中你也可以继续直接使用 `http()`,使用 wagmi 内置的默认的实验性的节点服务。 34 | 35 | 同样,如果你使用的是 Infura 或者 Alchemy 的节点服务,你也可以将它们的节点服务地址添加到 `WagmiWeb3ConfigProvider` 的 `http()` 方法中。 36 | 37 | ## 从水龙头获取测试网 ETH 38 | 39 | 除了节点服务,用于测试的 ETH 也是开发中必不可少的部分。通常,我们可以通过水龙头服务来获取。水龙头(Faucet)是一种在线服务,用于提供免费的测试网加密货币(通常是小额的代币),用于在开发环境中进行测试。这些服务通常由测试网官方、开发者社区、节点服务技术供应商等提供。 40 | 41 | 比如你可以通过 [ZAN 的水龙头服务](https://zan.top/faucet?chInfo=wtf) 来获取一定量的 Sepolia 测试网 ETH 用于测试。 42 | 43 | ![faucet](./img/faucet.png) 44 | 45 | 请在上图示意的水龙头网页中填入钱包地址,领取适量的测试网 SepoliaETH,我们在后面的课程中可能会用到。 46 | 47 | 本次领取 0.01 SepoliaETH 48 | 49 | ![faucet](./img/sepoliaeth.png) 50 | 51 | -------------------------------------------------------------------------------- /03_NodeService/web3.tsx: -------------------------------------------------------------------------------- 1 | import { http } from "wagmi"; 2 | import { Mainnet, WagmiWeb3ConfigProvider } from '@ant-design/web3-wagmi'; 3 | import { Address, NFTCard} from "@ant-design/web3"; 4 | 5 | export default function Web3() { 6 | return ( 7 | 13 |
14 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /04_CallContract/img/call-contract.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/04_CallContract/img/call-contract.png -------------------------------------------------------------------------------- /04_CallContract/img/call-read-contract.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/04_CallContract/img/call-read-contract.png -------------------------------------------------------------------------------- /04_CallContract/img/metamask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/04_CallContract/img/metamask.png -------------------------------------------------------------------------------- /04_CallContract/img/metamask3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/04_CallContract/img/metamask3.png -------------------------------------------------------------------------------- /04_CallContract/img/no-gas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/04_CallContract/img/no-gas.png -------------------------------------------------------------------------------- /04_CallContract/img/zan-api-doc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/04_CallContract/img/zan-api-doc.png -------------------------------------------------------------------------------- /04_CallContract/web3.tsx: -------------------------------------------------------------------------------- 1 | import { parseEther } from "viem"; 2 | import { Button, message } from "antd"; 3 | import { http, useReadContract, useWriteContract } from "wagmi"; 4 | import { Mainnet, WagmiWeb3ConfigProvider, MetaMask } from '@ant-design/web3-wagmi'; 5 | import { Address, NFTCard, ConnectButton, Connector, useAccount } from "@ant-design/web3"; 6 | 7 | 8 | const CallTest = () => { 9 | const { account } = useAccount(); 10 | const result = useReadContract({ 11 | abi: [ 12 | { 13 | type: 'function', 14 | name: 'balanceOf', 15 | stateMutability: 'view', 16 | inputs: [{ name: 'account', type: 'address' }], 17 | outputs: [{ type: 'uint256' }], 18 | }, 19 | ], 20 | address: '0xEcd0D12E21805803f70de03B72B1C162dB0898d9', 21 | functionName: 'balanceOf', 22 | args: [account?.address as `0x${string}`], 23 | }); 24 | 25 | 26 | const { writeContract } = useWriteContract(); 27 | return ( 28 |
29 | {result.data?.toString()} 30 | 67 |
68 | ); 69 | }; 70 | 71 | export default function Web3() { 72 | return ( 73 | 80 |
81 | 85 | 86 | 87 | 88 | 89 | 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /05_Events/readme.md: -------------------------------------------------------------------------------- 1 | 本节作者:[@愚指导](https://x.com/yudao1024) 2 | 3 | 这一讲将会介绍如何在 DApp 中监听合约事件,实时更新 DApp 界面。 4 | 5 | ## 简介 6 | 7 | 区块链智能合约的事件和我们传统的应用开发理解的事件概念是有些不同的,区块链本身并没有一个消息机制来向应用发送事件。它本质上是 EVM 上日志的抽象。 8 | 9 | 这一类日志相比智能合约的状态变化,更加便宜,是一种比较经济数据存储方式,每个事件大概消耗 2000 gas,而链上存储一个新变量至少需要 20000 gas。所以在智能合约中,我们通常会使用事件来记录一些重要的状态变化。另外基于节点服务提供的 RPC 接口可以实现 DApp 前端页面监听合约事件,实现相对实时的更新。 10 | 11 | ## 如何在智能合约中添加事件 12 | 13 | 在智能合约中事件由 `event` 声明,以 `emit` 触发,下面是一个示例,这里只做简单展示,具体我们会在后面的合约开发课程中具体讲解: 14 | 15 | ```diff 16 | // SPDX-License-Identifier: MIT 17 | pragma solidity ^0.8.20; 18 | 19 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 20 | import "@openzeppelin/contracts/access/Ownable.sol"; 21 | 22 | contract MyToken is ERC721, Ownable { 23 | uint256 private _nextTokenId = 0; 24 | + event Minted(address minter, uint256 amount); 25 | constructor() 26 | ERC721("MyToken", "MTK") 27 | Ownable(msg.sender) 28 | {} 29 | function mint(uint256 quantity) public payable { 30 | require(quantity == 1, "quantity must be 1"); 31 | require(msg.value == 0.01 ether, "must pay 0.01 ether"); 32 | uint256 tokenId = _nextTokenId++; 33 | _mint(msg.sender, tokenId); 34 | + emit Minted(msg.sender, quantity); 35 | } 36 | } 37 | ``` 38 | 39 | ## 如何在 DApp 中监听事件 40 | 41 | 在 DApp 前端页面,我们可以通过调用节点服务提供的 RPC 接口来监听合约事件,这里我们继续使用 `wagmi` 来开发。 42 | 43 | 首先引入 [useWatchContractEvent](https://wagmi.sh/react/api/hooks/useWatchContractEvent#abi)。 44 | 45 | ```diff 46 | import { 47 | createConfig, 48 | http, 49 | useReadContract, 50 | useWriteContract, 51 | + useWatchContractEvent, 52 | } from "wagmi"; 53 | ``` 54 | 55 | 然后使用 `useWatchContractEvent` 监听合约事件。 56 | 57 | ```ts 58 | useWatchContractEvent({ 59 | address: "0xEcd0D12E21805803f70de03B72B1C162dB0898d9", 60 | abi: [ 61 | { 62 | anonymous: false, 63 | inputs: [ 64 | { 65 | indexed: false, 66 | internalType: "address", 67 | name: "minter", 68 | type: "address", 69 | }, 70 | { 71 | indexed: false, 72 | internalType: "uint256", 73 | name: "amount", 74 | type: "uint256", 75 | }, 76 | ], 77 | name: "Minted", 78 | type: "event", 79 | }, 80 | ], 81 | eventName: "Minted", 82 | onLogs() { 83 | message.success("new minted!"); 84 | }, 85 | }); 86 | ``` 87 | 88 | 具体的使用方法其实在类似 wagmi 这样的 SDK 的封装下和传统的前端开发并没有太大的区别,你传入合约地址和要监听的事件后就可以在 `onLogs` 回调中处理事件。 89 | 90 | 当然,上面代码只是为了演示。实际编码的时候你可能需要在 `onLogs` 中判断事件的具体内容,然后更新页面。 91 | 92 | 此外,`abi` 参数通常来说都是合约编译时候自动生成的,整体在项目中维护一份完整的内容传入即可,不需要像示例这样单独在每个调用的地方手动编写。 93 | 94 | 因为事件只有在合约被调用后才会触发,所以我们目前还无法调试,你可以继续课程,等到我们在测试环境可以部署一个测试合约后再来测试。 95 | 96 | 完整的代码可以参考 [web3.tsx](./web3.tsx)。 97 | -------------------------------------------------------------------------------- /05_Events/web3.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | ConnectButton, 4 | Connector, 5 | NFTCard, 6 | useAccount, 7 | } from "@ant-design/web3"; 8 | import { MetaMask, WagmiWeb3ConfigProvider } from "@ant-design/web3-wagmi"; 9 | import { Button, message } from "antd"; 10 | import { parseEther } from "viem"; 11 | import { 12 | createConfig, 13 | http, 14 | useReadContract, 15 | useWriteContract, 16 | useWatchContractEvent, 17 | } from "wagmi"; 18 | import { mainnet } from "wagmi/chains"; 19 | import { injected } from "wagmi/connectors"; 20 | 21 | const config = createConfig({ 22 | chains: [mainnet], 23 | transports: { 24 | [mainnet.id]: http(), 25 | }, 26 | connectors: [ 27 | injected({ 28 | target: "metaMask", 29 | }), 30 | ], 31 | }); 32 | 33 | const CallTest = () => { 34 | const { account } = useAccount(); 35 | const result = useReadContract({ 36 | abi: [ 37 | { 38 | type: "function", 39 | name: "balanceOf", 40 | stateMutability: "view", 41 | inputs: [{ name: "account", type: "address" }], 42 | outputs: [{ type: "uint256" }], 43 | }, 44 | ], 45 | // Sepolia test contract 0x418325c3979b7f8a17678ec2463a74355bdbe72c 46 | address: "0xEcd0D12E21805803f70de03B72B1C162dB0898d9", 47 | functionName: "balanceOf", 48 | args: [account?.address as `0x${string}`], 49 | }); 50 | const { writeContract } = useWriteContract(); 51 | 52 | useWatchContractEvent({ 53 | address: "0xEcd0D12E21805803f70de03B72B1C162dB0898d9", 54 | abi: [ 55 | { 56 | anonymous: false, 57 | inputs: [ 58 | { 59 | indexed: false, 60 | internalType: "address", 61 | name: "minter", 62 | type: "address", 63 | }, 64 | { 65 | indexed: false, 66 | internalType: "uint256", 67 | name: "amount", 68 | type: "uint256", 69 | }, 70 | ], 71 | name: "Minted", 72 | type: "event", 73 | }, 74 | ], 75 | eventName: "Minted", 76 | onLogs() { 77 | message.success("new minted!"); 78 | }, 79 | }); 80 | 81 | return ( 82 |
83 | {result.data?.toString()} 84 | 121 |
122 | ); 123 | }; 124 | 125 | export default function Web3() { 126 | return ( 127 | 128 |
129 | 133 | 134 | 135 | 136 | 137 | 138 | ); 139 | } 140 | -------------------------------------------------------------------------------- /06_NextJS/img/createnew.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/06_NextJS/img/createnew.png -------------------------------------------------------------------------------- /06_NextJS/img/deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/06_NextJS/img/deploy.png -------------------------------------------------------------------------------- /06_NextJS/img/domain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/06_NextJS/img/domain.png -------------------------------------------------------------------------------- /06_NextJS/img/editroot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/06_NextJS/img/editroot.png -------------------------------------------------------------------------------- /06_NextJS/img/import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/06_NextJS/img/import.png -------------------------------------------------------------------------------- /06_NextJS/img/nextbuild.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/06_NextJS/img/nextbuild.png -------------------------------------------------------------------------------- /06_NextJS/readme.md: -------------------------------------------------------------------------------- 1 | 本节作者:[@愚指导](https://x.com/yudao1024) 2 | 3 | 这一讲将会介绍如何把我们的应用部署到服务器上,完成前端部分功能从开发到最终上线。 4 | 5 | ## 简介 6 | 7 | 这里会简单介绍一下 Next.js 以及前端部署的一些基本概念。如果你对前端已经很熟悉,可以跳过这部分。 8 | 9 | [Next.js](https://nextjs.org/) 是一个前端框架,它可以帮助我们快速搭建 React 应用。我们基于 React 和 Next.js 开发完成我们的应用之后,需要将其构建并部署到服务器上,让用户可以访问我们的应用。 10 | 11 | 通过在项目中执行 `npm run build`(对应就是 `package.json` 中定义的 `next build`),它会将我们的应用构建为 JavaScript、CSS 等文件,这些文件或是在浏览器中运行的文件,浏览器本质上只支持执行 JavaScript,并不会原生支持 React,所以需要我们将其编译为浏览器可以执行的文件。又或者是在服务器的 Node.JS 环境中执行的 JavaScript,这部分让我们有能力处理一些服务端的逻辑,我们前面的课程中并未涉及到这部分,但是在后续的课程中,类似签名验证这样的逻辑就依赖于服务端的实现了。 12 | 13 | 执行成功可以看到类似下面这样的效果: 14 | 15 | ![build next](./img/nextbuild.png) 16 | 17 | 构建完成后你可以继续执行 `npm run start`,这样你的应用就成功在你本地部署了。对应的,如果你需要部署在你自己的服务器,那么流程是相同的,你需要在你的服务器中安装依赖、构建并通过 `npm run start` 或者 `next start` 启动你的应用。 18 | 19 | 当然,如果你使用的是 [umi](https://umijs.org/) 等其它前端框架,也是类似的逻辑,通常都需要构建和启动服务。如果构建产物是只有浏览器环境运行的静态资源,那么你只需要类似 Nginx 或者 express 这样的框架来部署一个 Web 容器存放即可,或者也可以使用 [Github Pages](https://pages.github.com/) 来部署。 20 | 21 | ## 基于 Vercel 部署 22 | 23 | 这一部分我们引导你基于 [Vercel](https://vercel.com/) 部署你的应用。 Next.js 就是 Vercel 官方在做支持的框架,它提供了一个非常简单的部署方式,你只需要将你的代码上传到 Github 或者 Gitlab 等代码托管平台,然后在 Vercel 上选择你的仓库,它会自动帮你构建并部署你的应用。 24 | 25 | 首先,请将你之前的代码推送到你自己的 Github 上,或者你也可以 Fork 本仓库 [https://github.com/WTFAcademy/WTF-Dapp](https://github.com/WTFAcademy/WTF-Dapp),我们在 [demo](../demo/) 文件夹中提供了完整的代码。 26 | 27 | 登录 Vercel 后在其控制台中创建一个新的项目: 28 | 29 | ![createnew](./img/createnew.png) 30 | 31 | 接下来选择并导入你的 Github 项目,这个过程中可能需要你授权 Vercel 访问你的 Github 仓库: 32 | 33 | ![import](./img/import.png) 34 | 35 | 你需要选择 Root Directory 为 Next.js 项目的根目录,然后点击 Deploy: 36 | 37 | ![editroot](./img/editroot.png) 38 | 39 | 接下来一切都会自动完成: 40 | 41 | ![deploy](./img/deploy.png) 42 | 43 | 最后本教程的 Demo 就呈现在 [https://wtf-dapp.vercel.app/web3](https://wtf-dapp.vercel.app/web3) 中了。 44 | 45 | ## 自定义域名 46 | 47 | Vercel 会提供一个默认的域名,但是通常我们需要一个自定义的域名,这样更加专业。你可以在 Vercel 的控制台中找到你的项目,然后在 Domains 中添加你的域名: 48 | 49 | ![domain](./img/domain.png) 50 | 51 | 添加后你需要按照提示在你的域名服务商中添加一条 CNAME 记录,这样你的域名就可以指向 Vercel 的服务器了。 52 | -------------------------------------------------------------------------------- /07_ContractDev/MyToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | 7 | contract MyToken is ERC721, Ownable { 8 | uint256 private _nextTokenId = 0; 9 | 10 | constructor() 11 | ERC721("MyToken", "MTK") 12 | Ownable(msg.sender) 13 | {} 14 | 15 | function mint(uint256 quantity) public payable { 16 | require(quantity == 1, "quantity must be 1"); 17 | require(msg.value == 0.01 ether, "must pay 0.01 ether"); 18 | uint256 tokenId = _nextTokenId++; 19 | _mint(msg.sender, tokenId); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /07_ContractDev/img/chai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/07_ContractDev/img/chai.png -------------------------------------------------------------------------------- /07_ContractDev/img/create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/07_ContractDev/img/create.png -------------------------------------------------------------------------------- /07_ContractDev/img/createBtn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/07_ContractDev/img/createBtn.png -------------------------------------------------------------------------------- /07_ContractDev/img/generate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/07_ContractDev/img/generate.png -------------------------------------------------------------------------------- /07_ContractDev/img/initCode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/07_ContractDev/img/initCode.png -------------------------------------------------------------------------------- /07_ContractDev/img/mintable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/07_ContractDev/img/mintable.png -------------------------------------------------------------------------------- /07_ContractDev/img/more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/07_ContractDev/img/more.png -------------------------------------------------------------------------------- /07_ContractDev/img/remix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/07_ContractDev/img/remix.png -------------------------------------------------------------------------------- /07_ContractDev/img/run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/07_ContractDev/img/run.png -------------------------------------------------------------------------------- /07_ContractDev/img/slide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/07_ContractDev/img/slide.png -------------------------------------------------------------------------------- /07_ContractDev/img/unitTest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/07_ContractDev/img/unitTest.png -------------------------------------------------------------------------------- /07_ContractDev/img/unitTest1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/07_ContractDev/img/unitTest1.png -------------------------------------------------------------------------------- /08_ContractDeploy/demo/dapp.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | ConnectButton, 4 | Connector, 5 | NFTCard, 6 | useAccount, 7 | } from "@ant-design/web3"; 8 | import { 9 | Sepolia, 10 | MetaMask, 11 | WagmiWeb3ConfigProvider, 12 | } from "@ant-design/web3-wagmi"; 13 | import { Button, message } from "antd"; 14 | import { parseEther } from "viem"; 15 | import { createConfig, http, useReadContract, useWriteContract } from "wagmi"; 16 | import { sepolia, mainnet } from "wagmi/chains"; 17 | import { injected } from "wagmi/connectors"; 18 | 19 | const config = createConfig({ 20 | chains: [mainnet, sepolia], 21 | transports: { 22 | [mainnet.id]: http(), 23 | [sepolia.id]: http(), 24 | }, 25 | connectors: [ 26 | injected({ 27 | target: "metaMask", 28 | }), 29 | ], 30 | }); 31 | 32 | const CallTest = () => { 33 | const { account } = useAccount(); 34 | const result = useReadContract({ 35 | abi: [ 36 | { 37 | type: "function", 38 | name: "balanceOf", 39 | stateMutability: "view", 40 | inputs: [{ name: "account", type: "address" }], 41 | outputs: [{ type: "uint256" }], 42 | }, 43 | ], 44 | // Sepolia test contract 0x418325c3979b7f8a17678ec2463a74355bdbe72c 45 | address: "0xEcd0D12E21805803f70de03B72B1C162dB0898d9", 46 | functionName: "balanceOf", 47 | args: [account?.address as `0x${string}`], 48 | }); 49 | const { writeContract } = useWriteContract(); 50 | 51 | return ( 52 |
53 | {result.data?.toString()} 54 | 91 |
92 | ); 93 | }; 94 | 95 | export default function Web3() { 96 | return ( 97 | 102 |
103 | 107 | 108 | 109 | 110 | 111 | 112 | ); 113 | } 114 | -------------------------------------------------------------------------------- /08_ContractDeploy/img/call-in-ide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/08_ContractDeploy/img/call-in-ide.png -------------------------------------------------------------------------------- /08_ContractDeploy/img/changeNode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/08_ContractDeploy/img/changeNode.png -------------------------------------------------------------------------------- /08_ContractDeploy/img/compile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/08_ContractDeploy/img/compile.png -------------------------------------------------------------------------------- /08_ContractDeploy/img/connect1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/08_ContractDeploy/img/connect1.png -------------------------------------------------------------------------------- /08_ContractDeploy/img/connect2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/08_ContractDeploy/img/connect2.png -------------------------------------------------------------------------------- /08_ContractDeploy/img/copyABI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/08_ContractDeploy/img/copyABI.png -------------------------------------------------------------------------------- /08_ContractDeploy/img/json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/08_ContractDeploy/img/json.png -------------------------------------------------------------------------------- /08_ContractDeploy/img/mint-test-net.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/08_ContractDeploy/img/mint-test-net.png -------------------------------------------------------------------------------- /08_ContractDeploy/img/sendTrans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/08_ContractDeploy/img/sendTrans.png -------------------------------------------------------------------------------- /08_ContractDeploy/img/transInfo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/08_ContractDeploy/img/transInfo.png -------------------------------------------------------------------------------- /09_EIP1193/img/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/09_EIP1193/img/demo.png -------------------------------------------------------------------------------- /09_EIP1193/readme.md: -------------------------------------------------------------------------------- 1 | 本节作者:[@愚指导](https://x.com/yudao1024) 2 | 3 | 我们在第三讲[连接钱包](./03_ConnectWallet/readme.md)中介绍了如何连接钱包,但是没有深入讲解连接钱包的原理。EIP1193 和 EIP6963 是两个重要的协议,它们定义了 DApp 连接钱包的标准。EIP1193 是最早期的标准,但是有一些缺陷,EIP6963 则是对 EIP1193 的改进,解决了那些缺陷。 4 | 5 | 本文将介绍这两个协议的基本概念,帮助你理解链接钱包的逻辑。 6 | 7 | ## EIP1193 8 | 9 | EIP1193 的规范地址在[https://eips.ethereum.org/EIPS/eip-1193](https://eips.ethereum.org/EIPS/eip-1193),它定义了在浏览器中如何通过 JavaScript 与钱包进行交互,有了这个规范钱包才能按照他提供相关接口,DApp 才能按照它调用钱包提供的接口。 10 | 11 | 这个定义很简单,其实就是约定了浏览器运行时的全局对象 `window` 上的 `ethereum` 对象的格式,定义了一些方法和事件。 12 | 13 | 对于 DApp 来说,它需要检查 `window.ethereum` 是否存在,如果存在,那么它就可以调用 `window.ethereum` 上的方法来和钱包进行交互。就同调用浏览器其他 API 类似,比如 `window.localStorage`。 14 | 15 | 下面是一个简单的例子可以获取链的 ID: 16 | 17 | ```javascript 18 | const ethereum = window.ethereum; 19 | 20 | ethereum 21 | .request({ method: "eth_chainId" }) 22 | .then((chainId) => { 23 | console.log(`hexadecimal string: ${chainId}`); 24 | console.log(`decimal number: ${parseInt(chainId, 16)}`); 25 | }) 26 | .catch((error) => { 27 | console.error(`Error fetching chainId: ${error.code}: ${error.message}`); 28 | }); 29 | ``` 30 | 31 | 你可以尝试在浏览器的控制台运行查看效果: 32 | 33 | ![](./img/demo.png) 34 | 35 | 对于更多方法,也就对应修改 `request` 调用时的参数即可,支持的方法可以参考 [JSON-RPC API](https://ethereum.org/developers/docs/apis/json-rpc)。当然,对于一些钱包可能不支持的方法,你需要做好异常处理。也有一些钱包特有的一些方法或者一些已经约定俗成的方法,你需要查看钱包的文档。 36 | 37 | 通常来说,在 DApp 中你应该使用类似 `web3.js`、`ethers`、`viem` 这样的 SDK 来和钱包进行交互,这些 SDK 会帮你封装好一些方法,让你更方便的和钱包进行交互。 38 | 39 | 以上就是 EIP1193 的基本概念,但是 EIP1193 有一个主要的缺陷。就是 `window.ethereum` 对象只有一个,所以当用户安装了多个钱包时,用户只能选择一个钱包来使用。这样会导致钱包之间会争抢 `window.ethereum` 对象,一方面损害了用户体验,另外也不利于钱包之间的良性竞争。 40 | 41 | 在之前很长一段时间,针对这个问题钱包的做法一般是会注入自己独特的对象,比如 TokenPocket 会注入 `window.tokenPocket`。但是这样的做法并不是标准的,也不是一个好的解决方案。另外,这样的做法也会导致 DApp 需要适配很多钱包,增加了 DApp 的开发成本。 42 | 43 | 所以就有了 EIP6963,接下来我们会介绍 EIP6963。 44 | 45 | ## EIP6963 46 | 47 | EIP6963 的规范地址在[https://eips.ethereum.org/EIPS/eip-6963](https://eips.ethereum.org/EIPS/eip-6963)。 48 | 49 | EIP6963 不再通过 `window.ethereum` 对象来和钱包进行交互,而是通过往 `window` 发送事件的方式来和钱包进行交互。这样就解决了 EIP1193 的问题,多个钱包可以和 DApp 进行交互,而不会争抢 `window.ethereum` 对象。 50 | 51 | 另外钱包也可以通过发送事件的方式主动告知 DApp 它的存在,这样 DApp 就可以知道用户安装了哪些钱包,然后根据用户的选择来和钱包进行交互。 52 | 53 | 技术上来讲其实就是通过浏览器的 `window.addEventListener` 来监听消息,通过 `window.dispatchEvent` 来发送消息。所有消息的 `type` 都有 `eip6963:` 前缀,具体的消息内容定义可以参考规范文档。 54 | 55 | 对于开发者来说,和 EIP1193 一样,你使用一些社区的库即可,这样可以免去对细节的关注。比如你如果使用 wagmi,那么通过配置 [multiInjectedProviderDiscovery](https://wagmi.sh/core/api/createConfig#multiinjectedproviderdiscovery) 即可接入 EIP6963。 56 | 57 | 如果你使用了 [Ant Design Web3](https://web3.ant.design/zh-CN/components/wagmi#eip6963),通过配置 `WagmiWeb3ConfigProvider` 的 `eip6963` 即可在 DApp 中使用 EIP6963。它的连接钱包的弹窗会自动添加检测到的钱包。 58 | 59 | 下面是我们基于之前的课程例子的修改示例: 60 | 61 | ```diff 62 | export default function Web3() { 63 | return ( 64 | 71 |
72 | 76 | 77 | 78 | 79 | 80 | 81 | ); 82 | } 83 | ``` 84 | 85 | 其中配置了 `eip6963` 使得使用通过 EIP6963 协议连接钱包,避免了多个钱包之间可能出现的冲突。另外添加了 `autoAddInjectedWallets` 配置使得自动添加检测到的钱包到 Ant Design Web3 的 UI 中,提升用户体验,让用户可以自由选择他已经安装的钱包。 86 | 87 | ## 总结 88 | 89 | 不管是 EIP1193 还是 EIP6963,它们都是通过浏览器的 JavaScript API 来和钱包进行交互的。它要求钱包可以向 DApp 的运行时注入对象或者发送事件,比如通过 Chrome 浏览器插件,或者你在钱包内置的浏览器中访问 DApp。 90 | 91 | 但是对于有的场景,用户没有安装插件,或者是在移动端浏览器访问 DApp,无法使用插件。又或者用户需要用其他手机安装的钱包客户端来连接 DApp。不管是 EIP1193 还是 EIP6963,都无法满足这些场景。所以,我们还需要其他的方式来连接钱包,比如 WalletConnect。我们会在下一讲介绍如何使用 WalletConnect 来连接钱包。 92 | -------------------------------------------------------------------------------- /09_EIP1193/web3.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | ConnectButton, 4 | Connector, 5 | NFTCard, 6 | useAccount, 7 | } from "@ant-design/web3"; 8 | import { MetaMask, WagmiWeb3ConfigProvider } from "@ant-design/web3-wagmi"; 9 | import { Button, message } from "antd"; 10 | import { parseEther } from "viem"; 11 | import { 12 | createConfig, 13 | http, 14 | useReadContract, 15 | useWriteContract, 16 | useWatchContractEvent, 17 | } from "wagmi"; 18 | import { mainnet } from "wagmi/chains"; 19 | import { injected } from "wagmi/connectors"; 20 | 21 | const config = createConfig({ 22 | chains: [mainnet], 23 | transports: { 24 | [mainnet.id]: http(), 25 | }, 26 | connectors: [ 27 | injected({ 28 | target: "metaMask", 29 | }), 30 | ], 31 | }); 32 | 33 | const CallTest = () => { 34 | const { account } = useAccount(); 35 | const result = useReadContract({ 36 | abi: [ 37 | { 38 | type: "function", 39 | name: "balanceOf", 40 | stateMutability: "view", 41 | inputs: [{ name: "account", type: "address" }], 42 | outputs: [{ type: "uint256" }], 43 | }, 44 | ], 45 | // Sepolia test contract 0x418325c3979b7f8a17678ec2463a74355bdbe72c 46 | address: "0xEcd0D12E21805803f70de03B72B1C162dB0898d9", 47 | functionName: "balanceOf", 48 | args: [account?.address as `0x${string}`], 49 | }); 50 | const { writeContract } = useWriteContract(); 51 | 52 | useWatchContractEvent({ 53 | address: "0xEcd0D12E21805803f70de03B72B1C162dB0898d9", 54 | abi: [ 55 | { 56 | anonymous: false, 57 | inputs: [ 58 | { 59 | indexed: false, 60 | internalType: "address", 61 | name: "minter", 62 | type: "address", 63 | }, 64 | { 65 | indexed: false, 66 | internalType: "uint256", 67 | name: "amount", 68 | type: "uint256", 69 | }, 70 | ], 71 | name: "Minted", 72 | type: "event", 73 | }, 74 | ], 75 | eventName: "Minted", 76 | onLogs() { 77 | message.success("new minted!"); 78 | }, 79 | }); 80 | 81 | return ( 82 |
83 | {result.data?.toString()} 84 | 121 |
122 | ); 123 | }; 124 | 125 | export default function Web3() { 126 | return ( 127 | 134 |
135 | 139 | 140 | 141 | 142 | 143 | 144 | ); 145 | } 146 | -------------------------------------------------------------------------------- /10_WalletConnect/img/walletconnect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/10_WalletConnect/img/walletconnect.png -------------------------------------------------------------------------------- /10_WalletConnect/img/walletnetwork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/10_WalletConnect/img/walletnetwork.png -------------------------------------------------------------------------------- /10_WalletConnect/img/walletqrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/10_WalletConnect/img/walletqrcode.png -------------------------------------------------------------------------------- /10_WalletConnect/web3.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | ConnectButton, 4 | Connector, 5 | NFTCard, 6 | useAccount, 7 | } from "@ant-design/web3"; 8 | import { 9 | MetaMask, 10 | WagmiWeb3ConfigProvider, 11 | WalletConnect, 12 | } from "@ant-design/web3-wagmi"; 13 | import { Button, message } from "antd"; 14 | import { parseEther } from "viem"; 15 | import { 16 | createConfig, 17 | http, 18 | useReadContract, 19 | useWriteContract, 20 | useWatchContractEvent, 21 | } from "wagmi"; 22 | import { mainnet } from "wagmi/chains"; 23 | import { injected, walletConnect } from "wagmi/connectors"; 24 | 25 | const config = createConfig({ 26 | chains: [mainnet], 27 | transports: { 28 | [mainnet.id]: http(), 29 | }, 30 | connectors: [ 31 | injected({ 32 | target: "metaMask", 33 | }), 34 | walletConnect({ 35 | projectId: "c07c0051c2055890eade3556618e38a6", 36 | showQrModal: false, 37 | }), 38 | ], 39 | }); 40 | 41 | const CallTest = () => { 42 | const { account } = useAccount(); 43 | const result = useReadContract({ 44 | abi: [ 45 | { 46 | type: "function", 47 | name: "balanceOf", 48 | stateMutability: "view", 49 | inputs: [{ name: "account", type: "address" }], 50 | outputs: [{ type: "uint256" }], 51 | }, 52 | ], 53 | // Sepolia test contract 0x418325c3979b7f8a17678ec2463a74355bdbe72c 54 | address: "0xEcd0D12E21805803f70de03B72B1C162dB0898d9", 55 | functionName: "balanceOf", 56 | args: [account?.address as `0x${string}`], 57 | }); 58 | const { writeContract } = useWriteContract(); 59 | 60 | useWatchContractEvent({ 61 | address: "0xEcd0D12E21805803f70de03B72B1C162dB0898d9", 62 | abi: [ 63 | { 64 | anonymous: false, 65 | inputs: [ 66 | { 67 | indexed: false, 68 | internalType: "address", 69 | name: "minter", 70 | type: "address", 71 | }, 72 | { 73 | indexed: false, 74 | internalType: "uint256", 75 | name: "amount", 76 | type: "uint256", 77 | }, 78 | ], 79 | name: "Minted", 80 | type: "event", 81 | }, 82 | ], 83 | eventName: "Minted", 84 | onLogs() { 85 | message.success("new minted!"); 86 | }, 87 | }); 88 | 89 | return ( 90 |
91 | {result.data?.toString()} 92 | 129 |
130 | ); 131 | }; 132 | 133 | export default function Web3() { 134 | return ( 135 | 142 |
143 | 147 | 148 | 149 | 150 | 151 | 152 | ); 153 | } 154 | -------------------------------------------------------------------------------- /11_MultipleChain/img/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/11_MultipleChain/img/image.png -------------------------------------------------------------------------------- /11_MultipleChain/web3.tsx: -------------------------------------------------------------------------------- 1 | import { createConfig, http, useReadContract, useWriteContract } from "wagmi"; 2 | import { mainnet, sepolia, polygon } from "wagmi/chains"; 3 | import { 4 | WagmiWeb3ConfigProvider, 5 | MetaMask, 6 | Sepolia, 7 | Polygon, 8 | } from "@ant-design/web3-wagmi"; 9 | import { 10 | Address, 11 | NFTCard, 12 | Connector, 13 | ConnectButton, 14 | useAccount, 15 | useProvider, 16 | } from "@ant-design/web3"; 17 | import { injected } from "wagmi/connectors"; 18 | import { Button, message } from "antd"; 19 | import { parseEther } from "viem"; 20 | 21 | const config = createConfig({ 22 | chains: [mainnet, sepolia, polygon], 23 | transports: { 24 | [mainnet.id]: http(), 25 | [sepolia.id]: http(), 26 | [polygon.id]: http(), 27 | }, 28 | connectors: [ 29 | injected({ 30 | target: "metaMask", 31 | }), 32 | ], 33 | }); 34 | 35 | const contractInfo = [ 36 | { 37 | id: 1, 38 | name: "Ethereum", 39 | contractAddress: "0xEcd0D12E21805803f70de03B72B1C162dB0898d9", 40 | }, 41 | { 42 | id: 5, 43 | name: "Sepolia", 44 | contractAddress: "0x418325c3979b7f8a17678ec2463a74355bdbe72c", 45 | }, 46 | { 47 | id: 137, 48 | name: "Polygon", 49 | contractAddress: "0x418325c3979b7f8a17678ec2463a74355bdbe72c", 50 | }, 51 | ]; 52 | 53 | const CallTest = () => { 54 | const { account } = useAccount(); 55 | const { chain } = useProvider(); 56 | const result = useReadContract({ 57 | abi: [ 58 | { 59 | type: "function", 60 | name: "balanceOf", 61 | stateMutability: "view", 62 | inputs: [{ name: "account", type: "address" }], 63 | outputs: [{ type: "uint256" }], 64 | }, 65 | ], 66 | address: contractInfo.find((item) => item.id === chain?.id) 67 | ?.contractAddress as `0x${string}`, 68 | functionName: "balanceOf", 69 | args: [account?.address as `0x${string}`], 70 | }); 71 | const { writeContract } = useWriteContract(); 72 | 73 | return ( 74 |
75 | {result.data?.toString()} 76 | 114 |
115 | ); 116 | }; 117 | 118 | export default function Web3() { 119 | return ( 120 | 125 |
126 | 130 | 131 | 132 | 133 | 134 | 135 | ); 136 | } 137 | -------------------------------------------------------------------------------- /12_Signature/img/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/12_Signature/img/demo.png -------------------------------------------------------------------------------- /13_Payment/img/send.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/13_Payment/img/send.png -------------------------------------------------------------------------------- /14_LocalDev/img/addnetwork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/14_LocalDev/img/addnetwork.png -------------------------------------------------------------------------------- /14_LocalDev/img/hardhat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/14_LocalDev/img/hardhat.png -------------------------------------------------------------------------------- /14_LocalDev/img/initfiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/14_LocalDev/img/initfiles.png -------------------------------------------------------------------------------- /14_LocalDev/img/localnode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/14_LocalDev/img/localnode.png -------------------------------------------------------------------------------- /14_LocalDev/img/minttest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/14_LocalDev/img/minttest.png -------------------------------------------------------------------------------- /15_WagmiCli/readme.md: -------------------------------------------------------------------------------- 1 | 本节作者:[@愚指导](https://x.com/yudao1024) 2 | 3 | 在上一讲我们基于 Hardhat 在本地创建了一个合约项目,在未来的 DEX 实战课程中我们会继续基于该项目创建更复杂的合约。伴随着合约越来越复杂,ABI 也会越来越庞大,这时候我们就需要一个工具来帮助我们更好的调试合约。在这一讲中我们将会引导大家给予 Wagmi CLI 自动化的创建用于合约调试的代码,并在项目中使用它,为后续的 DEX 实战课程做准备。 4 | 5 | --- 6 | 7 | ## 初始化 Wagmi CLI 8 | 9 | 按照下面步骤初始化,你也可以参考 [Wagmi CLI 的官方文档](https://wagmi.sh/cli/getting-started)操作。 10 | 11 | 安装依赖: 12 | 13 | ```bash 14 | npm install --save-dev @wagmi/cli 15 | ``` 16 | 17 | 修改配置 `demo/wagmi.config.ts`,添加 [hardhat](https://wagmi.sh/cli/api/plugins/hardhat) 和 [react](https://wagmi.sh/cli/api/plugins/react) 插件。 18 | 19 | 完整配置如下: 20 | 21 | ```ts 22 | import { defineConfig } from "@wagmi/cli"; 23 | import { hardhat } from "@wagmi/cli/plugins"; 24 | import { react } from "@wagmi/cli/plugins"; 25 | 26 | export default defineConfig({ 27 | out: "utils/contracts.ts", 28 | plugins: [ 29 | hardhat({ 30 | project: "../demo-contract", 31 | }), 32 | react(), 33 | ], 34 | }); 35 | ``` 36 | 37 | 执行下面命令生成代码: 38 | 39 | ```bash 40 | npx wagmi generate 41 | ``` 42 | 43 | 执行完成后你会看到生成的代码在 `utils/contracts.ts` 中,接下来你就可以更方便的在项目中使用 `utils/contracts.ts` 导出的 React Hooks 来调用合约了。 44 | 45 | ## 使用 Wagmi CLI 生成的代码 46 | 47 | 我们继续修改 `demo/pages/web3.tsx` 的代码,将之前使用的 `useReadContract` 和 `useWriteContract` 替换为 Wagmi CLI 生成的代码。 48 | 49 | ```diff 50 | // ... 51 | -import { 52 | - createConfig, 53 | - http, 54 | - useReadContract, 55 | - useWriteContract, 56 | - useWatchContractEvent, 57 | -} from "wagmi"; 58 | +import { createConfig, http, useWatchContractEvent } from "wagmi"; 59 | import { injected, walletConnect } from "wagmi/connectors"; 60 | +import { 61 | + useReadMyTokenBalanceOf, 62 | + useWriteMyTokenMint, 63 | +} from "@/utils/contracts"; 64 | 65 | // ... 66 | ``` 67 | 68 | 修改 `balanceOf` 方法的调用逻辑: 69 | 70 | ```diff 71 | 72 | // ... 73 | 74 | - const result = useReadContract({ 75 | - abi: [ 76 | - { 77 | - type: "function", 78 | - name: "balanceOf", 79 | - stateMutability: "view", 80 | - inputs: [{ name: "account", type: "address" }], 81 | - outputs: [{ type: "uint256" }], 82 | - }, 83 | - ], 84 | + const result = useReadMyTokenBalanceOf({ 85 | address: contractInfo.find((item) => item.id === chain?.id) 86 | ?.contractAddress as `0x${string}`, 87 | - functionName: "balanceOf", 88 | args: [account?.address as `0x${string}`], 89 | }); 90 | 91 | // ... 92 | 93 | ``` 94 | 95 | 修改 `mint` 方法的调用逻辑: 96 | 97 | ```diff 98 | - const { writeContract } = useWriteContract(); 99 | + const { writeContract: mintNFT } = useWriteMyTokenMint(); 100 | 101 | // ... 102 | 103 | const CallTest = () => { 104 | {result.data?.toString()} 105 | 59 | 60 | 61 | 62 | 63 | 90 | 91 | 92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /P204_SwapUI/code/swap.module.css: -------------------------------------------------------------------------------- 1 | .swapCard { 2 | width: 600px; 3 | margin: 0 auto; 4 | position: absolute; 5 | top: 50%; 6 | left: 50%; 7 | transform: translate(-50%, -50%); 8 | } 9 | 10 | .switchBtn { 11 | font-size: '20px'; 12 | display: flex; 13 | justify-content: center; 14 | margin: 16px 0; 15 | } 16 | 17 | .swapSpace { 18 | width: 100%; 19 | justify-content: space-between; 20 | } 21 | 22 | .swapBtn { 23 | margin-top: 10px; 24 | } -------------------------------------------------------------------------------- /P204_SwapUI/img/swap1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/P204_SwapUI/img/swap1.png -------------------------------------------------------------------------------- /P204_SwapUI/img/swap2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/P204_SwapUI/img/swap2.png -------------------------------------------------------------------------------- /P204_SwapUI/img/swap3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/P204_SwapUI/img/swap3.png -------------------------------------------------------------------------------- /P204_SwapUI/img/swap4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/P204_SwapUI/img/swap4.png -------------------------------------------------------------------------------- /P204_SwapUI/img/ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/P204_SwapUI/img/ui.png -------------------------------------------------------------------------------- /P205_PoolListUI/code/pool.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin: 24px 148px; 3 | } 4 | -------------------------------------------------------------------------------- /P205_PoolListUI/code/pool.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Flex, Table, Space, Typography, Button } from "antd"; 3 | import type { TableProps } from "antd"; 4 | import WtfLayout from "@/components/WtfLayout"; 5 | import Link from "next/link"; 6 | import styles from "./pool.module.css"; 7 | 8 | const columns: TableProps["columns"] = [ 9 | { 10 | title: "Token 0", 11 | dataIndex: "token0", 12 | key: "token0", 13 | }, 14 | { 15 | title: "Token 1", 16 | dataIndex: "token1", 17 | key: "token1", 18 | }, 19 | { 20 | title: "Index", 21 | dataIndex: "index", 22 | key: "index", 23 | }, 24 | { 25 | title: "Fee", 26 | dataIndex: "fee", 27 | key: "fee", 28 | }, 29 | { 30 | title: "Fee Protocol", 31 | dataIndex: "feeProtocol", 32 | key: "feeProtocol", 33 | }, 34 | { 35 | title: "Tick Lower", 36 | dataIndex: "tickLower", 37 | key: "tickLower", 38 | }, 39 | { 40 | title: "Tick Upper", 41 | dataIndex: "tickUpper", 42 | key: "tickUpper", 43 | }, 44 | { 45 | title: "Tick", 46 | dataIndex: "tick", 47 | key: "tick", 48 | }, 49 | { 50 | title: "Price", 51 | dataIndex: "sqrtPriceX96", 52 | key: "sqrtPriceX96", 53 | render: (value: bigint) => { 54 | return value.toString(); 55 | }, 56 | }, 57 | ]; 58 | 59 | const PoolListTable: React.FC = () => { 60 | const data = [ 61 | { 62 | token0: "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", 63 | token1: "0xEcd0D12E21805803f70de03B72B1C162dB0898d9", 64 | index: 0, 65 | fee: 3000, 66 | feeProtocol: 0, 67 | tickLower: -100000, 68 | tickUpper: 100000, 69 | tick: 1000, 70 | sqrtPriceX96: BigInt("7922737261735934252089901697281"), 71 | }, 72 | ]; 73 | return ( 74 | ( 76 | 77 |
Pool List
78 | 79 | 80 | 81 | 82 | 83 | 84 |
85 | )} 86 | columns={columns} 87 | dataSource={data} 88 | /> 89 | ); 90 | }; 91 | 92 | export default function WtfswapPool() { 93 | return ( 94 | 95 |
96 | Pool 97 | 98 |
99 |
100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /P205_PoolListUI/img/pool-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/P205_PoolListUI/img/pool-1.png -------------------------------------------------------------------------------- /P205_PoolListUI/img/pool-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/P205_PoolListUI/img/pool-2.png -------------------------------------------------------------------------------- /P205_PoolListUI/img/pool-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/P205_PoolListUI/img/pool-3.png -------------------------------------------------------------------------------- /P205_PoolListUI/img/pool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/P205_PoolListUI/img/pool.png -------------------------------------------------------------------------------- /P206_AddPoolUI/img/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/P206_AddPoolUI/img/add.png -------------------------------------------------------------------------------- /P206_AddPoolUI/img/create-pool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/P206_AddPoolUI/img/create-pool.png -------------------------------------------------------------------------------- /P207_PositionList/code/positions.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin: 24px 148px; 3 | } 4 | -------------------------------------------------------------------------------- /P207_PositionList/code/positions.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Flex, Table, Space, Typography, Button } from "antd"; 3 | import type { TableProps } from "antd"; 4 | import WtfLayout from "@/components/WtfLayout"; 5 | import styles from "./position.module.css"; 6 | 7 | const columns: TableProps["columns"] = [ 8 | { 9 | title: "Owner", 10 | dataIndex: "owner", 11 | key: "owner", 12 | ellipsis: true, 13 | }, 14 | { 15 | title: "Token 0", 16 | dataIndex: "token0", 17 | key: "token0", 18 | ellipsis: true, 19 | }, 20 | { 21 | title: "Token 1", 22 | dataIndex: "token1", 23 | key: "token1", 24 | ellipsis: true, 25 | }, 26 | { 27 | title: "Index", 28 | dataIndex: "index", 29 | key: "index", 30 | }, 31 | { 32 | title: "Fee", 33 | dataIndex: "fee", 34 | key: "fee", 35 | }, 36 | { 37 | title: "Liquidity", 38 | dataIndex: "liquidity", 39 | key: "liquidity", 40 | render: (value: bigint) => { 41 | return value.toString(); 42 | }, 43 | }, 44 | { 45 | title: "Tick Lower", 46 | dataIndex: "tickLower", 47 | key: "tickLower", 48 | }, 49 | { 50 | title: "Tick Upper", 51 | dataIndex: "tickUpper", 52 | key: "tickUpper", 53 | }, 54 | { 55 | title: "Tokens Owed 0", 56 | dataIndex: "tokensOwed0", 57 | key: "tokensOwed0", 58 | render: (value: bigint) => { 59 | return value.toString(); 60 | }, 61 | }, 62 | { 63 | title: "Tokens Owed 1", 64 | dataIndex: "tokensOwed1", 65 | key: "tokensOwed1", 66 | render: (value: bigint) => { 67 | return value.toString(); 68 | }, 69 | }, 70 | { 71 | title: "Fee Growth Inside 0", 72 | dataIndex: "feeGrowthInside0LastX128", 73 | key: "feeGrowthInside0LastX128", 74 | render: (value: bigint) => { 75 | return value.toString(); 76 | }, 77 | }, 78 | { 79 | title: "Fee Growth Inside 1", 80 | dataIndex: "feeGrowthInside1LastX128", 81 | key: "feeGrowthInside1LastX128", 82 | render: (value: bigint) => { 83 | return value.toString(); 84 | }, 85 | }, 86 | { 87 | title: "Actions", 88 | key: "actions", 89 | render: () => ( 90 | 91 | Remove 92 | Collect 93 | 94 | ), 95 | }, 96 | ]; 97 | 98 | const PoolListTable: React.FC = () => { 99 | const data = [ 100 | { 101 | owner: "0x1234567890abcdef1234567890abcdef12345678", 102 | token0: "0x1234567890abcdef1234567890abcdef12345678", 103 | token1: "0x1234567890abcdef1234567890abcdef12345678", 104 | index: 0, 105 | fee: 3000, 106 | liquidity: BigInt(1234560000000), 107 | tickLower: -123456, 108 | tickUpper: 123456, 109 | tokensOwed0: BigInt(123456), 110 | tokensOwed1: BigInt(654321), 111 | feeGrowthInside0LastX128: BigInt(123456), 112 | feeGrowthInside1LastX128: BigInt(654321), 113 | }, 114 | ]; 115 | return ( 116 |
( 118 | 119 |
My Positions
120 | 121 | 122 | 123 |
124 | )} 125 | columns={columns} 126 | dataSource={data} 127 | /> 128 | ); 129 | }; 130 | 131 | export default function WtfswapPool() { 132 | return ( 133 | 134 |
135 | Postions 136 | 137 |
138 |
139 | ); 140 | } 141 | -------------------------------------------------------------------------------- /P207_PositionList/img/position-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/P207_PositionList/img/position-1.png -------------------------------------------------------------------------------- /P207_PositionList/img/position-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/P207_PositionList/img/position-2.png -------------------------------------------------------------------------------- /P207_PositionList/img/position.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/P207_PositionList/img/position.png -------------------------------------------------------------------------------- /P208_AddPositionUI/img/add-position.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/P208_AddPositionUI/img/add-position.png -------------------------------------------------------------------------------- /P208_AddPositionUI/img/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/P208_AddPositionUI/img/add.png -------------------------------------------------------------------------------- /P209_DebugWithChain/img/debugtoken.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/P209_DebugWithChain/img/debugtoken.png -------------------------------------------------------------------------------- /P210_DebugPool/img/pool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/P210_DebugPool/img/pool.png -------------------------------------------------------------------------------- /P210_DebugPool/readme.md: -------------------------------------------------------------------------------- 1 | 本节作者:[@愚指导](https://x.com/yudao1024) 2 | 3 | 这一讲开始将会开发前端调用合约相关逻辑,我们首先从支持添加交易对开始。 4 | 5 | --- 6 | 7 | ## 获取交易池列表 8 | 9 | 我们在之前的[《PoolManager 合约开发》](../P104_PoolManager/readme.md)中已经实现了 `getAllPools` 方法,这个方法可以获取所有的交易池列表。我们在前端调用这个方法,获取所有的交易池列表。 10 | 11 | > 需要注意的是,在实际的 DApp 开发中,该方法由服务端维护和返回更合适,而不需要在合约中实现,我们这么做只是为了优化课程内容。 12 | 13 | 我们在上一讲中通过 `npx wagmi generate` 更新了 `utils/coontract.ts` 文件,里面包含了一系列的 React Hooks,我们可以很方便的调用合约方法。 14 | 15 | 我们只需要使用 `useReadPoolManagerGetAllPools` 就可以获取到所有的交易池列表,如下所示,几行关键的代码即可完成: 16 | 17 | ```diff 18 | 19 | + import { useReadPoolManagerGetAllPools } from "@/utils/contracts"; 20 | 21 | const PoolListTable: React.FC = () => { 22 | + const { data = [] } = useReadPoolManagerGetAllPools({ 23 | + address: getContractAddress("PoolManager"), 24 | + }); 25 | return ( 26 | <> 27 |
( 31 | 32 | // ... 33 | 34 | )} 35 | columns={columns} 36 | + dataSource={data} 37 | /> 38 | 39 | ); 40 | }; 41 | ``` 42 | 43 | 对于类似 `getAllPools` 这样读的方法来说,因为不涉及到链上状态的变化,所以我们可以直接调用,不需要发起交易,也不需要消耗 GAS。 44 | 45 | ## 创建交易池 46 | 47 | 创建交易池需要调用的是 `createAndInitializePoolIfNecessary` 方法,这是一个写方法,需要发起交易,唤起用户钱包签名。 48 | 49 | 关键代码如下: 50 | 51 | ```diff 52 | import { 53 | useReadPoolManagerGetAllPools, 54 | + useWritePoolManagerCreateAndInitializePoolIfNecessary, 55 | } from "@/utils/contracts"; 56 | 57 | 58 | const PoolListTable: React.FC = () => { 59 | const [openAddPoolModal, setOpenAddPoolModal] = React.useState(false); 60 | + const [loading, setLoading] = React.useState(false); 61 | - const { data = [] } = useReadPoolManagerGetAllPools({ 62 | + const { data = [], refetch } = useReadPoolManagerGetAllPools({ 63 | address: getContractAddress("PoolManager"), 64 | }); 65 | + const { writeContractAsync } = 66 | + useWritePoolManagerCreateAndInitializePoolIfNecessary(); 67 | return ( 68 | <> 69 |
( 73 | 74 |
Pool List
75 | 76 | 77 | 78 | 79 | 88 | 89 |
90 | )} 91 | columns={columns} 92 | dataSource={data} 93 | /> 94 | { 97 | setOpenAddPoolModal(false); 98 | }} 99 | onCreatePool={async (createParams) => { 100 | console.log("get createParams", createParams); 101 | + setLoading(true); 102 | + setOpenAddPoolModal(false); 103 | + try { 104 | + await writeContractAsync({ 105 | + address: getContractAddress("PoolManager"), 106 | + args: [ 107 | + { 108 | + token0: createParams.token0, 109 | + token1: createParams.token1, 110 | + fee: createParams.fee, 111 | + tickLower: createParams.tickLower, 112 | + tickUpper: createParams.tickUpper, 113 | + sqrtPriceX96: createParams.sqrtPriceX96, 114 | + }, 115 | + ], 116 | + }); 117 | + message.success("Create Pool Success If Necessary"); 118 | + refetch(); 119 | + } catch (error: any) { 120 | + message.error(error.message); 121 | + } finally { 122 | + setLoading(false); 123 | + } 124 | }} 125 | /> 126 | 127 | ); 128 | }; 129 | ``` 130 | 131 | 核心逻辑是完善了 `onCreatePool` 方法,它会调用 `writeContractAsync` 创建一个交易池,这个过程会唤起用户钱包要求用户签名。另外创建成功后更新交易池列表,需要注意的是我们使用的是 `writeContractAsync` 而不是 `writeContract`,前者会等到真正创建成功后才会返回,后者会立即返回。 132 | 133 | 完成的代码你可以在 [demo/pages/wtfswap/pool.tsx](../demo/pages/wtfswap/pool.tsx) 中查看。 134 | 135 | 最后的效果如下: 136 | 137 | ![pool](./img/pool.png) 138 | -------------------------------------------------------------------------------- /P211_DebugPosition/img/positions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/P211_DebugPosition/img/positions.png -------------------------------------------------------------------------------- /P212_DebugSwap/img/swap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/P212_DebugSwap/img/swap.png -------------------------------------------------------------------------------- /P301_ContractOptimize/bugs/001.md: -------------------------------------------------------------------------------- 1 | 作者:[@愚指导](https://x.com/yudao1024) 2 | 3 | --- 4 | 5 | # payable 可能导致资金被锁 6 | 7 | ## 漏洞说明 8 | 9 | 漏洞在代码 [PositionManager.sol#L61](https://github.com/WTFAcademy/WTF-Dapp/blob/main/demo-contract/contracts/wtfswap/PositionManager.sol#L61) 处。 10 | 11 | ```solidity 12 | function mint( 13 | MintParams calldata params 14 | ) 15 | external 16 | payable 17 | override 18 | checkDeadline(params.deadline) 19 | returns ( 20 | uint256 positionId, 21 | uint128 liquidity, 22 | uint256 amount0, 23 | uint256 amount1 24 | ) 25 | { 26 | ``` 27 | 28 | 因为我们的合约并不支持原生代币的交易,所以也没有做原生代币的提取,我们的合约中的 `payable` 其实是没有必要的,相反它还可能会导致意外转入原生代币到合约之后无法提取。 29 | 30 | ## 修复方法 31 | 32 | 修复方法如下,去掉 `payable` 即可。 33 | 34 | ```diff 35 | function mint( 36 | MintParams calldata params 37 | ) 38 | external 39 | - payable 40 | override 41 | checkDeadline(params.deadline) 42 | returns ( 43 | uint256 positionId, 44 | uint128 liquidity, 45 | uint256 amount0, 46 | uint256 amount1 47 | ) 48 | { 49 | ``` 50 | 51 | > 这个是漏洞提交的简单的示例模板,你可以提交 PR 来提交你发现的漏洞,帮助社区同学学习。 52 | -------------------------------------------------------------------------------- /P301_ContractOptimize/bugs/002.md: -------------------------------------------------------------------------------- 1 | 作者:[@Ethan](https://x.com/SnowS39053) 2 | 3 | --- 4 | 5 | # getAllPools 方法可能出现数据覆盖 6 | 7 | ## 漏洞说明 8 | 9 | 漏洞在代码 [PoolManager.sol#L36](https://github.com/WTFAcademy/WTF-Dapp/blob/main/demo-contract/contracts/wtfswap/PoolManager.sol#L36) 处。 10 | 11 | ```solidity 12 | poolsInfo = new PoolInfo[](length); 13 | for (uint32 i = 0; i < pairs.length; i++) { 14 | address[] memory addresses = pools[pairs[i].token0][ 15 | pairs[i].token1 16 | ]; 17 | for (uint32 j = 0; j < addresses.length; j++) { 18 | IPool pool = IPool(addresses[j]); 19 | poolsInfo[i + j] = PoolInfo({ 20 | pool: addresses[j], 21 | token0: pool.token0(), 22 | token1: pool.token1(), 23 | index: j, 24 | fee: pool.fee(), 25 | feeProtocol: 0, 26 | tickLower: pool.tickLower(), 27 | tickUpper: pool.tickUpper(), 28 | tick: pool.tick(), 29 | sqrtPriceX96: pool.sqrtPriceX96(), 30 | liquidity: pool.liquidity() 31 | }); 32 | } 33 | } 34 | return poolsInfo; 35 | ``` 36 | 37 | 当执行 `poolsInfo[i + j] = ...` 对数组进行赋值时, `i=2; j=0` 会将 `i=0;j=2` 时的数据覆盖 38 | 39 | ## 问题测试 40 | 部分测试函数如下: 41 | 42 | ```solidity 43 | modifier poolCreated() { 44 | address pool1 = poolManager.createAndInitializePoolIfNecessary(params1); 45 | address pool2 = poolManager.createAndInitializePoolIfNecessary(params2); 46 | address pool3 = poolManager.createAndInitializePoolIfNecessary(params3); 47 | _; 48 | } 49 | 50 | function comparePoolInfoWithParams( 51 | PoolManager.PoolInfo calldata poolInfo, 52 | IPoolManager.CreateAndInitializeParams calldata params 53 | ) public pure returns(bool ok) { 54 | ok = poolInfo.token0 == params.token0 && 55 | poolInfo.token1 == params.token1 && 56 | poolInfo.fee == params.fee && 57 | poolInfo.tickLower == params.tickLower && 58 | poolInfo.tickUpper == params.tickUpper; 59 | } 60 | 61 | function testGetAllPools() public poolCreated { 62 | PoolManager.PoolInfo[] memory poolsInfo = poolManager.getAllPools(); 63 | assertEq(poolsInfo.length, 3); 64 | 65 | IPoolManager.CreateAndInitializeParams[3] memory multiParams = [params1, params2, params3]; 66 | console2.log(multiParams[0].token0, multiParams[1].token0, multiParams[2].token0); 67 | for (uint24 i=0; i < poolsInfo.length; i++) { 68 | assert(this.comparePoolInfoWithParams(poolsInfo[i], multiParams[i])); 69 | } 70 | } 71 | ``` 72 | 按照上述测试流程, 新建 pool 并通过 `getAllPools` 方法来获取 pool 信息, 与预期的参数进行比较, 将得到错误的比较结果 73 | 74 | ## 修复方法 75 | 76 | 两层 for 循环实际上已经获取了每个 pair 对应的 pool 数量, 只需要一个变量记录循环执行时的下标即可. 修复代码如下: 77 | 78 | ```diff 79 | poolsInfo = new PoolInfo[](length); 80 | + uint256 index; 81 | for (uint32 i = 0; i < pairs.length; i++) { 82 | address[] memory addresses = pools[pairs[i].token0][ 83 | pairs[i].token1 84 | ]; 85 | for (uint32 j = 0; j < addresses.length; j++) { 86 | IPool pool = IPool(addresses[j]); 87 | - poolsInfo[i + j] = PoolInfo({ 88 | + poolsInfo[index] = PoolInfo({ 89 | pool: addresses[j], 90 | token0: pool.token0(), 91 | token1: pool.token1(), 92 | index: j, 93 | fee: pool.fee(), 94 | feeProtocol: 0, 95 | tickLower: pool.tickLower(), 96 | tickUpper: pool.tickUpper(), 97 | tick: pool.tick(), 98 | sqrtPriceX96: pool.sqrtPriceX96(), 99 | liquidity: pool.liquidity() 100 | }); 101 | + index++; 102 | } 103 | } 104 | return poolsInfo; 105 | ``` 106 | -------------------------------------------------------------------------------- /P301_ContractOptimize/img/aiscan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/P301_ContractOptimize/img/aiscan.png -------------------------------------------------------------------------------- /P301_ContractOptimize/img/payable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/P301_ContractOptimize/img/payable.png -------------------------------------------------------------------------------- /P301_ContractOptimize/img/reentrancy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/P301_ContractOptimize/img/reentrancy.png -------------------------------------------------------------------------------- /P301_ContractOptimize/readme.md: -------------------------------------------------------------------------------- 1 | 本节作者:[@愚指导](https://x.com/yudao1024) 2 | 3 | 漏洞列表贡献:[@愚指导](https://x.com/yudao1024)、[@Ethan](https://x.com/SnowS39053) 4 | 5 | 这一讲会简单讲一下合约安全,也欢迎大家提交本课程的合约漏洞帮助完善该文档。 6 | 7 | --- 8 | 9 | ## 关于合约安全和优化 10 | 11 | 合约安全是 DApp 开发中非常重要的一个环节,智能合约的漏洞可能会导致资产的损失,因此在开发合约时需要格外小心。因为大部分区块链的合约一旦部署就无法更新,只能重新部署。而且合约代码通常都是开源的,所以一旦部署了有问题的合约就容易被黑客利用,或者资产被意外冻结在合约中,也可能会产生非预期的错误结果。 12 | 13 | 所以合约安全变得很重要,通常来说合约在正式发布前都会经过专业的团队进行安全审计,产出安全报告后才能发布。 14 | 更多的合约安全的内容大家可以参考 WTF Academy 的[《合约安全》](https://github.com/AmazingAng/WTF-Solidity?tab=readme-ov-file#%E5%90%88%E7%BA%A6%E5%AE%89%E5%85%A8)部分课程。 15 | 16 | 除了合约的安全外,合约的优化也很重要。因为合约的执行需要消耗 GAS,而合约发布之后就无法再更新代码,所以合约上线前对合约的代码做优化也很重要。 17 | 18 | ## 使用 AI 来审计合约 19 | 20 | 合约的审计目前来说还是需要专家团队审核来保障,但是也有一些 AI 工具可以做一些审计,这也是对合约进行初步审查避免一些初级和常见问题的有效手段。 21 | 22 | 我们这里尝试使用 ZAN 的 [AI Scan](https://zan.top/cn/home/ai-scan) 的服务来做一个简单的审计,如下图所示,我们提交了一个审计任务。 23 | 24 | ![AI Scan](./img/aiscan.png) 25 | 26 | 最后我们会得到一个报告,该报告基于一些已有的只能合约漏洞数据库生成,提供了一些可能的漏洞和建议。比如下图所示,因为我们的合约并不支持原生代币的交易,所以也没有做原生代币的提取,我们的合约中的 `payable` 其实是没有必要的,相反它还可能会导致意外转入原生代币到合约之后无法提取。 27 | 28 | ![AI Scan](./img/payable.png) 29 | 30 | 另外还有类似重入攻击、溢出攻击等常见的可能的漏洞: 31 | 32 | ![Reentrancy](./img/reentrancy.png) 33 | 34 | 完整的报告你可以通过 [https://zan.top/review/reports/public/51d6c489-8b2e-4f9a-b132-9916a30083b0](https://zan.top/review/reports/public/51d6c489-8b2e-4f9a-b132-9916a30083b0) 查看。 35 | 36 | ## Wtfswap 合约漏洞和优化案例收集 37 | 38 | 当然,AI 来审计合约目前还无法完全的保证合约的安全性,很多逻辑尤其是涉及到具体的业务逻辑时,都需要类似 [ZAN Expert Audit](https://zan.top/home/expert-audit) 这样的服务来请求安全专家对合约进行审查。比如 [Bevmswap-20240328.pdf](https://mdn.alipayobjects.com/huamei_hsbbrh/afts/file/A*hU0NTIk3I-4AAAAAAAAAAAAADiOMAQ/Bevmswap-20240328.pdf) 就是一个专家审计的报告。 39 | 40 | 因为资源原因我们也无法对 Wtfswap 合约进行完整的审查,在此欢迎社区给我们的合约提交漏洞。 41 | 42 | 你可以直接提交 [Pull Request](https://github.com/WTFAcademy/WTF-Dapp/pulls) 来提交你发现的漏洞,新增漏洞文件到 [bugs](./bugs/) 帮助社区同学学习。 43 | 44 | 漏洞列表: 45 | 46 | 1. [《payable 可能导致资金被锁》](./bugs/001.md) 47 | 2. [《getAllPools 方法存在数据覆盖的问题》](./bugs/002.md) 48 | 49 | 上面是当前已经发现的 Wtfswap 合约的漏洞或者优化点,欢迎社区同学提交补充更多的案例。 50 | -------------------------------------------------------------------------------- /P302_Deploy/img/contract.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/P302_Deploy/img/contract.png -------------------------------------------------------------------------------- /P302_Deploy/img/verify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/P302_Deploy/img/verify.png -------------------------------------------------------------------------------- /T001_WtfswapDemo/img/faucet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/T001_WtfswapDemo/img/faucet.png -------------------------------------------------------------------------------- /T001_WtfswapDemo/img/swap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/T001_WtfswapDemo/img/swap.png -------------------------------------------------------------------------------- /T001_WtfswapDemo/img/testtoken.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/T001_WtfswapDemo/img/testtoken.png -------------------------------------------------------------------------------- /T001_WtfswapDemo/readme.md: -------------------------------------------------------------------------------- 1 | 本节作者:[@愚指导](https://x.com/yudao1024) 2 | 3 | 这一篇文章我们将会指导你如何体验部署到测试网的 [WTFSwap Demo](https://wtf-dapp.vercel.app/wtfswap)。 4 | 5 | --- 6 | 7 | ## 1. 领取测试网代币 8 | 9 | WTFSwap Demo 是部署在测试网 Sepolia 上的一个去中心化交易所课程 Demo。在体验之前,你需要领取一些测试网 Sepolia ETH 代币作为手续费 GAS。 10 | 11 | 你可以通过 [ZAN 的水龙头](https://zan.top/faucet?chInfo=wtf) 来领取测试网代币。 12 | 13 | ![faucet](./img/faucet.png) 14 | 15 | 请在上图示意的水龙头网页中填入钱包地址,领取 0.01 SepoliaETH。 16 | 17 | 什么?你还没有钱包地址?那你可以选择 [MetaMask](https://metamask.io/)、[TokenPocket](https://www.tokenpocket.pro/)、[imToken](https://token.im/) 等,这里就不做赘述了,赶紧自己去搞一个,加入 Web3 吧! 18 | 19 | ## 2. 领取测试用的代币 20 | 21 | 访问 WTFSwap 的测试网地址:[https://wtf-dapp.vercel.app/wtfswap](https://wtf-dapp.vercel.app/wtfswap),链接钱包后你可以看到交易框下面的测试代币水龙头: 22 | 23 | ![testtoken](./img/testtoken.png) 24 | 25 | 点击可以领取我们已经部署好的三个测试代币 DTA、DTB、DTC,每次领取 10 个,领取后上面的代币余额会增加(可能需要等待一会,会自动更新)。 26 | 27 | ## 3. 交易 28 | 29 | 领取好测试代币后就可以交易了。选择任意交易对(但是记得不要选相同的代币),在上面的输入框或者下面的输入框输入指定的代币数量,DApp 会自动预估交易的代币数量。如果是在上面输入,那么下面会更新为指定输入后获得多少代币。如果是在下面输入,那么上面会更新为指定输入需要支付多少代币。 30 | 31 | ![swap](./img/swap.png) 32 | 33 | 预估结果出来之后点击 Swap 按钮就可以发起交易了,交易会在测试网上进行,欢迎体验。另外如果你感兴趣可以尝试添加交易对或者注入流动性。 34 | 35 | 下面是部署在测试网上的各个合约地址: 36 | 37 | - Wtfswap#PoolManager - [0xF35DE8597A617cfA23de794BCBB4c2f8fc9bC896](https://sepolia.etherscan.io/address/0xF35DE8597A617cfA23de794BCBB4c2f8fc9bC896) 38 | - Wtfswap#PositionManager - [0x59ebEa058E193B64f0E091220d5Db98288EFec57](https://sepolia.etherscan.io/address/0x59ebEa058E193B64f0E091220d5Db98288EFec57) 39 | - Wtfswap#SwapRouter - [0xA8b9Fa84A4Df935e768d3cC211E3d679027d0B31](https://sepolia.etherscan.io/address/0xA8b9Fa84A4Df935e768d3cC211E3d679027d0B31) 40 | - DebugToken#DebugTokenA - [0x5AAB2806D12E380c24C640a8Cd94906d7fA59b16](https://sepolia.etherscan.io/address/0x5AAB2806D12E380c24C640a8Cd94906d7fA59b16) 41 | - DebugToken#DebugTokenB - [0x00E6EC12a0Fc35d7064cD0d551Ac74A02bA8a5A5](https://sepolia.etherscan.io/address/0x00E6EC12a0Fc35d7064cD0d551Ac74A02bA8a5A5) 42 | - DebugToken#DebugTokenC - [0x1D46AD43cc80BFb66C1D574d2B0E4abab191d1E0](https://sepolia.etherscan.io/address/0x1D46AD43cc80BFb66C1D574d2B0E4abab191d1E0) 43 | 44 | > 需要注意的是,WTFSwap 目前仅作为课程使用,可能存在很多问题,如果你在体验过程中遇到问题,欢迎提 [issue](https://github.com/WTFAcademy/WTF-Dapp/issues) 或者给[《合约优化和安全》](../P301_ContractOptimize/readme.md) 这一章提交内容。 45 | -------------------------------------------------------------------------------- /demo-contract/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | 4 | # Hardhat files 5 | /cache 6 | /artifacts 7 | 8 | # TypeChain files 9 | /typechain 10 | /typechain-types 11 | 12 | # solidity-coverage files 13 | /coverage 14 | /coverage.json 15 | 16 | # Hardhat Ignition default folder for deployments against a local node 17 | ignition/deployments/ -------------------------------------------------------------------------------- /demo-contract/README.md: -------------------------------------------------------------------------------- 1 | # Sample Hardhat Project 2 | 3 | This project demonstrates a basic Hardhat use case. It comes with a sample contract, a test for that contract, and a Hardhat Ignition module that deploys that contract. 4 | 5 | Try running some of the following tasks: 6 | 7 | ```shell 8 | npx hardhat help 9 | npx hardhat test 10 | REPORT_GAS=true npx hardhat test 11 | npx hardhat node 12 | npx hardhat ignition deploy ./ignition/modules/Lock.ts 13 | ``` 14 | -------------------------------------------------------------------------------- /demo-contract/contracts/Lock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.24; 3 | 4 | // Uncomment this line to use console.log 5 | // import "hardhat/console.sol"; 6 | 7 | contract Lock { 8 | uint public unlockTime; 9 | address payable public owner; 10 | 11 | event Withdrawal(uint amount, uint when); 12 | 13 | constructor(uint _unlockTime) payable { 14 | require( 15 | block.timestamp < _unlockTime, 16 | "Unlock time should be in the future" 17 | ); 18 | 19 | unlockTime = _unlockTime; 20 | owner = payable(msg.sender); 21 | } 22 | 23 | function withdraw() public { 24 | // Uncomment this line, and the import of "hardhat/console.sol", to print a log in your terminal 25 | // console.log("Unlock time is %o and block timestamp is %o", unlockTime, block.timestamp); 26 | 27 | require(block.timestamp >= unlockTime, "You can't withdraw yet"); 28 | require(msg.sender == owner, "You aren't the owner"); 29 | 30 | emit Withdrawal(address(this).balance, block.timestamp); 31 | 32 | owner.transfer(address(this).balance); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /demo-contract/contracts/MyToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | 7 | contract MyToken is ERC721, Ownable { 8 | uint256 private _nextTokenId = 0; 9 | 10 | constructor() 11 | ERC721("MyToken", "MTK") 12 | Ownable(msg.sender) 13 | {} 14 | 15 | function mint(uint256 quantity) public payable { 16 | require(quantity == 1, "quantity must be 1"); 17 | require(msg.value == 0.01 ether, "must pay 0.01 ether"); 18 | uint256 tokenId = _nextTokenId++; 19 | _mint(msg.sender, tokenId); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /demo-contract/contracts/wtfswap/Factory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.24; 3 | 4 | import "./interfaces/IFactory.sol"; 5 | import "./Pool.sol"; 6 | 7 | contract Factory is IFactory { 8 | mapping(address => mapping(address => address[])) public pools; 9 | 10 | Parameters public override parameters; 11 | 12 | function sortToken( 13 | address tokenA, 14 | address tokenB 15 | ) private pure returns (address, address) { 16 | return tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); 17 | } 18 | 19 | function getPool( 20 | address tokenA, 21 | address tokenB, 22 | uint32 index 23 | ) external view override returns (address) { 24 | require(tokenA != tokenB, "IDENTICAL_ADDRESSES"); 25 | require(tokenA != address(0) && tokenB != address(0), "ZERO_ADDRESS"); 26 | 27 | // Declare token0 and token1 28 | address token0; 29 | address token1; 30 | 31 | (token0, token1) = sortToken(tokenA, tokenB); 32 | 33 | return pools[token0][token1][index]; 34 | } 35 | 36 | function createPool( 37 | address tokenA, 38 | address tokenB, 39 | int24 tickLower, 40 | int24 tickUpper, 41 | uint24 fee 42 | ) external override returns (address pool) { 43 | // validate token's individuality 44 | require(tokenA != tokenB, "IDENTICAL_ADDRESSES"); 45 | 46 | // Declare token0 and token1 47 | address token0; 48 | address token1; 49 | 50 | // sort token, avoid the mistake of the order 51 | (token0, token1) = sortToken(tokenA, tokenB); 52 | 53 | // get current all pools 54 | address[] memory existingPools = pools[token0][token1]; 55 | 56 | // check if the pool already exists 57 | for (uint256 i = 0; i < existingPools.length; i++) { 58 | IPool currentPool = IPool(existingPools[i]); 59 | 60 | if ( 61 | currentPool.tickLower() == tickLower && 62 | currentPool.tickUpper() == tickUpper && 63 | currentPool.fee() == fee 64 | ) { 65 | return existingPools[i]; 66 | } 67 | } 68 | 69 | // save pool info 70 | parameters = Parameters( 71 | address(this), 72 | token0, 73 | token1, 74 | tickLower, 75 | tickUpper, 76 | fee 77 | ); 78 | 79 | // generate create2 salt 80 | bytes32 salt = keccak256( 81 | abi.encode(token0, token1, tickLower, tickUpper, fee) 82 | ); 83 | 84 | // create pool 85 | pool = address(new Pool{salt: salt}()); 86 | 87 | // save created pool 88 | pools[token0][token1].push(pool); 89 | 90 | // delete pool info 91 | delete parameters; 92 | 93 | emit PoolCreated( 94 | token0, 95 | token1, 96 | uint32(existingPools.length), 97 | tickLower, 98 | tickUpper, 99 | fee, 100 | pool 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /demo-contract/contracts/wtfswap/PoolManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.24; 3 | pragma abicoder v2; 4 | 5 | import "./interfaces/IPoolManager.sol"; 6 | import "./Factory.sol"; 7 | import "./interfaces/IPool.sol"; 8 | 9 | contract PoolManager is Factory, IPoolManager { 10 | Pair[] public pairs; 11 | 12 | function getPairs() external view override returns (Pair[] memory) { 13 | return pairs; 14 | } 15 | 16 | function getAllPools() 17 | external 18 | view 19 | override 20 | returns (PoolInfo[] memory poolsInfo) 21 | { 22 | uint32 length = 0; 23 | // 先算一下大小 24 | for (uint32 i = 0; i < pairs.length; i++) { 25 | length += uint32(pools[pairs[i].token0][pairs[i].token1].length); 26 | } 27 | 28 | // 再填充数据 29 | poolsInfo = new PoolInfo[](length); 30 | uint256 index; 31 | for (uint32 i = 0; i < pairs.length; i++) { 32 | address[] memory addresses = pools[pairs[i].token0][ 33 | pairs[i].token1 34 | ]; 35 | for (uint32 j = 0; j < addresses.length; j++) { 36 | IPool pool = IPool(addresses[j]); 37 | poolsInfo[index] = PoolInfo({ 38 | pool: addresses[j], 39 | token0: pool.token0(), 40 | token1: pool.token1(), 41 | index: j, 42 | fee: pool.fee(), 43 | feeProtocol: 0, 44 | tickLower: pool.tickLower(), 45 | tickUpper: pool.tickUpper(), 46 | tick: pool.tick(), 47 | sqrtPriceX96: pool.sqrtPriceX96(), 48 | liquidity: pool.liquidity() 49 | }); 50 | index++; 51 | } 52 | } 53 | return poolsInfo; 54 | } 55 | 56 | function createAndInitializePoolIfNecessary( 57 | CreateAndInitializeParams calldata params 58 | ) external payable override returns (address poolAddress) { 59 | require( 60 | params.token0 < params.token1, 61 | "token0 must be less than token1" 62 | ); 63 | 64 | poolAddress = this.createPool( 65 | params.token0, 66 | params.token1, 67 | params.tickLower, 68 | params.tickUpper, 69 | params.fee 70 | ); 71 | 72 | IPool pool = IPool(poolAddress); 73 | 74 | uint256 index = pools[pool.token0()][pool.token1()].length; 75 | 76 | // 新创建的池子,没有初始化价格,需要初始化价格 77 | if (pool.sqrtPriceX96() == 0) { 78 | pool.initialize(params.sqrtPriceX96); 79 | 80 | if (index == 1) { 81 | // 如果是第一次添加该交易对,需要记录 82 | pairs.push( 83 | Pair({token0: pool.token0(), token1: pool.token1()}) 84 | ); 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /demo-contract/contracts/wtfswap/interfaces/IFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.24; 3 | 4 | interface IFactory { 5 | struct Parameters { 6 | address factory; 7 | address tokenA; 8 | address tokenB; 9 | int24 tickLower; 10 | int24 tickUpper; 11 | uint24 fee; 12 | } 13 | 14 | function parameters() 15 | external 16 | view 17 | returns ( 18 | address factory, 19 | address tokenA, 20 | address tokenB, 21 | int24 tickLower, 22 | int24 tickUpper, 23 | uint24 fee 24 | ); 25 | 26 | event PoolCreated( 27 | address token0, 28 | address token1, 29 | uint32 index, 30 | int24 tickLower, 31 | int24 tickUpper, 32 | uint24 fee, 33 | address pool 34 | ); 35 | 36 | function getPool( 37 | address tokenA, 38 | address tokenB, 39 | uint32 index 40 | ) external view returns (address pool); 41 | 42 | function createPool( 43 | address tokenA, 44 | address tokenB, 45 | int24 tickLower, 46 | int24 tickUpper, 47 | uint24 fee 48 | ) external returns (address pool); 49 | } 50 | -------------------------------------------------------------------------------- /demo-contract/contracts/wtfswap/interfaces/IPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.24; 3 | 4 | interface IMintCallback { 5 | function mintCallback( 6 | uint256 amount0Owed, 7 | uint256 amount1Owed, 8 | bytes calldata data 9 | ) external; 10 | } 11 | 12 | interface ISwapCallback { 13 | function swapCallback( 14 | int256 amount0Delta, 15 | int256 amount1Delta, 16 | bytes calldata data 17 | ) external; 18 | } 19 | 20 | interface IPool { 21 | function factory() external view returns (address); 22 | 23 | function token0() external view returns (address); 24 | 25 | function token1() external view returns (address); 26 | 27 | function fee() external view returns (uint24); 28 | 29 | function tickLower() external view returns (int24); 30 | 31 | function tickUpper() external view returns (int24); 32 | 33 | function sqrtPriceX96() external view returns (uint160); 34 | 35 | function tick() external view returns (int24); 36 | 37 | function liquidity() external view returns (uint128); 38 | 39 | function initialize(uint160 sqrtPriceX96) external; 40 | 41 | /// feeGrowthGlobal0X128 记录从创建到现在,每个流动性累计产生的 token0 的手续费 42 | /// @notice The fee growth as a Q128.128 fees of token0 collected per unit of liquidity for the entire life of the pool 43 | /// @dev This value can overflow the uint256 44 | function feeGrowthGlobal0X128() external view returns (uint256); 45 | 46 | /// feeGrowthGlobal1X128 记录从创建到现在,每个流动性累计产生的 token1 的手续费 47 | /// @notice The fee growth as a Q128.128 fees of token1 collected per unit of liquidity for the entire life of the pool 48 | /// @dev This value can overflow the uint256 49 | function feeGrowthGlobal1X128() external view returns (uint256); 50 | 51 | function getPosition( 52 | address owner 53 | ) 54 | external 55 | view 56 | returns ( 57 | uint128 _liquidity, 58 | uint256 feeGrowthInside0LastX128, 59 | uint256 feeGrowthInside1LastX128, 60 | uint128 tokensOwed0, 61 | uint128 tokensOwed1 62 | ); 63 | 64 | event Mint( 65 | address sender, 66 | address indexed owner, 67 | uint128 amount, 68 | uint256 amount0, 69 | uint256 amount1 70 | ); 71 | 72 | function mint( 73 | address recipient, 74 | uint128 amount, 75 | bytes calldata data 76 | ) external returns (uint256 amount0, uint256 amount1); 77 | 78 | event Collect( 79 | address indexed owner, 80 | address recipient, 81 | uint128 amount0, 82 | uint128 amount1 83 | ); 84 | 85 | function collect( 86 | address recipient, 87 | uint128 amount0Requested, 88 | uint128 amount1Requested 89 | ) external returns (uint128 amount0, uint128 amount1); 90 | 91 | event Burn( 92 | address indexed owner, 93 | uint128 amount, 94 | uint256 amount0, 95 | uint256 amount1 96 | ); 97 | 98 | function burn( 99 | uint128 amount 100 | ) external returns (uint256 amount0, uint256 amount1); 101 | 102 | event Swap( 103 | address indexed sender, 104 | address indexed recipient, 105 | int256 amount0, 106 | int256 amount1, 107 | uint160 sqrtPriceX96, 108 | uint128 liquidity, 109 | int24 tick 110 | ); 111 | 112 | function swap( 113 | address recipient, 114 | bool zeroForOne, 115 | int256 amountSpecified, 116 | uint160 sqrtPriceLimitX96, 117 | bytes calldata data 118 | ) external returns (int256 amount0, int256 amount1); 119 | } 120 | -------------------------------------------------------------------------------- /demo-contract/contracts/wtfswap/interfaces/IPoolManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.24; 3 | pragma abicoder v2; 4 | 5 | import "./IFactory.sol"; 6 | 7 | interface IPoolManager is IFactory { 8 | struct PoolInfo { 9 | address pool; 10 | address token0; 11 | address token1; 12 | uint32 index; 13 | uint24 fee; 14 | uint8 feeProtocol; 15 | int24 tickLower; 16 | int24 tickUpper; 17 | int24 tick; 18 | uint160 sqrtPriceX96; 19 | uint128 liquidity; 20 | } 21 | 22 | struct Pair { 23 | address token0; 24 | address token1; 25 | } 26 | 27 | function getPairs() external view returns (Pair[] memory); 28 | 29 | function getAllPools() external view returns (PoolInfo[] memory poolsInfo); 30 | 31 | struct CreateAndInitializeParams { 32 | address token0; 33 | address token1; 34 | uint24 fee; 35 | int24 tickLower; 36 | int24 tickUpper; 37 | uint160 sqrtPriceX96; 38 | } 39 | 40 | function createAndInitializePoolIfNecessary( 41 | CreateAndInitializeParams calldata params 42 | ) external payable returns (address pool); 43 | } 44 | -------------------------------------------------------------------------------- /demo-contract/contracts/wtfswap/interfaces/IPositionManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.24; 3 | pragma abicoder v2; 4 | 5 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 6 | 7 | interface IPositionManager is IERC721 { 8 | struct PositionInfo { 9 | uint256 id; 10 | address owner; 11 | address token0; 12 | address token1; 13 | uint32 index; 14 | uint24 fee; 15 | uint128 liquidity; 16 | int24 tickLower; 17 | int24 tickUpper; 18 | uint128 tokensOwed0; 19 | uint128 tokensOwed1; 20 | // feeGrowthInside0LastX128 和 feeGrowthInside1LastX128 用于计算手续费 21 | uint256 feeGrowthInside0LastX128; 22 | uint256 feeGrowthInside1LastX128; 23 | } 24 | 25 | function getAllPositions() 26 | external 27 | view 28 | returns (PositionInfo[] memory positionInfo); 29 | 30 | struct MintParams { 31 | address token0; 32 | address token1; 33 | uint32 index; 34 | uint256 amount0Desired; 35 | uint256 amount1Desired; 36 | address recipient; 37 | uint256 deadline; 38 | } 39 | 40 | function mint( 41 | MintParams calldata params 42 | ) 43 | external 44 | payable 45 | returns ( 46 | uint256 positionId, 47 | uint128 liquidity, 48 | uint256 amount0, 49 | uint256 amount1 50 | ); 51 | 52 | function burn( 53 | uint256 positionId 54 | ) external returns (uint256 amount0, uint256 amount1); 55 | 56 | function collect( 57 | uint256 positionId, 58 | address recipient 59 | ) external returns (uint256 amount0, uint256 amount1); 60 | 61 | function mintCallback( 62 | uint256 amount0, 63 | uint256 amount1, 64 | bytes calldata data 65 | ) external; 66 | } 67 | -------------------------------------------------------------------------------- /demo-contract/contracts/wtfswap/interfaces/ISwapRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.24; 3 | pragma abicoder v2; 4 | 5 | import "./IPool.sol"; 6 | 7 | interface ISwapRouter is ISwapCallback { 8 | event Swap( 9 | address indexed sender, 10 | bool zeroForOne, 11 | uint256 amountIn, 12 | uint256 amountInRemaining, 13 | uint256 amountOut 14 | ); 15 | 16 | struct ExactInputParams { 17 | address tokenIn; 18 | address tokenOut; 19 | uint32[] indexPath; 20 | address recipient; 21 | uint256 deadline; 22 | uint256 amountIn; 23 | uint256 amountOutMinimum; 24 | uint160 sqrtPriceLimitX96; 25 | } 26 | 27 | function exactInput( 28 | ExactInputParams calldata params 29 | ) external payable returns (uint256 amountOut); 30 | 31 | struct ExactOutputParams { 32 | address tokenIn; 33 | address tokenOut; 34 | uint32[] indexPath; 35 | address recipient; 36 | uint256 deadline; 37 | uint256 amountOut; 38 | uint256 amountInMaximum; 39 | uint160 sqrtPriceLimitX96; 40 | } 41 | 42 | function exactOutput( 43 | ExactOutputParams calldata params 44 | ) external payable returns (uint256 amountIn); 45 | 46 | struct QuoteExactInputParams { 47 | address tokenIn; 48 | address tokenOut; 49 | uint32[] indexPath; 50 | uint256 amountIn; 51 | uint160 sqrtPriceLimitX96; 52 | } 53 | 54 | function quoteExactInput( 55 | QuoteExactInputParams calldata params 56 | ) external returns (uint256 amountOut); 57 | 58 | struct QuoteExactOutputParams { 59 | address tokenIn; 60 | address tokenOut; 61 | uint32[] indexPath; 62 | uint256 amountOut; 63 | uint160 sqrtPriceLimitX96; 64 | } 65 | 66 | function quoteExactOutput( 67 | QuoteExactOutputParams calldata params 68 | ) external returns (uint256 amountIn); 69 | } 70 | -------------------------------------------------------------------------------- /demo-contract/contracts/wtfswap/libraries/BitMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | /// @title BitMath 5 | /// @dev This library provides functionality for computing bit properties of an unsigned integer 6 | /// @author Solady (https://github.com/Vectorized/solady/blob/8200a70e8dc2a77ecb074fc2e99a2a0d36547522/src/utils/LibBit.sol) 7 | library BitMath { 8 | /// @notice Returns the index of the most significant bit of the number, 9 | /// where the least significant bit is at index 0 and the most significant bit is at index 255 10 | /// @param x the value for which to compute the most significant bit, must be greater than 0 11 | /// @return r the index of the most significant bit 12 | function mostSignificantBit(uint256 x) internal pure returns (uint8 r) { 13 | require(x > 0); 14 | 15 | assembly ("memory-safe") { 16 | r := shl(7, lt(0xffffffffffffffffffffffffffffffff, x)) 17 | r := or(r, shl(6, lt(0xffffffffffffffff, shr(r, x)))) 18 | r := or(r, shl(5, lt(0xffffffff, shr(r, x)))) 19 | r := or(r, shl(4, lt(0xffff, shr(r, x)))) 20 | r := or(r, shl(3, lt(0xff, shr(r, x)))) 21 | // forgefmt: disable-next-item 22 | r := or( 23 | r, 24 | byte( 25 | and( 26 | 0x1f, 27 | shr(shr(r, x), 0x8421084210842108cc6318c6db6d54be) 28 | ), 29 | 0x0706060506020500060203020504000106050205030304010505030400000000 30 | ) 31 | ) 32 | } 33 | } 34 | 35 | /// @notice Returns the index of the least significant bit of the number, 36 | /// where the least significant bit is at index 0 and the most significant bit is at index 255 37 | /// @param x the value for which to compute the least significant bit, must be greater than 0 38 | /// @return r the index of the least significant bit 39 | function leastSignificantBit(uint256 x) internal pure returns (uint8 r) { 40 | require(x > 0); 41 | 42 | assembly ("memory-safe") { 43 | // Isolate the least significant bit. 44 | x := and(x, sub(0, x)) 45 | // For the upper 3 bits of the result, use a De Bruijn-like lookup. 46 | // Credit to adhusson: https://blog.adhusson.com/cheap-find-first-set-evm/ 47 | // forgefmt: disable-next-item 48 | r := shl( 49 | 5, 50 | shr( 51 | 252, 52 | shl( 53 | shl( 54 | 2, 55 | shr( 56 | 250, 57 | mul( 58 | x, 59 | 0xb6db6db6ddddddddd34d34d349249249210842108c6318c639ce739cffffffff 60 | ) 61 | ) 62 | ), 63 | 0x8040405543005266443200005020610674053026020000107506200176117077 64 | ) 65 | ) 66 | ) 67 | // For the lower 5 bits of the result, use a De Bruijn lookup. 68 | // forgefmt: disable-next-item 69 | r := or( 70 | r, 71 | byte( 72 | and(div(0xd76453e0, shr(r, x)), 0x1f), 73 | 0x001f0d1e100c1d070f090b19131c1706010e11080a1a141802121b1503160405 74 | ) 75 | ) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /demo-contract/contracts/wtfswap/libraries/FixedPoint128.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.4.0; 3 | 4 | /// @title FixedPoint128 5 | /// @notice A library for handling binary fixed point numbers, see https://en.wikipedia.org/wiki/Q_(number_format) 6 | library FixedPoint128 { 7 | uint256 internal constant Q128 = 0x100000000000000000000000000000000; 8 | } 9 | -------------------------------------------------------------------------------- /demo-contract/contracts/wtfswap/libraries/FixedPoint96.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.4.0; 3 | 4 | /// @title FixedPoint96 5 | /// @notice A library for handling binary fixed point numbers, see https://en.wikipedia.org/wiki/Q_(number_format) 6 | /// @dev Used in SqrtPriceMath.sol 7 | library FixedPoint96 { 8 | uint8 internal constant RESOLUTION = 96; 9 | uint256 internal constant Q96 = 0x1000000000000000000000000; 10 | } 11 | -------------------------------------------------------------------------------- /demo-contract/contracts/wtfswap/libraries/LiquidityMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.5.0; 3 | 4 | /// @title Math library for liquidity 5 | library LiquidityMath { 6 | /// @notice Add a signed liquidity delta to liquidity and revert if it overflows or underflows 7 | /// @param x The liquidity before change 8 | /// @param y The delta by which liquidity should be changed 9 | /// @return z The liquidity delta 10 | function addDelta(uint128 x, int128 y) internal pure returns (uint128 z) { 11 | if (y < 0) { 12 | require((z = x - uint128(-y)) < x, 'LS'); 13 | } else { 14 | require((z = x + uint128(y)) >= x, 'LA'); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /demo-contract/contracts/wtfswap/libraries/LowGasSafeMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.7.0; 3 | 4 | /// @title Optimized overflow and underflow safe math operations 5 | /// @notice Contains methods for doing math operations that revert on overflow or underflow for minimal gas cost 6 | library LowGasSafeMath { 7 | /// @notice Returns x + y, reverts if sum overflows uint256 8 | /// @param x The augend 9 | /// @param y The addend 10 | /// @return z The sum of x and y 11 | function add(uint256 x, uint256 y) internal pure returns (uint256 z) { 12 | require((z = x + y) >= x); 13 | } 14 | 15 | /// @notice Returns x - y, reverts if underflows 16 | /// @param x The minuend 17 | /// @param y The subtrahend 18 | /// @return z The difference of x and y 19 | function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { 20 | require((z = x - y) <= x); 21 | } 22 | 23 | /// @notice Returns x * y, reverts if overflows 24 | /// @param x The multiplicand 25 | /// @param y The multiplier 26 | /// @return z The product of x and y 27 | function mul(uint256 x, uint256 y) internal pure returns (uint256 z) { 28 | require(x == 0 || (z = x * y) / x == y); 29 | } 30 | 31 | /// @notice Returns x + y, reverts if overflows or underflows 32 | /// @param x The augend 33 | /// @param y The addend 34 | /// @return z The sum of x and y 35 | function add(int256 x, int256 y) internal pure returns (int256 z) { 36 | require((z = x + y) >= x == (y >= 0)); 37 | } 38 | 39 | /// @notice Returns x - y, reverts if overflows or underflows 40 | /// @param x The minuend 41 | /// @param y The subtrahend 42 | /// @return z The difference of x and y 43 | function sub(int256 x, int256 y) internal pure returns (int256 z) { 44 | require((z = x - y) <= x == (y >= 0)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /demo-contract/contracts/wtfswap/libraries/SafeCast.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.5.0; 3 | 4 | /// @title Safe casting methods 5 | /// @notice Contains methods for safely casting between types 6 | library SafeCast { 7 | /// @notice Cast a uint256 to a uint160, revert on overflow 8 | /// @param y The uint256 to be downcasted 9 | /// @return z The downcasted integer, now type uint160 10 | function toUint160(uint256 y) internal pure returns (uint160 z) { 11 | require((z = uint160(y)) == y); 12 | } 13 | 14 | /// @notice Cast a int256 to a int128, revert on overflow or underflow 15 | /// @param y The int256 to be downcasted 16 | /// @return z The downcasted integer, now type int128 17 | function toInt128(int256 y) internal pure returns (int128 z) { 18 | require((z = int128(y)) == y); 19 | } 20 | 21 | /// @notice Cast a uint256 to a int256, revert on overflow 22 | /// @param y The uint256 to be casted 23 | /// @return z The casted integer, now type int256 24 | function toInt256(uint256 y) internal pure returns (int256 z) { 25 | require(y < 2 ** 255); 26 | z = int256(y); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /demo-contract/contracts/wtfswap/libraries/TransferHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.6.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | /// @title TransferHelper 7 | /// @notice Contains helper methods for interacting with ERC20 tokens that do not consistently return true/false 8 | library TransferHelper { 9 | /// @notice Transfers tokens from msg.sender to a recipient 10 | /// @dev Calls transfer on token contract, errors with TF if transfer fails 11 | /// @param token The contract address of the token which will be transferred 12 | /// @param to The recipient of the transfer 13 | /// @param value The value of the transfer 14 | function safeTransfer(address token, address to, uint256 value) internal { 15 | (bool success, bytes memory data) = token.call( 16 | abi.encodeWithSelector(IERC20.transfer.selector, to, value) 17 | ); 18 | require( 19 | success && (data.length == 0 || abi.decode(data, (bool))), 20 | "TF" 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /demo-contract/contracts/wtfswap/libraries/UnsafeMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.5.0; 3 | 4 | /// @title Math functions that do not check inputs or outputs 5 | /// @notice Contains methods that perform common math functions but do not do any overflow or underflow checks 6 | library UnsafeMath { 7 | /// @notice Returns ceil(x / y) 8 | /// @dev division by 0 has unspecified behavior, and must be checked externally 9 | /// @param x The dividend 10 | /// @param y The divisor 11 | /// @return z The quotient, ceil(x / y) 12 | function divRoundingUp(uint256 x, uint256 y) internal pure returns (uint256 z) { 13 | assembly { 14 | z := add(div(x, y), gt(mod(x, y), 0)) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /demo-contract/contracts/wtfswap/test-contracts/DebugToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract DebugToken is ERC20 { 7 | uint256 private _nextTokenId = 0; 8 | 9 | constructor(string memory name, string memory symbol) ERC20(name, symbol) {} 10 | 11 | function mint(address recipient, uint256 quantity) public payable { 12 | require(quantity > 0, "quantity must be greater than 0"); 13 | require( 14 | quantity < 10000000 * 10 ** 18, 15 | "quantity must be less than 10000000 * 10 ** 18" 16 | ); 17 | _mint(recipient, quantity); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /demo-contract/contracts/wtfswap/test-contracts/TestLP.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | import "../interfaces/IPool.sol"; 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | contract TestLP is IMintCallback { 7 | function sortToken( 8 | address tokenA, 9 | address tokenB 10 | ) private pure returns (address, address) { 11 | return tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); 12 | } 13 | 14 | function mint( 15 | address recipient, 16 | uint128 amount, 17 | address pool, 18 | address tokenA, 19 | address tokenB 20 | ) external returns (uint256 amount0, uint256 amount1) { 21 | (address token0, address token1) = sortToken(tokenA, tokenB); 22 | 23 | (amount0, amount1) = IPool(pool).mint( 24 | recipient, 25 | amount, 26 | abi.encode(token0, token1) 27 | ); 28 | } 29 | 30 | function burn( 31 | uint128 amount, 32 | address pool 33 | ) external returns (uint256 amount0, uint256 amount1) { 34 | (amount0, amount1) = IPool(pool).burn(amount); 35 | } 36 | 37 | function collect( 38 | address recipient, 39 | address pool 40 | ) external returns (uint256 amount0, uint256 amount1) { 41 | (, , , uint128 tokensOwed0, uint128 tokensOwed1) = IPool(pool) 42 | .getPosition(address(this)); 43 | (amount0, amount1) = IPool(pool).collect( 44 | recipient, 45 | tokensOwed0, 46 | tokensOwed1 47 | ); 48 | } 49 | 50 | function mintCallback( 51 | uint256 amount0Owed, 52 | uint256 amount1Owed, 53 | bytes calldata data 54 | ) external override { 55 | // transfer token 56 | (address token0, address token1) = abi.decode(data, (address, address)); 57 | if (amount0Owed > 0) { 58 | IERC20(token0).transfer(msg.sender, amount0Owed); 59 | } 60 | if (amount1Owed > 0) { 61 | IERC20(token1).transfer(msg.sender, amount1Owed); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /demo-contract/contracts/wtfswap/test-contracts/TestSwap.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | import "../interfaces/IPool.sol"; 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | contract TestSwap is ISwapCallback { 7 | function testSwap( 8 | address recipient, 9 | int256 amount, 10 | uint160 sqrtPriceLimitX96, 11 | address pool, 12 | address token0, 13 | address token1 14 | ) external returns (int256 amount0, int256 amount1) { 15 | (amount0, amount1) = IPool(pool).swap( 16 | recipient, 17 | true, 18 | amount, 19 | sqrtPriceLimitX96, 20 | abi.encode(token0, token1) 21 | ); 22 | } 23 | 24 | function swapCallback( 25 | int256 amount0Delta, 26 | int256 amount1Delta, 27 | bytes calldata data 28 | ) external { 29 | // transfer token 30 | (address token0, address token1) = abi.decode(data, (address, address)); 31 | if (amount0Delta > 0) { 32 | IERC20(token0).transfer(msg.sender, uint(amount0Delta)); 33 | } 34 | if (amount1Delta > 0) { 35 | IERC20(token1).transfer(msg.sender, uint(amount1Delta)); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /demo-contract/contracts/wtfswap/test-contracts/TestToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract TestToken is ERC20 { 7 | uint256 private _nextTokenId = 0; 8 | 9 | constructor() ERC20("TestToken", "TK") {} 10 | 11 | function mint(address recipient, uint256 quantity) public payable { 12 | _mint(recipient, quantity); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /demo-contract/hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import { HardhatUserConfig } from "hardhat/config"; 2 | import "@nomicfoundation/hardhat-toolbox-viem"; 3 | 4 | const config: HardhatUserConfig = { 5 | solidity: { 6 | version: "0.8.24", 7 | settings: { 8 | optimizer: { 9 | enabled: true, 10 | runs: 200, 11 | }, 12 | }, 13 | }, 14 | networks: { 15 | sepolia: { 16 | url: "https://api.zan.top/public/eth-sepolia", // 实际项目中需要替换为你的 ZAN 的 RPC 地址,这里用的是测试用的公共地址,可能不稳定 17 | accounts: ["YOUR_PRIVATE_KEY"], // 替换为你的钱包私钥 18 | }, 19 | }, 20 | etherscan: { 21 | apiKey: { 22 | sepolia: "YOUR_ETHERSCAN_API_KEY", // 替换为你的 Etherscan API Key 23 | }, 24 | }, 25 | }; 26 | 27 | export default config; 28 | -------------------------------------------------------------------------------- /demo-contract/ignition/modules/DebugToken.ts: -------------------------------------------------------------------------------- 1 | import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; 2 | 3 | const DebugTokenModule = buildModule("DebugToken", (m) => { 4 | const debugTokenA = m.contract("DebugToken", ["DebugTokenA", "DTA"], { 5 | id: "DebugTokenA", 6 | }); 7 | const debugTokenB = m.contract("DebugToken", ["DebugTokenB", "DTB"], { 8 | id: "DebugTokenB", 9 | }); 10 | const debugTokenC = m.contract("DebugToken", ["DebugTokenC", "DTC"], { 11 | id: "DebugTokenC", 12 | }); 13 | 14 | return { 15 | debugTokenA, 16 | debugTokenB, 17 | debugTokenC, 18 | }; 19 | }); 20 | 21 | export default DebugTokenModule; 22 | -------------------------------------------------------------------------------- /demo-contract/ignition/modules/Lock.ts: -------------------------------------------------------------------------------- 1 | import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; 2 | import { parseEther } from "viem"; 3 | 4 | const JAN_1ST_2030 = 1893456000; 5 | const ONE_GWEI: bigint = parseEther("0.001"); 6 | 7 | const LockModule = buildModule("LockModule", (m) => { 8 | const unlockTime = m.getParameter("unlockTime", JAN_1ST_2030); 9 | const lockedAmount = m.getParameter("lockedAmount", ONE_GWEI); 10 | 11 | const lock = m.contract("Lock", [unlockTime], { 12 | value: lockedAmount, 13 | }); 14 | 15 | return { lock }; 16 | }); 17 | 18 | export default LockModule; 19 | -------------------------------------------------------------------------------- /demo-contract/ignition/modules/MyToken.ts: -------------------------------------------------------------------------------- 1 | import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; 2 | 3 | const MyTokenModule = buildModule("MyTokenModule", (m) => { 4 | const lock = m.contract("MyToken"); 5 | 6 | return { lock }; 7 | }); 8 | 9 | export default MyTokenModule; 10 | -------------------------------------------------------------------------------- /demo-contract/ignition/modules/Wtfswap.ts: -------------------------------------------------------------------------------- 1 | import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; 2 | 3 | const WtfswapModule = buildModule("Wtfswap", (m) => { 4 | const poolManager = m.contract("PoolManager"); 5 | const swapRouter = m.contract("SwapRouter", [poolManager]); 6 | const positionManager = m.contract("PositionManager", [poolManager]); 7 | 8 | return { 9 | poolManager, 10 | swapRouter, 11 | positionManager, 12 | }; 13 | }); 14 | 15 | export default WtfswapModule; 16 | -------------------------------------------------------------------------------- /demo-contract/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hardhat-project", 3 | "devDependencies": { 4 | "@nomicfoundation/hardhat-toolbox-viem": "^3.0.0", 5 | "@uniswap/v3-sdk": "^3.17.1", 6 | "hardhat": "^2.22.3" 7 | }, 8 | "dependencies": { 9 | "@openzeppelin/contracts": "^5.0.2" 10 | }, 11 | "repository": "git@github.com:WTFAcademy/WTF-Dapp.git" 12 | } 13 | -------------------------------------------------------------------------------- /demo-contract/test/MyToken.ts: -------------------------------------------------------------------------------- 1 | import { loadFixture } from "@nomicfoundation/hardhat-toolbox-viem/network-helpers"; 2 | import { expect } from "chai"; 3 | import hre from "hardhat"; 4 | 5 | describe("MyToken", function () { 6 | async function deployFixture() { 7 | const token = await hre.viem.deployContract("MyToken"); 8 | return { 9 | token, 10 | }; 11 | } 12 | 13 | describe("ERC721", function () { 14 | describe("name", function () { 15 | it("Get NFT name", async function () { 16 | const { token } = await loadFixture(deployFixture); 17 | expect(await token.read.name()).to.equal("MyToken"); 18 | }); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /demo-contract/test/wtfswap/Factory.ts: -------------------------------------------------------------------------------- 1 | import { loadFixture } from "@nomicfoundation/hardhat-toolbox-viem/network-helpers"; 2 | import { expect } from "chai"; 3 | import hre from "hardhat"; 4 | 5 | describe("Factory", function () { 6 | async function deployFixture() { 7 | const factory = await hre.viem.deployContract("Factory"); 8 | const publicClient = await hre.viem.getPublicClient(); 9 | return { 10 | factory, 11 | publicClient, 12 | }; 13 | } 14 | 15 | it("createPool", async function () { 16 | const { factory, publicClient } = await loadFixture(deployFixture); 17 | const tokenA: `0x${string}` = "0xEcd0D12E21805803f70de03B72B1C162dB0898d9"; 18 | const tokenB: `0x${string}` = "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"; 19 | 20 | const hash = await factory.write.createPool([ 21 | tokenA, 22 | tokenB, 23 | 1, 24 | 100000, 25 | 3000, 26 | ]); 27 | await publicClient.waitForTransactionReceipt({ hash }); 28 | const createEvents = await factory.getEvents.PoolCreated(); 29 | expect(createEvents).to.have.lengthOf(1); 30 | expect(createEvents[0].args.pool).to.match(/^0x[a-fA-F0-9]{40}$/); 31 | expect(createEvents[0].args.token0).to.equal(tokenB); 32 | expect(createEvents[0].args.token1).to.equal(tokenA); 33 | expect(createEvents[0].args.tickLower).to.equal(1); 34 | expect(createEvents[0].args.tickUpper).to.equal(100000); 35 | expect(createEvents[0].args.fee).to.equal(3000); 36 | 37 | // simulate for test return address 38 | const poolAddress = await factory.simulate.createPool([ 39 | tokenA, 40 | tokenB, 41 | 1, 42 | 100000, 43 | 3000, 44 | ]); 45 | expect(poolAddress.result).to.match(/^0x[a-fA-F0-9]{40}$/); 46 | expect(poolAddress.result).to.equal(createEvents[0].args.pool); 47 | }); 48 | 49 | it("createPool with same token", async function () { 50 | const { factory } = await loadFixture(deployFixture); 51 | const tokenA: `0x${string}` = "0xEcd0D12E21805803f70de03B72B1C162dB0898d9"; 52 | const tokenB: `0x${string}` = "0xEcd0D12E21805803f70de03B72B1C162dB0898d9"; 53 | await expect( 54 | factory.write.createPool([tokenA, tokenB, 1, 100000, 3000]) 55 | ).to.be.rejectedWith("IDENTICAL_ADDRESSES"); 56 | 57 | await expect(factory.read.getPool([tokenA, tokenB, 3])).to.be.rejectedWith( 58 | "IDENTICAL_ADDRESSES" 59 | ); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /demo-contract/test/wtfswap/PoolManager.ts: -------------------------------------------------------------------------------- 1 | import { loadFixture } from "@nomicfoundation/hardhat-toolbox-viem/network-helpers"; 2 | import { expect } from "chai"; 3 | import hre from "hardhat"; 4 | import { TickMath, encodeSqrtRatioX96 } from "@uniswap/v3-sdk"; 5 | 6 | describe("PoolManager", function () { 7 | async function deployFixture() { 8 | const manager = await hre.viem.deployContract("PoolManager"); 9 | const publicClient = await hre.viem.getPublicClient(); 10 | return { 11 | manager, 12 | publicClient, 13 | }; 14 | } 15 | 16 | it("getPairs & getAllPools", async function () { 17 | const { manager } = await loadFixture(deployFixture); 18 | const tokenA: `0x${string}` = "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"; 19 | const tokenB: `0x${string}` = "0xEcd0D12E21805803f70de03B72B1C162dB0898d9"; 20 | const tokenC: `0x${string}` = "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"; 21 | const tokenD: `0x${string}` = "0x6B175474E89094C44Da98b954EedeAC495271d0F"; 22 | 23 | // 创建 tokenA-tokenB 24 | await manager.write.createAndInitializePoolIfNecessary([ 25 | { 26 | token0: tokenA, 27 | token1: tokenB, 28 | fee: 3000, 29 | tickLower: TickMath.getTickAtSqrtRatio(encodeSqrtRatioX96(1, 1)), 30 | tickUpper: TickMath.getTickAtSqrtRatio(encodeSqrtRatioX96(10000, 1)), 31 | sqrtPriceX96: BigInt(encodeSqrtRatioX96(100, 1).toString()), 32 | }, 33 | ]); 34 | 35 | // 由于和前一个参数一样,会被合并 36 | await manager.write.createAndInitializePoolIfNecessary([ 37 | { 38 | token0: tokenA, 39 | token1: tokenB, 40 | fee: 3000, 41 | tickLower: TickMath.getTickAtSqrtRatio(encodeSqrtRatioX96(1, 1)), 42 | tickUpper: TickMath.getTickAtSqrtRatio(encodeSqrtRatioX96(10000, 1)), 43 | sqrtPriceX96: BigInt(encodeSqrtRatioX96(100, 1).toString()), 44 | }, 45 | ]); 46 | 47 | await manager.write.createAndInitializePoolIfNecessary([ 48 | { 49 | token0: tokenC, 50 | token1: tokenD, 51 | fee: 2000, 52 | tickLower: TickMath.getTickAtSqrtRatio(encodeSqrtRatioX96(100, 1)), 53 | tickUpper: TickMath.getTickAtSqrtRatio(encodeSqrtRatioX96(5000, 1)), 54 | sqrtPriceX96: BigInt(encodeSqrtRatioX96(200, 1).toString()), 55 | }, 56 | ]); 57 | 58 | // 判断返回的 pairs 的数量是否正确 59 | const pairs = await manager.read.getPairs(); 60 | expect(pairs.length).to.equal(2); 61 | 62 | // 判断返回的 pools 的数量、参数是否正确 63 | const pools = await manager.read.getAllPools(); 64 | expect(pools.length).to.equal(2); 65 | expect(pools[0].token0).to.equal(tokenA); 66 | expect(pools[0].token1).to.equal(tokenB); 67 | expect(pools[0].sqrtPriceX96).to.equal( 68 | BigInt(encodeSqrtRatioX96(100, 1).toString()) 69 | ); 70 | expect(pools[1].token0).to.equal(tokenC); 71 | expect(pools[1].token1).to.equal(tokenD); 72 | expect(pools[1].sqrtPriceX96).to.equal( 73 | BigInt(encodeSqrtRatioX96(200, 1).toString()) 74 | ); 75 | }); 76 | 77 | it("require token0 < token1", async function () { 78 | const { manager } = await loadFixture(deployFixture); 79 | const tokenA: `0x${string}` = "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"; 80 | const tokenB: `0x${string}` = "0xEcd0D12E21805803f70de03B72B1C162dB0898d9"; 81 | 82 | await expect( 83 | manager.write.createAndInitializePoolIfNecessary([ 84 | { 85 | token0: tokenB, 86 | token1: tokenA, 87 | fee: 3000, 88 | tickLower: TickMath.getTickAtSqrtRatio(encodeSqrtRatioX96(1, 1)), 89 | tickUpper: TickMath.getTickAtSqrtRatio(encodeSqrtRatioX96(10000, 1)), 90 | sqrtPriceX96: BigInt(encodeSqrtRatioX96(100, 1).toString()), 91 | }, 92 | ]) 93 | ).to.be.rejected; 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /demo-contract/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /demo/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 20 | 21 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 22 | 23 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 24 | 25 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 26 | 27 | ## Learn More 28 | 29 | To learn more about Next.js, take a look at the following resources: 30 | 31 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 32 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 33 | 34 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 35 | 36 | ## Deploy on Vercel 37 | 38 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 39 | 40 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 41 | -------------------------------------------------------------------------------- /demo/components/AddPoolModal/index.tsx: -------------------------------------------------------------------------------- 1 | import { Modal, Form, Input, InputNumber, Select, message } from "antd"; 2 | import { parsePriceToSqrtPriceX96, getContractAddress } from "@/utils/common"; 3 | 4 | interface CreatePoolParams { 5 | token0: `0x${string}`; 6 | token1: `0x${string}`; 7 | fee: number; 8 | tickLower: number; 9 | tickUpper: number; 10 | sqrtPriceX96: bigint; 11 | } 12 | 13 | interface AddPoolModalProps { 14 | open: boolean; 15 | onCancel: () => void; 16 | onCreatePool: (params: CreatePoolParams) => void; 17 | } 18 | 19 | export default function AddPoolModal(props: AddPoolModalProps) { 20 | const { open, onCancel, onCreatePool } = props; 21 | const [form] = Form.useForm(); 22 | 23 | return ( 24 | { 30 | const values = await form.validateFields().then((values) => { 31 | if (values.token0 >= values.token1) { 32 | message.error("Token0 should be less than Token1"); 33 | return false; 34 | } 35 | onCreatePool({ 36 | ...values, 37 | sqrtPriceX96: parsePriceToSqrtPriceX96(values.price), 38 | }); 39 | }); 40 | }} 41 | > 42 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /demo/components/AddPositionModal/index.tsx: -------------------------------------------------------------------------------- 1 | import { Modal, Form, Input, InputNumber, message } from "antd"; 2 | import { getContractAddress } from "@/utils/common"; 3 | 4 | interface CreatePositionParams { 5 | token0: `0x${string}`; 6 | token1: `0x${string}`; 7 | index: number; 8 | amount0Desired: bigint; 9 | amount1Desired: bigint; 10 | recipient: string; 11 | deadline: bigint; 12 | } 13 | 14 | interface AddPositionModalProps { 15 | open: boolean; 16 | onCancel: () => void; 17 | onCreatePosition: (params: CreatePositionParams) => void; 18 | } 19 | 20 | export default function AddPositionModal(props: AddPositionModalProps) { 21 | const { open, onCancel, onCreatePosition } = props; 22 | const [form] = Form.useForm(); 23 | 24 | return ( 25 | { 31 | form.validateFields().then((values) => { 32 | if (values.token0 >= values.token1) { 33 | message.error("Token0 must be less than Token1"); 34 | return; 35 | } 36 | onCreatePosition({ 37 | ...values, 38 | amount0Desired: BigInt(values.amount0Desired), 39 | amount1Desired: BigInt(values.amount1Desired), 40 | deadline: BigInt(Date.now() + 100000), 41 | }); 42 | }); 43 | }} 44 | > 45 |
56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 |
73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /demo/components/Balance.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import type { Token } from "@ant-design/web3"; 3 | import { CryptoPrice } from "@ant-design/web3"; 4 | import { useReadErc20BalanceOf } from "@/utils/contracts"; 5 | import { useAccount } from "wagmi"; 6 | import useTokenAddress from "@/hooks/useTokenAddress"; 7 | 8 | interface Props { 9 | token?: Token; 10 | } 11 | 12 | export default function Balance(props: Props) { 13 | const { address } = useAccount(); 14 | const tokenAddress = useTokenAddress(props.token); 15 | const { data: balance } = useReadErc20BalanceOf({ 16 | address: tokenAddress, 17 | args: [address as `0x${string}`], 18 | query: { 19 | enabled: !!tokenAddress, 20 | // 每 3 秒刷新一次 21 | refetchInterval: 3000, 22 | }, 23 | }); 24 | 25 | return balance === undefined ? ( 26 | "-" 27 | ) : ( 28 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /demo/components/Faucet.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Flex, Space, message, Divider } from "antd"; 3 | import { useAccount } from "@ant-design/web3"; 4 | import { useWriteDebugTokenMint } from "@/utils/contracts"; 5 | import { getContractAddress } from "@/utils/common"; 6 | 7 | export default function Faucet() { 8 | const { account } = useAccount(); 9 | const [loading, setLoading] = React.useState(false); 10 | const { writeContractAsync } = useWriteDebugTokenMint(); 11 | 12 | const claim = async (address: `0x${string}`, name: string) => { 13 | if (loading) { 14 | return; 15 | } 16 | try { 17 | setLoading(true); 18 | await writeContractAsync({ 19 | address, 20 | // default to 10 TestToken 21 | args: [ 22 | account?.address as `0x${string}`, 23 | BigInt("10000000000000000000"), 24 | ], 25 | }); 26 | message.success(`Claim 10 ${name} success`); 27 | } catch (error: any) { 28 | message.error(error.message); 29 | } finally { 30 | setLoading(false); 31 | } 32 | }; 33 | 34 | return ( 35 | <> 36 | 37 | 38 |
领取测试代币:
39 | {loading ? ( 40 | "Claiming..." 41 | ) : ( 42 | 43 | { 46 | claim(getContractAddress("DebugTokenA"), "DTA"); 47 | }} 48 | > 49 | DTA 50 | 51 | { 54 | claim(getContractAddress("DebugTokenB"), "DTB"); 55 | }} 56 | > 57 | DTB 58 | 59 | { 62 | claim(getContractAddress("DebugTokenC"), "DTC"); 63 | }} 64 | > 65 | DTC 66 | 67 | 68 | )} 69 |
70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /demo/components/SendEth/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Button, Checkbox, Form, type FormProps, Input } from 'antd'; 3 | import { type BaseError, useSendTransaction, useWaitForTransactionReceipt} from 'wagmi'; 4 | import { parseEther } from 'viem'; 5 | 6 | type FieldType = { 7 | to: `0x${string}`; 8 | value: string; 9 | }; 10 | 11 | export const SendEth:React.FC = () => { 12 | const { data: hash, error, isPending, sendTransaction } = useSendTransaction(); 13 | 14 | const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ hash }); 15 | 16 | const onFinish: FormProps["onFinish"] = (values) => { 17 | console.log('Success:', values); 18 | sendTransaction({ to: values.to, value: parseEther(values.value) }) 19 | }; 20 | 21 | const onFinishFailed: FormProps["onFinishFailed"] = (errorInfo) => { 22 | console.log('Failed:', errorInfo); 23 | }; 24 | 25 | return ( 26 |
36 | 37 | label="to" 38 | name="to" 39 | rules={[{ required: true, message: 'Please input!' }]} 40 | > 41 | 42 | 43 | 44 | 45 | label="value" 46 | name="value" 47 | rules={[{ required: true, message: 'Please input!' }]} 48 | > 49 | 50 | 51 | 52 | 53 | 56 | 57 | 58 | {hash &&
Transaction Hash: {hash}
} 59 | {isConfirming &&
Waiting for confirmation...
} 60 | {isConfirmed &&
Transaction confirmed.
} 61 | {error && ( 62 |
Error: {(error as BaseError).shortMessage || error.message}
63 | )} 64 | 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /demo/components/SignDemo/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ConnectButton, Connector, useAccount } from "@ant-design/web3"; 3 | import { useSignMessage } from "wagmi"; 4 | import { message, Space, Button } from "antd"; 5 | 6 | const SignDemo: React.FC = () => { 7 | const { signMessageAsync } = useSignMessage(); 8 | const { account } = useAccount(); 9 | const [signLoading, setSignLoading] = React.useState(false); 10 | 11 | const doSignature = async () => { 12 | setSignLoading(true); 13 | try { 14 | const signature = await signMessageAsync({ 15 | message: "test message for WTF-DApp demo", 16 | }); 17 | await checkSignature({ 18 | address: account?.address, 19 | signature, 20 | }); 21 | } catch (error: any) { 22 | message.error(`Signature failed: ${error.message}`); 23 | } 24 | setSignLoading(false); 25 | }; 26 | 27 | const checkSignature = async (params: { 28 | address?: string; 29 | signature: string; 30 | }) => { 31 | try { 32 | const response = await fetch("/api/signatureCheck", { 33 | method: "POST", 34 | headers: { "Content-Type": "application/json" }, 35 | body: JSON.stringify(params), 36 | }); 37 | const result = await response.json(); 38 | if (result.data) { 39 | message.success("Signature success"); 40 | } else { 41 | message.error("Signature failed"); 42 | } 43 | } catch (error) { 44 | message.error("An error occurred"); 45 | } 46 | }; 47 | 48 | return ( 49 | 50 | 51 | 52 | 53 | 60 | 61 | ); 62 | }; 63 | export default SignDemo; 64 | -------------------------------------------------------------------------------- /demo/components/WtfLayout/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import { usePathname } from "next/navigation"; 4 | import { Connector, ConnectButton } from "@ant-design/web3"; 5 | import styles from "./styles.module.css"; 6 | 7 | export default function WtfHeader() { 8 | const pathname = usePathname(); 9 | const isSwapPage = pathname === "/wtfswap"; 10 | const [loading, setLoading] = React.useState(true); 11 | 12 | React.useEffect(() => { 13 | setLoading(false); 14 | }, []); 15 | 16 | return ( 17 |
18 |
WTFSwap
19 |
20 | 24 | Swap 25 | 26 | 30 | Pool 31 | 32 |
33 |
34 | {loading ? null : ( 35 | 40 | 41 | 42 | )} 43 |
44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /demo/components/WtfLayout/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Header from "./Header"; 3 | import styles from "./styles.module.css"; 4 | import { 5 | MetaMask, 6 | OkxWallet, 7 | TokenPocket, 8 | WagmiWeb3ConfigProvider, 9 | WalletConnect, 10 | Hardhat, 11 | Sepolia, 12 | } from "@ant-design/web3-wagmi"; 13 | import { useAccount, http } from "wagmi"; 14 | 15 | interface WtfLayoutProps { 16 | children: React.ReactNode; 17 | } 18 | 19 | const LayoutContent: React.FC = ({ children }) => { 20 | const { address } = useAccount(); 21 | const [loading, setLoading] = React.useState(true); 22 | 23 | React.useEffect(() => { 24 | setLoading(false); 25 | }, []); 26 | 27 | if (loading || !address) { 28 | return
Please Connect First.
; 29 | } 30 | return children; 31 | }; 32 | 33 | const WtfLayout: React.FC = ({ children }) => { 34 | return ( 35 | 57 |
58 |
59 | {children} 60 |
61 |
62 | ); 63 | }; 64 | 65 | export default WtfLayout; 66 | -------------------------------------------------------------------------------- /demo/components/WtfLayout/styles.module.css: -------------------------------------------------------------------------------- 1 | .layout { 2 | min-height: 100vh; 3 | background: linear-gradient(to bottom, #e8f1ff, #f6f7f9); 4 | } 5 | 6 | .header { 7 | height: 56px; 8 | line-height: 56px; 9 | padding-inline: 24px; 10 | background-color: #e8f1ff; 11 | display: flex; 12 | flex-direction: row; 13 | justify-content: space-between; 14 | border-bottom: 1px solid #d7e1eb; 15 | } 16 | 17 | .title { 18 | font-size: 16px; 19 | font-weight: bold; 20 | } 21 | 22 | .nav { 23 | display: flex; 24 | gap: 64px; 25 | margin-left: 180px; 26 | } 27 | 28 | .nav a { 29 | font-size: 14px; 30 | opacity: 0.65; 31 | } 32 | 33 | .nav a.active { 34 | font-weight: bold; 35 | opacity: 1; 36 | } 37 | 38 | .connectTip { 39 | padding: 24px; 40 | text-align: center; 41 | } 42 | -------------------------------------------------------------------------------- /demo/hooks/useTokenAddress.tsx: -------------------------------------------------------------------------------- 1 | import type { Token } from "@ant-design/web3"; 2 | import { useChainId } from "wagmi"; 3 | 4 | export default (token?: Token): `0x${string}` | undefined => { 5 | const chainId = useChainId(); 6 | return token?.availableChains.find((item) => item.chain.id === chainId) 7 | ?.contract as `0x${string}` | undefined; 8 | }; 9 | -------------------------------------------------------------------------------- /demo/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | transpilePackages: [ 5 | "@ant-design", 6 | "antd", 7 | "rc-util", 8 | "rc-pagination", 9 | "rc-picker", 10 | "rc-table", 11 | "rc-tree", 12 | "rc-input", 13 | ], 14 | }; 15 | 16 | module.exports = nextConfig; 17 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ant-design-web3-demo", 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 | "@ant-design/web3": "^1.20.0", 13 | "@ant-design/web3-wagmi": "^2.10.0", 14 | "@tanstack/react-query": "^5.28.4", 15 | "@uniswap/v3-sdk": "^3.17.1", 16 | "antd": "^5.21.1", 17 | "lodash-es": "^4.17.21", 18 | "next": "^14.0.4", 19 | "react": "^18", 20 | "react-dom": "^18", 21 | "viem": "^2.21.15", 22 | "wagmi": "^2.14.6" 23 | }, 24 | "devDependencies": { 25 | "@types/lodash-es": "^4.17.12", 26 | "@types/node": "20.16.9", 27 | "@types/react": "18.3.9", 28 | "@types/react-dom": "^18", 29 | "@wagmi/cli": "^2.1.15", 30 | "eslint": "^8", 31 | "eslint-config-next": "^14.0.4", 32 | "typescript": "5.6.2" 33 | }, 34 | "repository": "git@github.com:WTFAcademy/WTF-Dapp.git" 35 | } 36 | -------------------------------------------------------------------------------- /demo/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | 4 | export default function App({ Component, pageProps }: AppProps) { 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /demo/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /demo/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 | -------------------------------------------------------------------------------- /demo/pages/api/signatureCheck.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import { createPublicClient, http } from "viem"; 3 | import { mainnet } from "viem/chains"; 4 | 5 | export const publicClient = createPublicClient({ 6 | chain: mainnet, 7 | transport: http(), 8 | }); 9 | 10 | export default async function handler( 11 | req: NextApiRequest, 12 | res: NextApiResponse 13 | ) { 14 | try { 15 | const body = req.body; 16 | const valid = await publicClient.verifyMessage({ 17 | address: body.address, 18 | message: "test message for WTF-DApp demo", 19 | signature: body.signature, 20 | }); 21 | res.status(200).json({ data: valid }); 22 | } catch (err: any) { 23 | res.status(500).json({ error: err.message }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /demo/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import Image from "next/image"; 3 | import { Inter } from "next/font/google"; 4 | import styles from "@/styles/Home.module.css"; 5 | 6 | const inter = Inter({ subsets: ["latin"] }); 7 | 8 | export default function Home() { 9 | return ( 10 | <> 11 | 12 | WTF Academy DApp 极简入门教程 13 | 17 | 18 | 19 | 20 |
21 |
22 |

WTF Academy DApp 极简入门教程,帮助开发者入门去中心应用开发。

23 | 40 |
41 | 42 |
43 | 51 |
52 | 53 | 102 |
103 | 104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /demo/pages/sign/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { MetaMask, WagmiWeb3ConfigProvider } from "@ant-design/web3-wagmi"; 3 | import { createConfig, http } from "wagmi"; 4 | import { injected } from "wagmi/connectors"; 5 | import { mainnet } from "wagmi/chains"; 6 | import SignDemo from "../../components/SignDemo"; 7 | 8 | const config = createConfig({ 9 | chains: [mainnet], 10 | transports: { 11 | [mainnet.id]: http(), 12 | }, 13 | connectors: [ 14 | injected({ 15 | target: "metaMask", 16 | }), 17 | ], 18 | }); 19 | const Demo: React.FC = () => { 20 | return ( 21 | 22 | 23 | 24 | ); 25 | }; 26 | export default Demo; 27 | -------------------------------------------------------------------------------- /demo/pages/test.tsx: -------------------------------------------------------------------------------- 1 | import { useReadPoolManagerGetAllPools } from "@/utils/contracts"; 2 | 3 | import { hardhat } from "wagmi/chains"; 4 | import { WagmiWeb3ConfigProvider, Hardhat } from "@ant-design/web3-wagmi"; 5 | import { Button } from "antd"; 6 | import { createConfig, http } from "wagmi"; 7 | import { Connector, ConnectButton } from "@ant-design/web3"; 8 | 9 | const config = createConfig({ 10 | chains: [hardhat], 11 | transports: { 12 | [hardhat.id]: http("http://127.0.0.1:8545/"), 13 | }, 14 | }); 15 | 16 | const CallTest = () => { 17 | const { data, refetch } = useReadPoolManagerGetAllPools({ 18 | address: "0x5FbDB2315678afecb367f032d93F642f64180aa3", 19 | }); 20 | console.log("get data", data); 21 | return ( 22 | <> 23 | {data?.toString()} 24 | 31 | 32 | ); 33 | }; 34 | 35 | export default function Web3() { 36 | return ( 37 | 44 | 45 | 46 | 47 | 48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /demo/pages/transaction/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { MetaMask, WagmiWeb3ConfigProvider} from "@ant-design/web3-wagmi"; 3 | import { createConfig, http } from "wagmi"; 4 | import { injected } from "wagmi/connectors"; 5 | import { mainnet, sepolia } from "wagmi/chains"; 6 | import { ConnectButton, Connector } from '@ant-design/web3'; 7 | import { SendEth } from "../../components/SendEth"; 8 | 9 | 10 | const config = createConfig({ 11 | chains: [mainnet, sepolia], 12 | transports: { 13 | [mainnet.id]: http(), 14 | [sepolia.id]: http(), 15 | }, 16 | connectors: [ 17 | injected({ 18 | target: "metaMask", 19 | }), 20 | ], 21 | }); 22 | const TransactionDemo: React.FC = () => { 23 | 24 | return ( 25 | 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | }; 39 | export default TransactionDemo; 40 | -------------------------------------------------------------------------------- /demo/pages/web3.tsx: -------------------------------------------------------------------------------- 1 | import { mainnet, sepolia, polygon, hardhat } from "wagmi/chains"; 2 | import { 3 | WagmiWeb3ConfigProvider, 4 | MetaMask, 5 | Sepolia, 6 | Polygon, 7 | Hardhat, 8 | WalletConnect, 9 | } from "@ant-design/web3-wagmi"; 10 | import { 11 | Address, 12 | ConnectButton, 13 | Connector, 14 | NFTCard, 15 | useAccount, 16 | useProvider, 17 | } from "@ant-design/web3"; 18 | import { Button, message } from "antd"; 19 | import { parseEther } from "viem"; 20 | import { createConfig, http, useWatchContractEvent } from "wagmi"; 21 | import { injected, walletConnect } from "wagmi/connectors"; 22 | import { 23 | useReadMyTokenBalanceOf, 24 | useWriteMyTokenMint, 25 | } from "@/utils/contracts"; 26 | 27 | const config = createConfig({ 28 | chains: [mainnet, sepolia, polygon, hardhat], 29 | transports: { 30 | [mainnet.id]: http(), 31 | [sepolia.id]: http(), 32 | [polygon.id]: http(), 33 | [hardhat.id]: http("http://127.0.0.1:8545/"), 34 | }, 35 | connectors: [ 36 | injected({ 37 | target: "metaMask", 38 | }), 39 | walletConnect({ 40 | projectId: "c07c0051c2055890eade3556618e38a6", 41 | showQrModal: false, 42 | }), 43 | ], 44 | }); 45 | 46 | const contractInfo = [ 47 | { 48 | id: 1, 49 | name: "Ethereum", 50 | contractAddress: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", 51 | }, 52 | { 53 | id: 5, 54 | name: "Sepolia", 55 | contractAddress: "0x418325c3979b7f8a17678ec2463a74355bdbe72c", 56 | }, 57 | { 58 | id: 137, 59 | name: "Polygon", 60 | contractAddress: "0x418325c3979b7f8a17678ec2463a74355bdbe72c", 61 | }, 62 | { 63 | id: hardhat.id, 64 | name: "Hardhat", 65 | contractAddress: "0x5FbDB2315678afecb367f032d93F642f64180aa3", 66 | }, 67 | ]; 68 | 69 | const CallTest = () => { 70 | const { account } = useAccount(); 71 | const { chain } = useProvider(); 72 | const result = useReadMyTokenBalanceOf({ 73 | address: contractInfo.find((item) => item.id === chain?.id) 74 | ?.contractAddress as `0x${string}`, 75 | args: [account?.address as `0x${string}`], 76 | }); 77 | const { writeContract: mintNFT } = useWriteMyTokenMint(); 78 | 79 | useWatchContractEvent({ 80 | address: "0xEcd0D12E21805803f70de03B72B1C162dB0898d9", 81 | abi: [ 82 | { 83 | anonymous: false, 84 | inputs: [ 85 | { 86 | indexed: false, 87 | internalType: "address", 88 | name: "minter", 89 | type: "address", 90 | }, 91 | { 92 | indexed: false, 93 | internalType: "uint256", 94 | name: "amount", 95 | type: "uint256", 96 | }, 97 | ], 98 | name: "Minted", 99 | type: "event", 100 | }, 101 | ], 102 | eventName: "Minted", 103 | onLogs() { 104 | message.success("new minted!"); 105 | }, 106 | }); 107 | 108 | return ( 109 |
110 | {result.data?.toString()} 111 | 133 |
134 | ); 135 | }; 136 | 137 | export default function Web3() { 138 | return ( 139 | 147 |
148 | 152 | 153 | 154 | 155 | 156 | 157 | ); 158 | } 159 | -------------------------------------------------------------------------------- /demo/pages/wtfswap/debug.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button, message, Select, Card, Input, Space } from "antd"; 3 | import WtfLayout from "@/components/WtfLayout"; 4 | import { getContractAddress } from "@/utils/common"; 5 | import { useWriteDebugTokenMint } from "@/utils/contracts"; 6 | import { useAccount } from "@ant-design/web3"; 7 | 8 | const GetDebugToken: React.FC = () => { 9 | const { account } = useAccount(); 10 | const [tokenAddress, setTokenAddress] = React.useState<`0x${string}`>( 11 | getContractAddress("DebugTokenA") 12 | ); 13 | const [amount, setAmount] = React.useState("1000000000000000000"); 14 | const { writeContractAsync } = useWriteDebugTokenMint(); 15 | const [loading, setLoading] = React.useState(false); 16 | return ( 17 | 18 | 19 | 30 | setAmount(e.target.value)} 33 | placeholder="Amount" 34 | /> 35 |

36 | tokenAddress: {tokenAddress} 37 |
38 | amount: {amount.slice(0, -18) || "<1"} Token 39 |

40 | 64 |
65 |
66 | ); 67 | }; 68 | 69 | export default function WtfswapPool() { 70 | return ( 71 | 72 |
77 | 78 |
79 |
80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /demo/pages/wtfswap/pool.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin: 24px 148px; 3 | } 4 | -------------------------------------------------------------------------------- /demo/pages/wtfswap/positions.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin: 24px 148px; 3 | } 4 | -------------------------------------------------------------------------------- /demo/pages/wtfswap/swap.module.css: -------------------------------------------------------------------------------- 1 | .swapCard { 2 | width: 600px; 3 | margin: 0 auto; 4 | position: absolute; 5 | top: 50%; 6 | left: 50%; 7 | transform: translate(-50%, -50%); 8 | } 9 | 10 | .switchBtn { 11 | font-size: '20px'; 12 | display: flex; 13 | justify-content: center; 14 | margin: 16px 0; 15 | } 16 | 17 | .swapSpace { 18 | width: 100%; 19 | justify-content: space-between; 20 | } 21 | 22 | .swapBtn { 23 | margin-top: 10px; 24 | } -------------------------------------------------------------------------------- /demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/demo/public/favicon.ico -------------------------------------------------------------------------------- /demo/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/public/wtf-antdweb3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/demo/public/wtf-antdweb3.png -------------------------------------------------------------------------------- /demo/public/wtf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WTFAcademy/WTF-Dapp/9703ad24deeb7b83afa7ff2b5232a1070dcd31e9/demo/public/wtf.png -------------------------------------------------------------------------------- /demo/styles/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --max-width: 1100px; 3 | --border-radius: 12px; 4 | --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", 5 | "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", 6 | "Fira Mono", "Droid Sans Mono", "Courier New", monospace; 7 | 8 | --foreground-rgb: 0, 0, 0; 9 | --background-start-rgb: 214, 219, 220; 10 | --background-end-rgb: 255, 255, 255; 11 | 12 | --primary-glow: conic-gradient( 13 | from 180deg at 50% 50%, 14 | #16abff33 0deg, 15 | #0885ff33 55deg, 16 | #54d6ff33 120deg, 17 | #0071ff33 160deg, 18 | transparent 360deg 19 | ); 20 | --secondary-glow: radial-gradient( 21 | rgba(255, 255, 255, 1), 22 | rgba(255, 255, 255, 0) 23 | ); 24 | 25 | --tile-start-rgb: 239, 245, 249; 26 | --tile-end-rgb: 228, 232, 233; 27 | --tile-border: conic-gradient( 28 | #00000080, 29 | #00000040, 30 | #00000030, 31 | #00000020, 32 | #00000010, 33 | #00000010, 34 | #00000080 35 | ); 36 | 37 | --callout-rgb: 238, 240, 241; 38 | --callout-border-rgb: 172, 175, 176; 39 | --card-rgb: 180, 185, 188; 40 | --card-border-rgb: 131, 134, 135; 41 | } 42 | 43 | @media (prefers-color-scheme: dark) { 44 | :root { 45 | --foreground-rgb: 255, 255, 255; 46 | --background-start-rgb: 0, 0, 0; 47 | --background-end-rgb: 0, 0, 0; 48 | 49 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); 50 | --secondary-glow: linear-gradient( 51 | to bottom right, 52 | rgba(1, 65, 255, 0), 53 | rgba(1, 65, 255, 0), 54 | rgba(1, 65, 255, 0.3) 55 | ); 56 | 57 | --tile-start-rgb: 2, 13, 46; 58 | --tile-end-rgb: 2, 5, 19; 59 | --tile-border: conic-gradient( 60 | #ffffff80, 61 | #ffffff40, 62 | #ffffff30, 63 | #ffffff20, 64 | #ffffff10, 65 | #ffffff10, 66 | #ffffff80 67 | ); 68 | 69 | --callout-rgb: 20, 20, 20; 70 | --callout-border-rgb: 108, 108, 108; 71 | --card-rgb: 100, 100, 100; 72 | --card-border-rgb: 200, 200, 200; 73 | } 74 | } 75 | 76 | * { 77 | box-sizing: border-box; 78 | padding: 0; 79 | margin: 0; 80 | } 81 | 82 | html, 83 | body { 84 | max-width: 100vw; 85 | min-height: 100vh; 86 | overflow-x: hidden; 87 | } 88 | 89 | body { 90 | color: rgb(var(--foreground-rgb)); 91 | background: linear-gradient( 92 | to bottom, 93 | transparent, 94 | rgb(var(--background-end-rgb)) 95 | ) 96 | rgb(var(--background-start-rgb)); 97 | } 98 | 99 | a { 100 | color: inherit; 101 | text-decoration: none; 102 | } 103 | 104 | @media (prefers-color-scheme: dark) { 105 | html { 106 | color-scheme: dark; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "paths": { 17 | "@/*": ["./*"] 18 | }, 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ] 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules", "wagmi.config.ts"] 27 | } 28 | -------------------------------------------------------------------------------- /demo/wagmi.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@wagmi/cli"; 2 | import { hardhat } from "@wagmi/cli/plugins"; 3 | import { react } from "@wagmi/cli/plugins"; 4 | 5 | export default defineConfig({ 6 | out: "utils/contracts.ts", 7 | plugins: [ 8 | hardhat({ 9 | project: "../demo-contract", 10 | }), 11 | react(), 12 | ], 13 | }); 14 | --------------------------------------------------------------------------------