DApp基础学习(一)

这是WTF社区做的一个DApp的教程,有基础,感兴趣的可以学习:https://github.com/WTFAcademy/WTF-Dapp 文章相当于我的总结,开始教程前建议学习一下React。代码仓库:https://github.com/langjiyunmie/WTF-DAPP-STUDY

基本库的使用

基本库的认识

依据教程初期,我们需要了解这下面些库是干嘛的

  • wagmi:是一个专为 React 开发者设计的 Web3 工具库,它能快速连接以太坊等区块链网络,轻松实现钱包登录(如 MetaMask)、读取链上数据(如代币余额)、发送交易、与智能合约交互等功能
  • @ant-design/web3: 是一个 Web3 专用 UI 组件库,它通过适配器(Adapter)将 UI 组件(如 <ConnectButton />)与底层区块链工具库(如 wagmiethers.js 或 Solana SDK)解耦。适配器的作用是 定义统一的交互接口,让同一个组件无需修改代码,即可通过不同适配器调用对应区块链的操作逻辑(例如连接 Ethereum 的 MetaMask 或 Solana 的 Phantom 钱包)。实际区块链网络通信和钱包交互仍由底层库(如 wagmi)完成,适配器仅充当「翻译层」,让 UI 组件与多链生态兼容。
  • @ant-design/web3-wagmi:它是一个以太坊适配器,为 @ant-design/web3 提供了连接以太坊等 EVM 兼容链的能力。不需要开发者自己去处理处理组件的连接状态,链数据请求等逻辑。通过 [Web3ConfigProvider] 提供相关全局状态和接口

代码示例

import {
  Address,
  ConnectButton,
  Connector,
  NFTCard,
  useAccount,
  useProvider
} from "@ant-design/web3";
import {
  Sepolia,
  MetaMask,
  WagmiWeb3ConfigProvider,
  WalletConnect,
  Polygon
} from "@ant-design/web3-wagmi";
import { Button, message } from "antd";
import { parseEther } from "viem";
import { createConfig, http, useReadContract, useWriteContract } from "wagmi";
import { sepolia, mainnet,polygon } from "wagmi/chains";
import { injected , walletConnect} from "wagmi/connectors";

const config = createConfig({
  chains: [mainnet, sepolia,polygon],
  transports: {
    [mainnet.id]: http(),
    [sepolia.id]: http(),
    [polygon.id]: http(),
  },
  connectors: [
    injected({
      target: "metaMask",
    }),
    walletConnect({
      projectId:'c07c0051c2055890eade3556618e38a6',
      showQrModal:false
    }),
  ],
});

const contractInfo = [
  {
    id:1,
    name:"Ethereum",
    contractAddress:"0xEcd0D12E21805803f70de03B72B1C162dB0898d9",
  },
  {
    id:5,
    name:"Sepolia",
    contractAddress:"0x7e061614FAE039D59A6A1554886F1e235EEA6Fc1",
  },
  {
    id:137,
    name:"Polygon",
    contractAddress:"0x7e061614FAE039D59A6A1554886F1e235EEA6Fc1"
  }
]

const CallTest = () => {
  const { account } = useAccount();
  const { chain} = useProvider();
  const result = useReadContract({
    abi: [
      {
        type: "function",
        name: "balanceOf",
        stateMutability: "view",
        inputs: [{ name: "account", type: "address" }],
        outputs: [{ type: "uint256" }],
      },
    ],
      address: contractInfo.find( (item) => item.id === chain?.id)?.contractAddress as `0x${string}`,
      functionName: "balanceOf",
    args: [account?.address as `0x${string}`],
  });
  const { writeContract } = useWriteContract();

  return (
    <div>
      {result.data?.toString()}
      <Button
        onClick={() => {
          writeContract(
            {
              abi: [
                {
                  type: "function",
                  name: "mint",
                  stateMutability: "payable",
                  inputs: [
                    {
                      internalType: "uint256",
                      name: "quantity",
                      type: "uint256",
                    },
                  ],
                  outputs: [],
                },
              ],
              address: contractInfo.find((item) => item.id === chain?.id)?.contractAddress as `0x${string}`,
              functionName: "mint",
              args: [BigInt(1)],
              value: parseEther("0.01"),
            },
            {
              onSuccess: () => {
                message.success("Mint Success");
              },
              onError: (err) => {
                message.error(err.message);
              },
            }
          );  
        }}
      >
        mint
      </Button>
    </div>
  );
};

export default function Web3() {
  return (
    <WagmiWeb3ConfigProvider
      config={config}
      chains={[Sepolia,Polygon]}
      wallets={[MetaMask(), WalletConnect()]}
    >
      <Address format address="0xEcd0D12E21805803f70de03B72B1C162dB0898d9" />
      <NFTCard
        address="0xEcd0D12E21805803f70de03B72B1C162dB0898d9"
        tokenId={641}
      />
      <Connector>
        <ConnectButton />
      </Connector>
      <CallTest />
    </WagmiWeb3ConfigProvider>
  );
}

config声明

chains

声明 DApp 支持哪些区块链网络,从 “wagmi/chains” 导入链的信息

import { sepolia, mainnet,polygon } from "wagmi/chains";
import { http } from "wagmi";
 chains: [mainnet, sepolia,polygon],
transports

为每个链指定 RPC 节点的通信方式(默认用 HTTP 协议连接公共节点)

transports: {
    [mainnet.id]: http(),
    [sepolia.id]: http(),
    [polygon.id]: http(),
  },

http() 内可以填入你的RPC节点信息

http("https://eth-mainnet.g.alchemy.com/v2/your-api-key")
connectors
import { injected , walletConnect} from "wagmi/connectors";

定义支持哪些钱包(如 MetaMask、WalletConnect)供用户连接。可以看到的是这两个钱包的定义方法是不一样的。

connectors: [
    injected({
      target: "metaMask",
    }),
    walletConnect({
      projectId:'c07c0051c2055890eade3556618e38a6',
      showQrModal:false
    }),
  ],

MetaMask

它是一个浏览器插件钱包(当然也有移动端,只不过这里是浏览器插件的)。MetaMask 安装后,会向浏览器的 window.ethereum 注入一个 Provider 对象,DApp 通过此对象直接与钱包交互(如获取账户、签名交易)。所以类似这种注入式的钱包(coinbase,metamask等),都需要用到 injected()方法来定义

WalletConnect

它不是一个的具体钱包,而是一个开放协议。当我们要在 电脑上的 DApp(如 OpenSea)用 手机钱包(如 MetaMask Mobile)付款,但电脑和手机处在不同网络环境(可能不在同一个 WiFi),两者无法直接通信。此时需要一个 中间人(中继服务器) 帮助传递消息,但又要确保中间人无法窃取敏感数据(如私钥、交易内容),而 WalletConnect 就能让用户用手机钱包连接桌面 DApp。

projectId:'c07c0051c2055890eade3556618e38a6',
showQrModal:false
  • projectId 需去 WalletConnect Cloud 注册项目获取。
  • showQrModal:false :WalletConnect协议是自带弹窗的,这里我们使用了 @ant-design/web3 定义的组件 <ConnectButton />,不需要重复弹窗了。

CallTest

import {
  Address,
  ConnectButton,
  Connector,
  NFTCard,
  useAccount,
  useProvider
} from "@ant-design/web3";
useAccount()
const { account } = useAccount()

我们需要知道 wagmi 负责所有链上交互和状态管理。ant-design/web3-wagmi 中的 WagmiWeb3ConfigProvider,它内部整合了 wagmi 库的状态管理。当用户点击 &lt;ConnectButton /> 并成功连接钱包后,wagmi 调用钱包的 API(如 MetaMask 的 eth_requestAccounts),触发钱包弹窗钱包返回账户地址,wagmi 将其存入内部状态管理(如 Redux 或 Context)。作为 wagmi 的上层封装,WagmiWeb3ConfigProvider 会自动监听 wagmi 的状态变化,并将最新的 account 更新到 Web3ConfigProvider 的上下文中。

&lt;WagmiWeb3ConfigProvider
      config={config}
      chains={[Sepolia,Polygon]}
      wallets={[MetaMask(), WalletConnect()]}
    >
useProvider()
const { chain} = useProvider();

同理,跟 useAccount() 方法一样,当用户切换网络(如从 Ethereum 转到 Polygon),wagmi 会自动更新当前链信息。WagmiWeb3ConfigProvider 同步数据:将 wagmi 中的 chainprovider 数据传递到 @ant-design/web3 的上下文中。

useReadContract({})

只读方法

const result = useReadContract({
    abi: [
      {
        type: "function",
        name: "balanceOf",
        stateMutability: "view",
        inputs: [{ name: "account", type: "address" }],
        outputs: [{ type: "uint256" }],
      },
    ],
      address: contractInfo.find( (item) => item.id === chain?.id)?.contractAddress as `0x${string}`,
      functionName: "balanceOf",
    args: [account?.address as `0x${string}`],
  });

abi结构

    abi: [
      {
        type: "function",
        name: "balanceOf",
        stateMutability: "view",
        inputs: [{ name: "account", type: "address" }],
        outputs: [{ type: "uint256" }],
      }  

? 的使用

address: contractInfo.find( (item) => item.id === chain?.id)?.contractAddress as `0x${string}`

? 为可选链操作符。当chain不存在时,返回 undefined,而不是报错。反之存在时返回chain.id

下面是该方法定义传入的参数

export function useReadContract&lt;
  const abi extends Abi | readonly unknown[], //传入合约 ABI
  functionName extends ContractFunctionName&lt;abi, 'pure' | 'view'>, //functionName: "balanceOf", ABI 中定义的 pure 或 view 函数
  args extends ContractFunctionArgs&lt;abi, 'pure' | 'view', functionName>,//args: [account?.address as `0x${string}`], 参数数量:必须为 1 个(因 ABI 中 inputs 长度为 1),必须为 address 类型(通过 as 0x${string} 强制类型)
  config extends Config = ResolvedRegister['config']  //自动继承 createConfig 中定义的全局配置(如 RPC 节点、链信息)
  selectData = ReadContractData&lt;abi, functionName, args>, //型参数,根据合约函数的 ABI 定义自动确定返回值类型
>
useWriteContract()

想合约写入数据,改变链上的参数状态。这里我们结合了 Button 组件。Button组件这里被我们命名为 mint 。当我们在页面上点击 mint 按钮,便会触发 useWriteContract() 的函数逻辑,为我们铸造一个链上的nft。

return (
  &lt;div>
    {/* 显示合约调用结果(代币余额),将 bigint 转换为字符串 */}
    {result.data?.toString()}

    {/* Mint 按钮,点击后调用合约的 mint 函数 */}
    &lt;Button
      onClick={() => {
        // 调用 writeContract 发送交易
        writeContract(
          // 第一个参数:交易配置
          {
            // 合约的 ABI 定义,描述如何与合约交互
            abi: [
              {
                type: "function",       // 类型为函数
                name: "mint",           // 函数名
                stateMutability: "payable", // 函数可接收 ETH(需支付 Gas)
                inputs: [               // 输入参数定义
                  {
                    internalType: "uint256", // Solidity 内部类型
                    name: "quantity",    // 参数名
                    type: "uint256"      // 参数类型(256 位无符号整数)
                  },
                ],
                outputs: [],            // 无返回值(写入操作)
              },
            ],

            // 合约地址:根据当前链 ID 动态获取
            address: contractInfo.find((item) => item.id === chain?.id)?.contractAddress as `0x${string}`, 
            // ^ 从 contractInfo 数组中找到与当前链 ID 匹配的合约地址
            // ^ 强制转换为以太坊地址格式(0x 开头的字符串)

            functionName: "mint",       // 调用的函数名

            // 函数参数:铸造数量为 1 个(需转换为 BigInt 类型)
            args: [BigInt(1)],          // BigInt 用于精确表示大整数

            // 支付金额:0.01 ETH(需转换为 Wei 单位)
            value: parseEther("0.01"),  // parseEther("0.01") = 10000000000000000 Wei
          },

          // 第二个参数:交易回调配置
          {
            onSuccess: () => {          // 交易成功回调
              message.success("Mint Success"); // 显示成功提示
            },
            onError: (err) => {         // 交易失败回调
              message.error(err.message); // 显示错误信息(来自钱包或合约)
            },
          }
        );  
      }}
    >
      mint
    &lt;/Button>
  &lt;/div>
);

export

将 UI组件与函数逻辑结合,输出。这里的NTFCard的address是链上存在的合约地址。

export default function Web3() {
  return (
    // Web3 配置提供者:为子组件注入区块链配置
    &lt;WagmiWeb3ConfigProvider
      config={config}          // 使用 wagmi 的全局配置(链、节点、连接器)
      chains={[Sepolia,Polygon]} // 声明支持的网络:Sepolia 测试网 & Polygon 主网
      wallets={[MetaMask(), WalletConnect()]} // 支持的钱包:MetaMask 插件和 WalletConnect 扫码
    >
      {/* 地址显示组件:格式化显示指定的以太坊地址 */}
      &lt;Address 
        format               // 启用地址格式化(如 0x123...4567)
        address="0xEcd0D12E21805803f70de03B72B1C162dB0898d9" 
      />

      {/* NFT 卡片组件:显示指定合约和 Token ID 的 NFT 信息 */}
      &lt;NFTCard
        address="0xEcd0D12E21805803f70de03B72B1C162dB0898d9" // NFT 合约地址
        tokenId={641} // 具体的 NFT Token ID
      />

      {/* 钱包连接器容器:包裹连接按钮 */}
      &lt;Connector>
        {/* 连接钱包按钮:点击后弹出钱包选择界面 */}
        &lt;ConnectButton />
      &lt;/Connector>

      {/* 自定义组件:包含代币铸造逻辑(如调用合约的 mint 函数) */}
      &lt;CallTest />
    &lt;/WagmiWeb3ConfigProvider>
  );
}

验签功能

为什么要签名

区块链地址本质是用户公钥的密码学哈希值(如以太坊地址为keccak256(公钥).slice(-20)),作为链上资产的唯一标识符。私钥作为用户主权的密码学凭证,始终在客户端离线存储。智能合约的调用都需要地址对应的私钥签名认证,用户被强制性通过钱包私钥对交易签名,之后区块链网络自动验证签名有效性。但是对于非链上资产,就缺少了这一步,所以我们要补充这一步,由DApp服务端发起消息,而不是单纯的由客户端连接钱包就确定DApp中的资产就是用户的,因为其中的钱包api是可以被伪造的。

整个过程

  • 当用户发起身份验证请求时,DApp服务端调用钱包api获取地址的同时,会生成密码学安全的随机数值(Nonce)作为一次性会话凭证,并通过抗碰撞哈希算法保证其唯一性,防止重放攻击。
  • 客户端需构造结构化请求消息(遵循 EIP-712 标准---结构化数据签名标准),使用椭圆曲线数字签名算法(ECDSA-secp256k1)对消息摘要进行私钥签名,成包含 (r, s, v) 参数的三元组数字签名
  • 服务端接收到三元组数字签名后,通过ecrecover(椭圆曲线公钥恢复函数)恢复出公钥,比对之前从前端(window.ethereum)的地址。

代码示例

import React from "react";
import { ConnectButton, Connector, useAccount } from "@ant-design/web3";
import { useSignMessage } from "wagmi";
import { message, Space, Button } from "antd";

const SignDemo: React.FC = () => {
    const { signMessageAsync } = useSignMessage();
    const { account } = useAccount();
    const [signature,setSignature] =  React.useState(false);

    const doSignature = async () => {
        setSignature(true);
        try {
            const signature = await signMessageAsync({
                message: "test message for WTF-DApp demo",
            });
            await checkSignature({
                address: account?.address,
                signature,
            });
        }
        catch (error: any) {
            message.error(`Sign error:${error.message}`);
        }
        setSignature(false);
    }

    const checkSignature = async (
        params:{
            address?: string,
            signature?: string,
        }
    ) => {
        try{
            const response = await fetch("/api/signatureCheck",{
                method:"POST",
                headers: {"Content-Type":"application/json"},
                body: JSON.stringify(params),
            });
            const result = await response.json();
            if(result.data){
                message.success("Signature success");
            }
            else{
                message.error("Signature failed");
            }
        }
        catch (error: any) {
            message.error(`Check signature error:${error.message}`);
        }
    }

    return (
        &lt;Space>
            &lt;Connector>
                &lt;ConnectButton />
            &lt;/Connector>
            &lt;Button
                loading={signature}
                disabled={!account?.address}
                onClick={doSignature}
            >
                Sign Message
            &lt;/Button>
        &lt;/Space>
    )
}

export default SignDemo;
React.useState()

方法结构如下:

const [state, setState] = React.useState(initialState);
  • 参数initialState(状态的初始值,可以是任意类型)

  • 返回值

    :一个数组,包含两个元素:

    • state:当前状态的值。
    • setState:更新状态的函数。
import React from 'react';

function Counter() {
  // 使用 useState 定义状态:count 初始值为 0
  const [count, setCount] = React.useState(0);

  return (
    &lt;div>
      &lt;p>当前计数: {count}&lt;/p>
      {/* 点击按钮调用 setCount 更新状态 */}
      &lt;button onClick={() => setCount(count + 1)}>+1&lt;/button>
    &lt;/div>
  );
}

点击按钮,通过向 setCount 函数传入参数 count + 1,更新 count 参数的值为 1。

useSignMessage()
 const doSignature = async () => {
        setSignature(true);
        try {
            const signature = await signMessageAsync({
                message: "test message for WTF-DApp demo",
            });
            await checkSignature({
                address: account?.address,
                signature,
            });
        }
        catch (error: any) {
            message.error(`Sign error:${error.message}`);
        }
        setSignature(false);
    }
try...catch 语句
  • try:包裹可能出错的代码(如网络请求、用户取消操作)。

  • catch:捕获错误并处理(如提示用户、记录日志)。

    就像网购时「尝试下单 → 若失败显示错误提示」

signMessageAsync

该方法会对指定消息进行数字签名,这里的签名消息是 “test message for WTF-DApp demo",

checkSignature

客户端进行签名,发送消息给服务端,服务端调用api目录下的signatureCheck文件,从三元组的签名消息中恢复公钥,跟前端(window.ethereum)返回的地址进行匹配。之后返回消息 response.json()

 const checkSignature = async (
        params:{
            address?: string,
            signature?: string,
        }
    ) => {
        try{
            const response = await fetch("/api/signatureCheck",{
                method:"POST",
                headers: {"Content-Type":"application/json"},
                body: JSON.stringify(params),
            });
            const result = await response.json();
            if(result.data){
                message.success("Signature success");
            }
            else{
                message.error("Signature failed");
            }
        }
        catch (error: any) {
            message.error(`Check signature error:${error.message}`);
        }
    }
  • fetch("/api/signatureCheck", ...)

    向服务器发送 HTTP 请求,路径为 /api/signatureCheck),就像在浏览器地址栏输入网址。这里是程序自动发送请求。

  • method: "POST

    使用 POST 方法发送数据(适合敏感操作,如提交表单、验证签名)。POST 请求的数据在请求体中,GET 请求的数据在 URL 中(不适用于敏感数据)。

  • headers: {"Content-Type": "application/json"}

    告诉服务器发送的数据是 JSON 格式(如 {"address": "0x123...", "signature": "0x456..."})。若未设置,服务器可能无法正确解析数据。

  • body: JSON.stringify(params)

    将 JavaScript 对象(params)转换为 JSON 字符串发送到服务器。

  • response.json()

    将服务器返回的响应体解析为 JSON 对象(假设服务器返回如 { data: true })。

return语块
 return (
        &lt;Space>
            &lt;Connector>
                &lt;ConnectButton />
            &lt;/Connector>
            &lt;Button
                loading={signature}// 控制按钮的加载状态(是否显示加载动画),来源:通过 React.useState 定义的 signature 状态
                disabled={!account?.address}// 根据钱包地址是否存在,控制按钮是否可点击。
                onClick={doSignature}// 触发该函数同时触发 response请求,调用signatureCheck文件进行验签,返回请求数据。
            >
                Sign Message
            &lt;/Button>
        &lt;/Space>
    )

这里就包含了我在 checkSignature 所说的行为逻辑。那我们下面来看看 signatureCheck文件。

signatureCheck

代码实例

import type { NextApiRequest, NextApiResponse } from "next";
import { createPublicClient, http} from "viem";
import { mainnet} from "viem/chains";

export const publicClient = createPublicClient({
    chain:mainnet, //指定要连接的区块链网络是以太坊主网(Mainnet)
    transport:http(), //// 使用 HTTP 协议与节点通信
})

export default async function handler(
    // 这里定义两个消息参数,req: NextApiRequest —— 接收客户端请求,  res: NextApiResponse —— 向客户端发送响应
    req: NextApiRequest,
    res: NextApiResponse
) {
    try{
    const body = req.body;
    const valid = await publicClient.verifyMessage({
        address: body.address,
        message: "test message for WTF-DApp demo",
        signature: body.signature,
        })
        res.status(200).json({ data: valid });// 表示签名验证逻辑成功完成
    }
    catch (error: any) {
        res.status(500).json({ error: error.message }); //表示服务端发生了意外错误
    }
}
createPublicClient({})

创建一个与以太坊主网(Mainnet)连接的公共客户端实例,用于与区块链进行交互。

verifyMessage

这个方法就是验签的核心。它会接收 signature参数,执行底层函数。同时匹配地址。

转账支付

SendETH

支付逻辑的组件

import * as React from "react";
import { useSendTransaction, useWaitForTransactionReceipt,type BaseError } from "wagmi";
import { parseEther } from "viem";
import { FormProps } from "antd";
import { Form, Input, Button } from "antd";

type FieldType = {
    to: `0x${string}`;
    value: string;
};

export const SendEth:React.FC = () => {
    const {data: hash,error,isPending,sendTransaction} = useSendTransaction();

    const{ isLoading:isConfirming,isSuccess:isConfirmed} = useWaitForTransactionReceipt({hash});

    const onFinish: FormProps&lt;FieldType>["onFinish"] = (values) => {
        console.log("Success:", values);
        sendTransaction({
            to: values.to,
            value: parseEther(values.value),
        });
    }

   const onFinishFailed: FormProps&lt;FieldType>["onFinishFailed"] = (errorInfo) => {
    console.log("Failed:", errorInfo);
   }

   return (
    &lt;Form
      name="basic"
      labelCol={{ span: 8 }}
      wrapperCol={{ span: 16 }}
      style={{ maxWidth: 600 }}
      initialValues={{ remember: true }}
      onFinish={onFinish}
      onFinishFailed={onFinishFailed}
      autoComplete="off"
    >
      &lt;Form.Item&lt;FieldType>
        label="to"
        name="to"
        rules={[{ required: true, message: 'Please input!' }]}
      >
        &lt;Input />
      &lt;/Form.Item>

      &lt;Form.Item&lt;FieldType>
        label="value"
        name="value"
        rules={[{ required: true, message: 'Please input!' }]}
      >
        &lt;Input />
      &lt;/Form.Item>

      &lt;Form.Item wrapperCol={{ offset: 8, span: 16 }}>
        &lt;Button type="primary" htmlType="submit">
          {isPending ? 'Confirming...' : 'Send'} 
        &lt;/Button>
      &lt;/Form.Item>

      {hash && &lt;div>Transaction Hash: {hash}&lt;/div>} 
      {isConfirming && &lt;div>Waiting for confirmation...&lt;/div>} 
      {isConfirmed && &lt;div>Transaction confirmed.&lt;/div>} 
      {error && ( 
        &lt;div>Error: {(error as BaseError).shortMessage || error.message}&lt;/div> 
      )} 
    &lt;/Form>
  )
}

useSendTransaction()

创建并发送以太坊交易

{
  data: hash,       // 交易哈希(发送成功后获得)
  error,            // 错误对象(发送失败时存在)
  isPending,        // 发送中状态(true表示正在等待钱包确认)
  sendTransaction   // 触发交易发送的函数
}

核心参数--sendTransaction

sendTransaction({
  to: '0x...',       // 必填:接收地址
  value: parseEther('0.1'), // 转账金额(需转换为wei单位)
  // 可选参数:
  gas: 21000,        // 手动设置gas limit
  gasPrice: 5000000000, // 手动设置gas价格
  chainId: 1,        // 指定链ID
})

useWaitForTransactionReceipt({hash})

监听交易确认状态

{ 
  hash: '0x...' // 必须提供有效的交易哈希 
}

返回状态

{
  isLoading: isConfirming,  // 是否在等待区块确认
  isSuccess: isConfirmed,   // 是否已成功确认
  isError,                  // 是否确认失败
  error                     // 失败错误信息
}

整个流程

交易发送成功 → 获得hash → 开始监听 → 区块打包中(isConfirming=true) → 打包完成(isConfirmed=true)
                                   ↘ 超过等待时间/失败 → isError=true

泛型参数

const onFinish: FormProps&lt;FieldType>["onFinish"] = (values) => {}
  • FormProps&lt;FieldType> 中的 &lt;FieldType> 相当于一个类型的定义,这个类型就是 下面代码中定义的type(自定义)类型,传入的参数values 必须满足该类型结构。

    type FieldType = {
      to: `0x${string}`;
      value: string;
    };
    sendTransaction({
              to: values.to,
              value: parseEther(values.value),
          });
    
    &lt;Form.Item&lt;FieldType>
          label="to"
          name="to"
          rules={[{ required: true, message: 'Please input!' }]}
        >
  • onFinish 是 Form 组件的内置属性名,不能随意更名。

  • required: true : 强制用户必须填写该字段,否则阻止表单提交并显示错误提示。且填入参数类型满足 FieldType

表单

定义了输入参数的UI组件

 return (
    &lt;Form
      name="basic"
      labelCol={{ span: 8 }}
      wrapperCol={{ span: 16 }}
      style={{ maxWidth: 600 }}
      initialValues={{ remember: true }}
      onFinish={onFinish}
      onFinishFailed={onFinishFailed}
      autoComplete="off"
    >
      &lt;Form.Item&lt;FieldType>
        label="to" 
        name="to"
        rules={[{ required: true, message: 'Please input!' }]}
      >
        &lt;Input />
      &lt;/Form.Item>

      &lt;Form.Item&lt;FieldType>
        label="value"
        name="value"
        rules={[{ required: true, message: 'Please input!' }]}
      >
        &lt;Input />
      &lt;/Form.Item>

      &lt;Form.Item wrapperCol={{ offset: 8, span: 16 }}>
        &lt;Button type="primary" htmlType="submit">
          {isPending ? 'Confirming...' : 'Send'} 
        &lt;/Button>
      &lt;/Form.Item>

      {hash && &lt;div>Transaction Hash: {hash}&lt;/div>} 
      {isConfirming && &lt;div>Waiting for confirmation...&lt;/div>} 
      {isConfirmed && &lt;div>Transaction confirmed.&lt;/div>} 
      {error && ( 
        &lt;div>Error: {(error as BaseError).shortMessage || error.message}&lt;/div> 
      )} 
    &lt;/Form>
  )
  • onFinish={onFinish} //定义表单提交成功的业务逻辑(如发送请求)
    onFinishFailed={onFinishFailed} //定义表单提交失败的处理逻辑(如错误提示)

    表单验证 通过 或 失败 后的回调函数

  • label属性标签:定义表单项的 标签文本(即输入框左侧的说明文字)

  • &lt;Form.Item&lt;FieldType>> 泛型参数,确保 name 只能是 FieldType 的键(tovalue),且是独立的输入项,要明确数据归属,name="to" → 用户输入的地址会映射到 values.to

index.tsx

import { mainnet } from "viem/chains";
import { sepolia } from "wagmi/chains";
import { createConfig, http, injected } from "wagmi";
import React from "react";
import { MetaMask ,WagmiWeb3ConfigProvider} from "@ant-design/web3-wagmi";
import { ConnectButton, Connector } from "@ant-design/web3";
import {SendEth} from "../../componments/SendEth"

const config = createConfig({
    chains: [mainnet ,sepolia],
    transports:{
        [mainnet.id] :http(),
        [sepolia.id] :http(),
    },
    connectors:[
        injected({
            target:"metaMask",
        }),
    ],
});

const TransactionDemo:React.FC = () => {

    return(
        &lt;WagmiWeb3ConfigProvider 
        config={config}
        eip6963={{
            autoAddInjectedWallets:true,
        }}
        wallets={[
            MetaMask()
        ]}
        >
        &lt;Connector>
            &lt;ConnectButton />
            &lt;/Connector>
            &lt;SendEth />
        &lt;/WagmiWeb3ConfigProvider>
    )
}

export default TransactionDemo;
  • eip6963={{
              autoAddInjectedWallets:true,
          }}

    当设置为 true 时,DApp 会自动检测用户浏览器中安装的 符合 EIP-6963 标准 的钱包(如 MetaMask、Coinbase Wallet 等),并将其添加到可连接钱包列表中。

点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
浪迹陨灭
浪迹陨灭
0x0c37...a92b
专注于solidity智能合约的开发