web3开发第13讲

第十三讲:DApp 转账与收款功能实现

本讲概述

这一讲将会介绍如何在 DApp 中实现通过区块链转账和收款功能。转账是区块链最基础的操作之一,我们将学习如何使用 wagmi 和 Ant Design Web3 来构建一个完整的转账功能。1

1. 区块链转账基础概念

1.1 什么是区块链转账

在区块链环境中,转账是指在参与者之间转移资产的行为,这些资产可以是加密货币(如比特币、以太坊等)或者其他基于区块链的数字资产(如代币、NFTs 等)。这些转账活动被记录在区块链上,并受到网络共识机制的安全保护。1

1.2 转账的关键要素

转账涉及以下几个要点:1

  • 源地址(发送方):需要转出资产的区块链账户地址
  • 目标地址(接收方):将接收资产的区块链账户地址
  • 资产数量:想要转账的资产的确切数量或价值
  • 交易费用:通常由执行交易的矿工或验证节点收取的费用
  • 网络:完成转账的区块链网络(例如比特币网络、以太坊网络)

1.3 转账流程

区块链转账的过程大致包括以下步骤:1

  1. 发起方用其私钥对转账信息进行签名,并将这些信息广播到网络
  2. 网络中的节点(矿工或验证者)接收到交易请求,并验证签名和交易的有效性
  3. 一旦验证无误,转账会被打包到其他交易中构成一个新的区块
  4. 这个区块经过网络共识机制的确认后,被添加到区块链上
  5. 交易完成后,目标地址上的资产余额更新,体现了转账的结果

2. 实现转账功能

2.1 创建转账页面

首先,我们创建一个新的转账页面。新建一个 pages/transaction/index.tsx 文件:1

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

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

const TransactionDemo: React.FC = () => {
  return (
    <WagmiWeb3ConfigProvider
      config={config}
      eip6963={{
        autoAddInjectedWallets: true,
      }}
      wallets={[MetaMask()]}
    >
      <Connector>
        <ConnectButton />
      </Connector>
      <SendEth />
    </WagmiWeb3ConfigProvider>
  );
};

export default TransactionDemo;

2.2 创建转账组件

接下来,我们创建 SendEth 组件来实现具体的转账功能。新建 components/SendEth.tsx 文件:1

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

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<FieldType>["onFinish"] = (values) => {
    console.log("Success:", values);
    try {
      sendTransaction({ 
        to: values.to, 
        value: parseEther(values.value) 
      });
    } catch (err) {
      message.error('转账失败,请检查输入信息');
    }
  };

  const onFinishFailed: FormProps<FieldType>["onFinishFailed"] = (
    errorInfo
  ) => {
    console.log("Failed:", errorInfo);
    message.error('请填写完整的转账信息');
  };

  // 监听交易状态变化
  React.useEffect(() => {
    if (isConfirmed) {
      message.success('转账成功!');
    }
  }, [isConfirmed]);

  React.useEffect(() => {
    if (error) {
      message.error(`转账失败: ${(error as BaseError).shortMessage || error.message}`);
    }
  }, [error]);

  return (
    <div style={{ maxWidth: 600, margin: '20px auto', padding: '20px' }}>
      <h2>ETH 转账</h2>
      <Form
        name="sendEth"
        labelCol={{ span: 6 }}
        wrapperCol={{ span: 18 }}
        onFinish={onFinish}
        onFinishFailed={onFinishFailed}
        autoComplete="off"
        layout="horizontal"
      >
        <Form.Item<FieldType>
          label="接收地址"
          name="to"
          rules={[
            { required: true, message: "请输入接收地址!" },
            { 
              pattern: /^0x[a-fA-F0-9]{40}$/, 
              message: "请输入有效的以太坊地址!" 
            }
          ]}
        >
          <Input placeholder="0x..." />
        </Form.Item>

        <Form.Item<FieldType>
          label="转账金额"
          name="value"
          rules={[
            { required: true, message: "请输入转账金额!" },
            { 
              pattern: /^\d+(\.\d+)?$/, 
              message: "请输入有效的数字!" 
            }
          ]}
        >
          <Input placeholder="0.01" suffix="ETH" />
        </Form.Item>

        <Form.Item wrapperCol={{ offset: 6, span: 18 }}>
          <Button 
            type="primary" 
            htmlType="submit" 
            loading={isPending || isConfirming}
            disabled={isPending || isConfirming}
          >
            {isPending ? "确认中..." : isConfirming ? "等待确认..." : "发送转账"}
          </Button>
        </Form.Item>
      </Form>

      {/* 交易状态显示 */}
      {hash && (
        <div style={{ marginTop: 20, padding: 16, background: '#f5f5f5', borderRadius: 8 }}>
          <h4>交易信息</h4>
          <p><strong>交易哈希:</strong> {hash}</p>
          {isConfirming && <p style={{ color: '#1890ff' }}>⏳ 等待区块链确认...</p>}
          {isConfirmed && <p style={{ color: '#52c41a' }}>✅ 交易已确认</p>}
        </div>
      )}
    </div>
  );
};

3. 核心 Hooks 详解

3.1 useSendTransaction Hook

useSendTransaction 是 wagmi 提供的核心 Hook,用于发送交易:2

const {
  data: hash,        // 交易哈希
  error,            // 错误信息
  isPending,        // 是否正在发送
  sendTransaction,  // 发送交易函数
} = useSendTransaction();

sendTransaction 参数说明:2

sendTransaction({
  to: "0xCe06B0A53b08C10fa508BF16D02bBdDc6961E3B3", // 收款地址或ENS域名
  value: parseEther('0.01'),                        // 转账金额
  gasPrice: parseGwei("10"),                       // Gas 价格
  maxFeePerGas: parseGwei("10"),                   // 最大 Gas 费用
  maxPriorityFeePerGas: parseGwei("10"),           // 最大优先 Gas
});

3.2 useWaitForTransactionReceipt Hook

用于等待交易确认:

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

4. 高级转账功能

4.1 代币转账组件

除了 ETH 转账,我们还可以实现 ERC-20 代币转账:

import React from 'react';
import { Button, Form, Input, Select, message } from 'antd';
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
import { parseUnits } from 'viem';

// ERC-20 标准 ABI(简化版)
const ERC20_ABI = [
  {
    name: 'transfer',
    type: 'function',
    stateMutability: 'nonpayable',
    inputs: [
      { name: 'to', type: 'address' },
      { name: 'amount', type: 'uint256' }
    ],
    outputs: [{ name: '', type: 'bool' }]
  }
] as const;

// 常用代币配置
const TOKENS = {
  USDC: {
    address: '0xA0b86a33E6441b8435b662f0E2d0B8C8C8C8C8C8' as `0x${string}`,
    decimals: 6,
    symbol: 'USDC'
  },
  USDT: {
    address: '0xdAC17F958D2ee523a2206206994597C13D831ec7' as `0x${string}`,
    decimals: 6,
    symbol: 'USDT'
  }
};

type TokenTransferForm = {
  token: keyof typeof TOKENS;
  to: `0x${string}`;
  amount: string;
};

export const SendToken: React.FC = () => {
  const { writeContract, data: hash, isPending } = useWriteContract();
  const { isLoading: isConfirming, isSuccess: isConfirmed } = 
    useWaitForTransactionReceipt({ hash });

  const onFinish = (values: TokenTransferForm) => {
    const token = TOKENS[values.token];
    const amount = parseUnits(values.amount, token.decimals);

    writeContract({
      address: token.address,
      abi: ERC20_ABI,
      functionName: 'transfer',
      args: [values.to, amount]
    });
  };

  return (
    <div style={{ maxWidth: 600, margin: '20px auto', padding: '20px' }}>
      <h2>代币转账</h2>
      <Form onFinish={onFinish} layout="horizontal" labelCol={{ span: 6 }}>
        <Form.Item
          label="选择代币"
          name="token"
          rules={[{ required: true, message: '请选择代币' }]}
        >
          <Select placeholder="选择要转账的代币">
            {Object.entries(TOKENS).map(([key, token]) => (
              <Select.Option key={key} value={key}>
                {token.symbol}
              </Select.Option>
            ))}
          </Select>
        </Form.Item>

        <Form.Item
          label="接收地址"
          name="to"
          rules={[
            { required: true, message: '请输入接收地址' },
            { pattern: /^0x[a-fA-F0-9]{40}$/, message: '请输入有效地址' }
          ]}
        >
          <Input placeholder="0x..." />
        </Form.Item>

        <Form.Item
          label="转账数量"
          name="amount"
          rules={[
            { required: true, message: '请输入转账数量' },
            { pattern: /^\d+(\.\d+)?$/, message: '请输入有效数字' }
          ]}
        >
          <Input placeholder="100" />
        </Form.Item>

        <Form.Item wrapperCol={{ offset: 6 }}>
          <Button 
            type="primary" 
            htmlType="submit"
            loading={isPending || isConfirming}
          >
            {isPending ? '确认中...' : isConfirming ? '等待确认...' : '发送代币'}
          </Button>
        </Form.Item>
      </Form>

      {hash && (
        <div style={{ marginTop: 20, padding: 16, background: '#f5f5f5' }}>
          <p><strong>交易哈希:</strong> {hash}</p>
          {isConfirming && <p style={{ color: '#1890ff' }}>等待确认...</p>}
          {isConfirmed && <p style={{ color: '#52c41a' }}>转账成功!</p>}
        </div>
      )}
    </div>
  );
};

4.2 批量转账功能

实现一次性向多个地址转账:

import React, { useState } from 'react';
import { Button, Form, Input, Table, message, Space } from 'antd';
import { useSendTransaction, useWaitForTransactionReceipt } from 'wagmi';
import { parseEther } from 'viem';

interface TransferItem {
  key: string;
  address: string;
  amount: string;
}

export const BatchTransfer: React.FC = () => {
  const [transferList, setTransferList] = useState<TransferItem[]>([]);
  const [currentIndex, setCurrentIndex] = useState(0);
  const [isProcessing, setIsProcessing] = useState(false);

  const { sendTransaction, data: hash, isPending } = useSendTransaction();
  const { isLoading: isConfirming, isSuccess: isConfirmed } = 
    useWaitForTransactionReceipt({ hash });

  // 添加转账项
  const addTransferItem = (values: { address: string; amount: string }) => {
    const newItem: TransferItem = {
      key: Date.now().toString(),
      address: values.address,
      amount: values.amount
    };
    setTransferList([...transferList, newItem]);
  };

  // 删除转账项
  const removeTransferItem = (key: string) => {
    setTransferList(transferList.filter(item => item.key !== key));
  };

  // 执行批量转账
  const executeBatchTransfer = async () => {
    if (transferList.length === 0) {
      message.warning('请先添加转账项');
      return;
    }

    setIsProcessing(true);
    setCurrentIndex(0);

    for (let i = 0; i < transferList.length; i++) {
      const item = transferList[i];
      setCurrentIndex(i);
      
      try {
        await sendTransaction({
          to: item.address as `0x${string}`,
          value: parseEther(item.amount)
        });
        
        // 等待当前交易确认后再进行下一个
        // 注意:实际应用中可能需要更复杂的状态管理
      } catch (error) {
        message.error(`第 ${i + 1} 笔转账失败`);
        break;
      }
    }

    setIsProcessing(false);
  };

  const columns = [
    {
      title: '接收地址',
      dataIndex: 'address',
      key: 'address',
    },
    {
      title: '金额 (ETH)',
      dataIndex: 'amount',
      key: 'amount',
    },
    {
      title: '操作',
      key: 'action',
      render: (_: any, record: TransferItem) => (
        <Button 
          type="link" 
          danger 
          onClick={() => removeTransferItem(record.key)}
        >
          删除
        </Button>
      ),
    },
  ];

  return (
    <div style={{ maxWidth: 800, margin: '20px auto', padding: '20px' }}>
      <h2>批量转账</h2>
      
      {/* 添加转账项表单 */}
      <Form onFinish={addTransferItem} layout="inline" style={{ marginBottom: 20 }}>
        <Form.Item
          name="address"
          rules={[
            { required: true, message: '请输入地址' },
            { pattern: /^0x[a-fA-F0-9]{40}$/, message: '无效地址' }
          ]}
        >
          <Input placeholder="接收地址" style={{ width: 300 }} />
        </Form.Item>
        
        <Form.Item
          name="amount"
          rules={[
            { required: true, message: '请输入金额' },
            { pattern: /^\d+(\.\d+)?$/, message: '无效金额' }
          ]}
        >
          <Input placeholder="金额" style={{ width: 150 }} />
        </Form.Item>
        
        <Form.Item>
          <Button type="primary" htmlType="submit">
            添加
          </Button>
        </Form.Item>
      </Form>

      {/* 转账列表 */}
      <Table 
        columns={columns} 
        dataSource={transferList} 
        pagination={false}
        style={{ marginBottom: 20 }}
      />

      {/* 执行按钮 */}
      <Space>
        <Button 
          type="primary" 
          size="large"
          onClick={executeBatchTransfer}
          loading={isProcessing}
          disabled={transferList.length === 0}
        >
          执行批量转账
        </Button>
        
        <Button 
          onClick={() => setTransferList([])}
          disabled={isProcessing}
        >
          清空列表
        </Button>
      </Space>

      {/* 进度显示 */}
      {isProcessing && (
        <div style={{ marginTop: 20, padding: 16, background: '#f5f5f5' }}>
          <p>正在执行第 {currentIndex + 1} / {transferList.length} 笔转账...</p>
        </div>
      )}
    </div>
  );
};

5. 收款功能实现

5.1 收款二维码组件

import React, { useState } from 'react';
import { Card, Input, Button, QRCode, message, Space, Typography } from 'antd';
import { useAccount } from 'wagmi';
import { CopyOutlined } from '@ant-design/icons';

const { Text, Title } = Typography;

export const ReceivePayment: React.FC = () => {
  const { address, isConnected } = useAccount();
  const [amount, setAmount] = useState('');
  const [description, setDescription] = useState('');

  // 生成支付链接
  const generatePaymentLink = () => {
    if (!address) return '';
    
    const params = new URLSearchParams();
    if (amount) params.append('value', amount);
    if (description) params.append('data', description);
    
    return `ethereum:${address}${params.toString() ? '?' + params.toString() : ''}`;
  };

  // 复制地址
  const copyAddress = async () => {
    if (address) {
      await navigator.clipboard.writeText(address);
      message.success('地址已复制到剪贴板');
    }
  };

  // 复制支付链接
  const copyPaymentLink = async () => {
    const link = generatePaymentLink();
    if (link) {
      await navigator.clipboard.writeText(link);
      message.success('支付链接已复制到剪贴板');
    }
  };

  if (!isConnected) {
    return (
      <Card title="收款功能">
        <p>请先连接钱包以使用收款功能</p>
      </Card>
    );
  }

  return (
    <div style={{ maxWidth: 600, margin: '20px auto' }}>
      <Card title="收款" style={{ marginBottom: 20 }}>
        <Space direction="vertical" style={{ width: '100%' }} size="large">
          {/* 钱包地址 */}
          <div>
            <Title level={5}>我的钱包地址</Title>
            <Space>
              <Text code copyable={{ onCopy: copyAddress }}>
                {address}
              </Text>
              <Button 
                icon={<CopyOutlined />} 
                size="small" 
                onClick={copyAddress}
              >
                复制
              </Button>
            </Space>
          </div>

          {/* 收款金额 */}
          <div>
            <Title level={5}>收款金额 (可选)</Title>
            <Input
              placeholder="输入收款金额"
              suffix="ETH"
              value={amount}
              onChange={(e) => setAmount(e.target.value)}
            />
          </div>

          {/* 收款说明 */}
          <div>
            <Title level={5}>收款说明 (可选)</Title>
            <Input.TextArea
              placeholder="输入收款说明"
              value={description}
              onChange={(e) => setDescription(e.target.value)}
              rows={3}
            />
          </div>

          {/* 二维码 */}
          <div style={{ textAlign: 'center' }}>
            <Title level={5}>收款二维码</Title>
            <QRCode 
              value={generatePaymentLink()} 
              size={200}
              style={{ margin: '16px 0' }}
            />
            <br />
            <Button onClick={copyPaymentLink}>
              复制支付链接
            </Button>
          </div>
        </Space>
      </Card>
    </div>
  );
};

5.2 收款记录组件

import React, { useEffect, useState } from 'react';
import { Table, Card, Tag, Button, DatePicker, Space } from 'antd';
import { useAccount, usePublicClient } from 'wagmi';
import { formatEther } from 'viem';
import dayjs from 'dayjs';

interface Transaction {
  hash: string;
  from: string;
  to: string;
  value: string;
  timestamp: number;
  status: 'success' | 'pending' | 'failed';
}

export const PaymentHistory: React.FC = () => {
  const { address } = useAccount();
  const publicClient = usePublicClient();
  const [transactions, setTransactions] = useState<Transaction[]>([]);
  const [loading, setLoading] = useState(false);
  const [dateRange, setDateRange] = useState<[dayjs.Dayjs, dayjs.Dayjs] | null>(null);

  // 获取交易记录
  const fetchTransactions = async () => {
    if (!address || !publicClient) return;
    
    setLoading(true);
    try {
      // 这里需要使用区块链浏览器 API 或者事件日志来获取交易记录
      // 示例代码,实际需要根据具体需求实现
      const latestBlock = await publicClient.getBlockNumber();
      const fromBlock = latestBlock - BigInt(1000); // 查询最近1000个区块
      
      // 获取转入交易(这里简化处理)
      const logs = await publicClient.getLogs({
        address: address,
        fromBlock,
        toBlock: 'latest'
      });
      
      // 处理日志数据转换为交易记录
      // 实际实现需要更复杂的逻辑
      
    } catch (error) {
      console.error('获取交易记录失败:', error);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchTransactions();
  }, [address]);

  const columns = [
    {
      title: '交易哈希',
      dataIndex: 'hash',
      key: 'hash',
      render: (hash: string) => (
        <a 
          href={`https://etherscan.io/tx/${hash}`} 
          target="_blank" 
          rel="noopener noreferrer"
        >
          {hash.slice(0, 10)}...{hash.slice(-8)}
        </a>
      ),
    },
    {
      title: '发送方',
      dataIndex: 'from',
      key: 'from',
      render: (from: string) => (
        <span>{from.slice(0, 6)}...{from.slice(-4)}</span>
      ),
    },
    {
      title: '金额',
      dataIndex: 'value',
      key: 'value',
      render: (value: string) => `${formatEther(BigInt(value))} ETH`,
    },
    {
      title: '时间',
      dataIndex: 'timestamp',
      key: 'timestamp',
      render: (timestamp: number) => 
        dayjs(timestamp * 1000).format('YYYY-MM-DD HH:mm:ss'),
    },
    {
      title: '状态',
      dataIndex: 'status',
      key: 'status',
      render: (status: string) => {
        const colorMap = {
          success: 'green',
          pending: 'orange',
          failed: 'red'
        };
        return <Tag color={colorMap[status as keyof typeof colorMap]}>{status}</Tag>;
      },
    },
  ];

  return (
    <Card title="收款记录">
      <Space style={{ marginBottom: 16 }}>
        <DatePicker.RangePicker
          value={dateRange}
          onChange={setDateRange}
          placeholder={['开始日期', '结束日期']}
        />
        <Button onClick={fetchTransactions} loading={loading}>
          刷新
        </Button>
      </Space>
      
      <Table
        columns={columns}
        dataSource={transactions}
        loading={loading}
        rowKey="hash"
        pagination={{
          pageSize: 10,
          showSizeChanger: true,
          showQuickJumper: true,
        }}
      />
    </Card>
  );
};

6. 错误处理和用户体验优化

6.1 常见错误处理

import { message } from 'antd';

export const handleTransactionError = (error: any) => {
  const errorMessage = error?.shortMessage || error?.message || '未知错误';
  
  // 常见错误类型处理
  if (errorMessage.includes('insufficient funds')) {
    message.error('余额不足,请检查账户余额');
  } else if (errorMessage.includes('user rejected')) {
    message.warning('用户取消了交易');
  } else if (errorMessage.includes('gas')) {
    message.error('Gas 费用不足或设置过低');
  } else if (errorMessage.includes('nonce')) {
    message.error('交易序号错误,请重试');
  } else {
    message.error(`交易失败: ${errorMessage}`);
  }
};

// 网络错误处理
export const handleNetworkError = (chainId: number) => {
  const networkNames: { [key: number]: string } = {
    1: '以太坊主网',
    11155111: 'Sepolia 测试网',
    137: 'Polygon 主网',
  };
  
  const networkName = networkNames[chainId] || `网络 ${chainId}`;
  message.error(`请切换到 ${networkName}`);
};

6.2 交易状态跟踪

import React from 'react';
import { Steps, Card, Button, Space } from 'antd';
import { CheckCircleOutlined, LoadingOutlined, CloseCircleOutlined } from '@ant-design/icons';

interface TransactionTrackerProps {
  hash?: string;
  isPending: boolean;
  isConfirming: boolean;
  isConfirmed: boolean;
  error?: Error;
  onRetry?: () => void;
}

export const TransactionTracker: React.FC<TransactionTrackerProps> = ({
  hash,
  isPending,
  isConfirming,
  isConfirmed,
  error,
  onRetry
}) => {
  const getStepStatus = (step: number) => {
    if (error) return 'error';
    if (step === 0 && isPending) return 'process';
    if (step === 1 && isConfirming) return 'process';
    if (step === 2 && isConfirmed) return 'finish';
    if (step === 0 && hash) return 'finish';
    if (step === 1 && hash && !isConfirming) return 'finish';
    return 'wait';
  };

  const getStepIcon = (step: number) => {
    if (error) return <CloseCircleOutlined />;
    const status = getStepStatus(step);
    if (status === 'process') return <LoadingOutlined />;
    if (status === 'finish') return <CheckCircleOutlined />;
    return undefined;
  };

  const steps = [
    {
      title: '发起交易',
      description: isPending ? '正在发送交易...' : '交易已发送',
      status: getStepStatus(0),
      icon: getStepIcon(0)
    },
    {
      title: '等待确认',
      description: isConfirming ? '等待区块链确认...' : '等待确认',
      status: getStepStatus(1),
      icon: getStepIcon(1)
    },
    {
      title: '交易完成',
      description: isConfirmed ? '交易已成功确认' : '等待完成',
      status: getStepStatus(2),
      icon: getStepIcon(2)
    }
  ];

  return (
    <Card title="交易状态" style={{ marginTop: 20 }}>
      <Steps
        current={isPending ? 0 : isConfirming ? 1 : isConfirmed ? 2 : -1}
        status={error ? 'error' : 'process'}
        items={steps}
      />
      
      {hash && (
        <div style={{ marginTop: 16, padding: 12, background: '#f5f5f5', borderRadius: 6 }}>
          <p><strong>交易哈希:</strong></p>
          <p style={{ wordBreak: 'break-all', fontSize: '12px' }}>{hash}</p>
          <a 
            href={`https://etherscan.io/tx/${hash}`} 
            target="_blank" 
            rel="noopener noreferrer"
          >
            在 Etherscan 上查看
          </a>
        </div>
      )}
      
      {error && (
        <div style={{ marginTop: 16, textAlign: 'center' }}>
          <p style={{ color: '#ff4d4f', marginBottom: 12 }}>
            {error.message}
          </p>
          {onRetry && (
            <Button type="primary" onClick={onRetry}>
              重试
            </Button>
          )}
        </div>
      )}
    </Card>
  );
};

7. 安全注意事项

7.1 输入验证

// 地址验证
export const isValidAddress = (address: string): boolean => {
  return /^0x[a-fA-F0-9]{40}$/.test(address);
};

// 金额验证
export const isValidAmount = (amount: string): boolean => {
  const num = parseFloat(amount);
  return !isNaN(num) && num > 0 && num < 1000000; // 设置合理上限
};

// Gas 价格验证
export const isValidGasPrice = (gasPrice: string): boolean => {
  const num = parseFloat(gasPrice);
  return !isNaN(num) && num >= 1 && num <= 1000; // Gwei 范围
};

7.2 交易确认

import React from 'react';
import { Modal, Descriptions, Button, Space, Alert } from 'antd';
import { formatEther, formatGwei } from 'viem';

interface TransactionConfirmModalProps {
  visible: boolean;
  onConfirm: () => void;
  onCancel: () => void;
  transaction: {
    to: string;
    value: bigint;
    gasPrice?: bigint;
    gasLimit?: bigint;
  };
}

export const TransactionConfirmModal: React.FC<TransactionConfirmModalProps> = ({
  visible,
  onConfirm,
  onCancel,
  transaction
}) => {
  const estimatedFee = transaction.gasPrice && transaction.gasLimit 
    ? transaction.gasPrice * transaction.gasLimit 
    : BigInt(0);

  return (
    <Modal
      title="确认交易"
      open={visible}
      onCancel={onCancel}
      footer={[
        <Button key="cancel" onClick={onCancel}>
          取消
        </Button>,
        <Button key="confirm" type="primary" onClick={onConfirm}>
          确认发送
        </Button>
      ]}
    >
      <Alert
        message="请仔细核对交易信息"
        description="交易一旦发送将无法撤销,请确保信息正确"
        type="warning"
        style={{ marginBottom: 16 }}
      />
      
      <Descriptions column={1} bordered>
        <Descriptions.Item label="接收地址">
          <span style={{ wordBreak: 'break-all' }}>{transaction.to}</span>
        </Descriptions.Item>
        <Descriptions.Item label="转账金额">
          {formatEther(transaction.value)} ETH
        </Descriptions.Item>
        {transaction.gasPrice && (
          <Descriptions.Item label="Gas 价格">
            {formatGwei(transaction.gasPrice)} Gwei
          </Descriptions.Item>
        )}
        {estimatedFee > 0 && (
          <Descriptions.Item label="预估手续费">
            {formatEther(estimatedFee)} ETH
          </Descriptions.Item>
        )}
      </Descriptions>
    </Modal>
  );
};

8. 本讲总结

在这一讲中,我们学习了:

  1. 区块链转账基础概念 - 了解转账的基本原理和流程
  2. 基础转账功能 - 使用 wagmi 实现 ETH 转账
  3. 代币转账 - 实现 ERC-20 代币转账功能
  4. 批量转账 - 一次性向多个地址转账
  5. 收款功能 - 生成收款二维码和管理收款记录
  6. 错误处理 - 处理各种交易错误情况
  7. 安全措施 - 输入验证和交易确认

通过本讲的学习,你已经掌握了在 DApp 中实现完整转账和收款功能的方法。这些功能是大多数 DApp 的核心组成部分,为用户提供了便捷的资产管理体验。

下一讲预告

下一讲我们将学习 “DApp 数据存储与状态管理”,包括:

  • 本地存储管理
  • 区块链数据缓存
  • 状态持久化
  • 离线数据同步

敬请期待!

源码来自:https://pan.quark.cn/s/41b9d28f0d6d 在信息技术领域中,jQuery作为一个广受欢迎的JavaScript框架,显著简化了诸多操作,包括对HTML文档的遍历、事件的管理、动画的设计以及Ajax通信等。 本篇文档将深入阐释如何运用jQuery达成一个图片自动播放的功能,这种效果常用于网站的轮播展示或幻灯片演示,有助于优化用户与页面的互动,使网页呈现更加动态的视觉体验。 为了有效实施这一功能,首先需掌握jQuery的核心操作。 通过$符号作为接口,jQuery能够迅速选取DOM组件,例如$("#id")用于选取具有特定ID的元素,而$(".class")则能选取所有应用了某类class的元素。 在选定元素之后,可以执行多种行为,诸如事件监听、样式的变更、内容的更新以及动画的制作等。 关于“一个基于jQuery的图片自动播放功能”,首要任务是准备一组图片素材,这些素材将被整合至一个容器元素之中。 例如,可以构建一个div元素,将其宽度设定为单张图片的尺寸,再借助CSS实现溢出内容的隐藏,从而构建出水平滚动的初始框架。 ```html<div id="slider"> <img src="image1.jpg" alt="Image 1"> <img src="image2.jpg" alt="Image 2"> <!-- 更多图片内容... --></div>```接着,需要编写jQuery脚本以实现图片的自动切换。 这通常涉及到定时器的运用,以设定周期性间隔自动更换当前显示的图片。 通过使用`.fadeOut()`和`.fadeIn()`方法,能够实现图片间的平滑过渡,增强视觉效果。 ```javascript$(document).re...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值