欢迎光临
我们一直在努力

web3开发第11讲

第十一讲:DApp 多链支持

本讲概述

以太坊除了有主网和测试网以外还有丰富的 L2 生态,对于有的 DApp 来说可能需要连接不同的链。这一讲将会指引开发者如何在 DApp 中连接多条链。0

什么是多链 DApp

多链概念

DApp 支持多链是指能够在多个区块链平台上运行或与之交互。这意味着 DApp 不仅限于单一的区块链生态系统,而是可以跨越不同的区块链,利用各个平台的特点和优势,以提供更广泛的功能、更好的用户体验或更高的效率。0

多链优势

  • 资产互操作性:可以使用户利用在一个区块链上的资产,在另一个区块链上进行操作。例如,用户可以将某区块链上的代币通过跨链技术(如区块链桥)转移到另一个区块链上,DApp 可以在这两个链上识别并使用这些资产。0

  • 技术特性优化:不同的区块链平台具有不同的技术特性和优势,比如交易速度、费用等。0

  • 用户社区聚合:每个链都有其自身的用户社区,支持多链也可以帮助 DApp 将这些不同社区的用户聚集起来。0

  • 多链实现技术

    核心技术方案

    实现 DApp 支持多链通常需要开发者使用一些特定的技术和工具:0

  • 跨链桥:这是最常见的方法之一,跨链桥允许不同区块链之间的资产转移。通过这样的桥,NFT 可以从一个链"包装"并转移到另一个链,用户可以在目标链上使用对应的"包装"资产。0

  • 侧链:侧链是主链的辅助链,可用于扩展主链的功能。NFT 可以在主链和它的侧链之间转移,从而在多个相关联的区块链之间实现流动性。0

  • 跨链协议:跨链协议例如 Cosmos 或 Polkadot,旨在不同区块链之间创建可互操作的生态系统。通过这些协议,NFT 可以在兼容的区块链网络之间轻松地移动。0

  • 多链智能合约:某些 NFT 项目开发了能在多个区块链上运行的智能合约。这些合约确保即便 NFT 在不同的区块链上,其属性和所有权记录仍然是一致的。0

  • 分散式身份和元数据存储服务:例如使用 IPFS(InterPlanetary File System)等分散式文件存储系统来存储 NFT 的元数据或内容。这样可以确保尽管 NFT 代币本身可能在不同的区块链上存在,其链接的内容却是统一和持久的。0

  • 代码实现:多链 NFT DApp

    基础配置改造

    在前边的第四讲和第五讲中我们已经学习过了合约的调用和事件监听,现在我们在这个基础上继续学习支持多链:0

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

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

    多链合约配置

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

    多链合约调用组件

    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" }],
    },
    ],
    – // Sepolia test contract 0x418325c3979b7f8a17678ec2463a74355bdbe72c
    – address: "0xEcd0D12E21805803f70de03B72B1C162dB0898d9",
    + 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",
    },
    ],
    },
    ],
    address: contractInfo.find((item) => item.id === chain?.id)?.contractAddress as `0x${string}`,
    functionName: "mint",
    args: [1n],
    value: parseEther("0.01"),
    }
    );
    }}
    >
    Mint NFT
    </Button>
    </div>
    );
    };

    完整的多链 DApp 实现

    多链配置管理

    import { mainnet, sepolia, polygon, arbitrum, optimism } from "wagmi/chains";

    export interface ChainConfig {
    id: number;
    name: string;
    contractAddress: string;
    rpcUrl: string;
    blockExplorer: string;
    nativeCurrency: {
    name: string;
    symbol: string;
    decimals: number;
    };
    }

    export const supportedChains = [
    {
    id: mainnet.id,
    name: "Ethereum Mainnet",
    contractAddress: "0xEcd0D12E21805803f70de03B72B1C162dB0898d9",
    rpcUrl: "https://eth-mainnet.g.alchemy.com/v2/your-api-key",
    blockExplorer: "https://etherscan.io",
    nativeCurrency: {
    name: "Ether",
    symbol: "ETH",
    decimals: 18
    }
    },
    {
    id: sepolia.id,
    name: "Sepolia Testnet",
    contractAddress: "0x418325c3979b7f8a17678ec2463a74355bdbe72c",
    rpcUrl: "https://eth-sepolia.g.alchemy.com/v2/your-api-key",
    blockExplorer: "https://sepolia.etherscan.io",
    nativeCurrency: {
    name: "Sepolia Ether",
    symbol: "SEP",
    decimals: 18
    }
    },
    {
    id: polygon.id,
    name: "Polygon Mainnet",
    contractAddress: "0x418325c3979b7f8a17678ec2463a74355bdbe72c",
    rpcUrl: "https://polygon-mainnet.g.alchemy.com/v2/your-api-key",
    blockExplorer: "https://polygonscan.com",
    nativeCurrency: {
    name: "MATIC",
    symbol: "MATIC",
    decimals: 18
    }
    },
    {
    id: arbitrum.id,
    name: "Arbitrum One",
    contractAddress: "0x418325c3979b7f8a17678ec2463a74355bdbe72c",
    rpcUrl: "https://arb-mainnet.g.alchemy.com/v2/your-api-key",
    blockExplorer: "https://arbiscan.io",
    nativeCurrency: {
    name: "Ether",
    symbol: "ETH",
    decimals: 18
    }
    },
    {
    id: optimism.id,
    name: "Optimism",
    contractAddress: "0x418325c3979b7f8a17678ec2463a74355bdbe72c",
    rpcUrl: "https://opt-mainnet.g.alchemy.com/v2/your-api-key",
    blockExplorer: "https://optimistic.etherscan.io",
    nativeCurrency: {
    name: "Ether",
    symbol: "ETH",
    decimals: 18
    }
    }
    ];

    export function getChainConfig(chainId: number): ChainConfig | undefined {
    return supportedChains.find(chain => chain.id === chainId);
    }

    export function getContractAddress(chainId: number): string | undefined {
    return getChainConfig(chainId)?.contractAddress;
    }

    多链 NFT 组件

    import React, { useState, useEffect } from "react";
    import {
    Address,
    ConnectButton,
    NFTCard,
    useAccount,
    useProvider,
    } from "@ant-design/web3";
    import {
    WagmiWeb3ConfigProvider,
    MetaMask,
    Sepolia,
    Polygon,
    Arbitrum,
    Optimism,
    } from "@ant-design/web3-wagmi";
    import { Button, Card, Select, message, Spin, Row, Col, Statistic } from "antd";
    import { createConfig, http, useReadContract, useWriteContract, useSwitchChain } from "wagmi";
    import { mainnet, sepolia, polygon, arbitrum, optimism } from "wagmi/chains";
    import { injected } from "wagmi/connectors";
    import { parseEther, formatEther } from "viem";
    import { supportedChains, getContractAddress } from "../config/multichain";

    const { Option } = Select;

    // wagmi 配置
    const config = createConfig({
    chains: [mainnet, sepolia, polygon, arbitrum, optimism],
    transports: {
    [mainnet.id]: http(),
    [sepolia.id]: http(),
    [polygon.id]: http(),
    [arbitrum.id]: http(),
    [optimism.id]: http(),
    },
    connectors: [
    injected({
    target: "metaMask",
    }),
    ],
    });

    // NFT 合约 ABI
    const nftABI = [
    {
    type: "function",
    name: "balanceOf",
    stateMutability: "view",
    inputs: [{ name: "account", type: "address" }],
    outputs: [{ type: "uint256" }],
    },
    {
    type: "function",
    name: "tokenURI",
    stateMutability: "view",
    inputs: [{ name: "tokenId", type: "uint256" }],
    outputs: [{ type: "string" }],
    },
    {
    type: "function",
    name: "mint",
    stateMutability: "payable",
    inputs: [
    {
    internalType: "uint256",
    name: "quantity",
    type: "uint256",
    },
    ],
    outputs: [],
    },
    {
    type: "function",
    name: "totalSupply",
    stateMutability: "view",
    inputs: [],
    outputs: [{ type: "uint256" }],
    },
    ] as const;

    function MultiChainNFTComponent() {
    const { account } = useAccount();
    const { chain } = useProvider();
    const { switchChain } = useSwitchChain();
    const { writeContract, isPending: isMinting } = useWriteContract();
    const [selectedChain, setSelectedChain] = useState<number>(chain?.id || mainnet.id);

    // 获取当前链的合约地址
    const currentContractAddress = getContractAddress(chain?.id || mainnet.id);

    // 读取 NFT 余额
    const { data: balance, isLoading: isLoadingBalance } = useReadContract({
    address: currentContractAddress as `0x${string}`,
    abi: nftABI,
    functionName: "balanceOf",
    args: [account?.address as `0x${string}`],
    query: {
    enabled: !!account?.address && !!currentContractAddress,
    },
    });

    // 读取总供应量
    const { data: totalSupply, isLoading: isLoadingSupply } = useReadContract({
    address: currentContractAddress as `0x${string}`,
    abi: nftABI,
    functionName: "totalSupply",
    query: {
    enabled: !!currentContractAddress,
    },
    });

    // 读取第一个 NFT 的 URI
    const { data: tokenURI } = useReadContract({
    address: currentContractAddress as `0x${string}`,
    abi: nftABI,
    functionName: "tokenURI",
    args: [1n],
    query: {
    enabled: !!currentContractAddress && totalSupply && totalSupply > 0n,
    },
    });

    // 处理链切换
    const handleChainSwitch = async (chainId: number) => {
    setSelectedChain(chainId);
    if (chain?.id !== chainId) {
    try {
    await switchChain({ chainId });
    message.success(`已切换到 ${supportedChains.find(c => c.id === chainId)?.name}`);
    } catch (error) {
    console.error("切换链失败:", error);
    message.error("切换链失败");
    }
    }
    };

    // 处理 NFT 铸造
    const handleMint = async () => {
    if (!account?.address) {
    message.error("请先连接钱包");
    return;
    }

    if (!currentContractAddress) {
    message.error("当前链不支持此合约");
    return;
    }

    try {
    await writeContract({
    address: currentContractAddress as `0x${string}`,
    abi: nftABI,
    functionName: "mint",
    args: [1n],
    value: parseEther("0.01"),
    });
    message.success("NFT 铸造成功!");
    } catch (error) {
    console.error("铸造失败:", error);
    message.error("NFT 铸造失败");
    }
    };

    return (
    <div style={{ padding: "20px", maxWidth: "1200px", margin: "0 auto" }}>
    <h1>多链 NFT DApp</h1>

    {/* 连接钱包 */}
    <Card style={{ marginBottom: "20px" }}>
    <Row gutter={16} align="middle">
    <Col span={12}>
    <ConnectButton />
    </Col>
    <Col span={12}>
    {account && (
    <div>
    <p>当前账户:<Address address={account.address} /></p>
    <p>当前网络:{chain?.name}</p>
    </div>
    )}
    </Col>
    </Row>
    </Card>

    {/* 链选择器 */}
    <Card title="选择区块链网络" style={{ marginBottom: "20px" }}>
    <Select
    style={{ width: "100%" }}
    value={selectedChain}
    onChange={handleChainSwitch}
    placeholder="选择区块链网络"
    >
    {supportedChains.map((chainConfig) => (
    <Option key={chainConfig.id} value={chainConfig.id}>
    <div style={{ display: "flex", justifyContent: "space-between" }}>
    <span>{chainConfig.name}</span>
    <span style={{ color: "#666" }}>({chainConfig.nativeCurrency.symbol})</span>
    </div>
    </Option>
    ))}
    </Select>

    {chain?.id !== selectedChain && (
    <div style={{ marginTop: "10px", color: "orange" }}>
    ⚠️ 请切换到选择的网络以继续操作
    </div>
    )}
    </Card>

    {/* NFT 统计信息 */}
    <Row gutter={16} style={{ marginBottom: "20px" }}>
    <Col span={8}>
    <Card>
    <Statistic
    title="我的 NFT 数量"
    value={balance?.toString() || "0"}
    loading={isLoadingBalance}
    />
    </Card>
    </Col>
    <Col span={8}>
    <Card>
    <Statistic
    title="总供应量"
    value={totalSupply?.toString() || "0"}
    loading={isLoadingSupply}
    />
    </Card>
    </Col>
    <Col span={8}>
    <Card>
    <Statistic
    title="合约地址"
    value={currentContractAddress ? `${currentContractAddress.slice(0, 6)}…${currentContractAddress.slice(-4)}` : "未部署"}
    />
    </Card>
    </Col>
    </Row>

    {/* NFT 展示和铸造 */}
    <Row gutter={16}>
    <Col span={12}>
    <Card title="NFT 预览">
    {tokenURI && currentContractAddress ? (
    <NFTCard
    address={currentContractAddress}
    tokenId={1}
    style={{ width: "100%" }}
    />
    ) : (
    <div style={{
    height: "300px",
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    background: "#f5f5f5",
    borderRadius: "8px"
    }}>
    <span style={{ color: "#999" }}>暂无 NFT 数据</span>
    </div>
    )}
    </Card>
    </Col>

    <Col span={12}>
    <Card title="铸造 NFT">
    <div style={{ textAlign: "center" }}>
    <p>铸造价格:0.01 {chain?.nativeCurrency?.symbol || "ETH"}</p>
    <p>当前网络:{chain?.name}</p>

    <Button
    type="primary"
    size="large"
    onClick={handleMint}
    disabled={!account || !currentContractAddress || chain?.id !== selectedChain}
    loading={isMinting}
    style={{ width: "100%", marginTop: "20px" }}
    >
    {isMinting ? "铸造中…" : "铸造 NFT"}
    </Button>

    {!currentContractAddress && (
    <p style={{ color: "red", marginTop: "10px" }}>
    当前链暂不支持此合约
    </p>
    )}
    </div>
    </Card>
    </Col>
    </Row>

    {/* 链信息展示 */}
    <Card title="支持的区块链网络" style={{ marginTop: "20px" }}>
    <Row gutter={16}>
    {supportedChains.map((chainConfig) => (
    <Col span={6} key={chainConfig.id}>
    <Card
    size="small"
    style={{
    border: chain?.id === chainConfig.id ? "2px solid #1890ff" : "1px solid #d9d9d9"
    }}
    >
    <div style={{ textAlign: "center" }}>
    <h4>{chainConfig.name}</h4>
    <p>货币:{chainConfig.nativeCurrency.symbol}</p>
    <p style={{ fontSize: "12px", color: "#666" }}>
    合约:{chainConfig.contractAddress.slice(0, 6)}…{chainConfig.contractAddress.slice(-4)}
    </p>
    {chain?.id === chainConfig.id && (
    <div style={{ color: "#1890ff", fontWeight: "bold" }}>当前网络</div>
    )}
    </div>
    </Card>
    </Col>
    ))}
    </Row>
    </Card>
    </div>
    );
    }

    export default function MultiChainNFTDApp() {
    return (
    <WagmiWeb3ConfigProvider
    config={config}
    wallets={[
    MetaMask(),
    // 可以根据需要添加其他钱包
    ]}
    chains={[
    Sepolia(),
    Polygon(),
    Arbitrum(),
    Optimism(),
    ]}
    >
    <MultiChainNFTComponent />
    </WagmiWeb3ConfigProvider>
    );
    }

    高级多链功能

    跨链资产查询

    import { useEffect, useState } from "react";
    import { useAccount } from "wagmi";
    import { supportedChains, getContractAddress } from "../config/multichain";

    interface ChainBalance {
    chainId: number;
    chainName: string;
    balance: bigint;
    contractAddress: string;
    }

    export function useMultiChainBalance() {
    const { address } = useAccount();
    const [balances, setBalances] = useState<ChainBalance[]>([]);
    const [isLoading, setIsLoading] = useState(false);

    useEffect(() => {
    if (!address) return;

    const fetchBalances = async () => {
    setIsLoading(true);
    const balancePromises = supportedChains.map(async (chain) => {
    try {
    const contractAddress = getContractAddress(chain.id);
    if (!contractAddress) return null;

    // 这里需要为每个链创建单独的 provider
    // 实际实现中可能需要使用 viem 的 createPublicClient
    const response = await fetch(chain.rpcUrl, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
    jsonrpc: "2.0",
    method: "eth_call",
    params: [
    {
    to: contractAddress,
    data: `0x70a08231000000000000000000000000${address.slice(2)}`, // balanceOf(address)
    },
    "latest",
    ],
    id: 1,
    }),
    });

    const result = await response.json();
    const balance = BigInt(result.result || "0");

    return {
    chainId: chain.id,
    chainName: chain.name,
    balance,
    contractAddress,
    };
    } catch (error) {
    console.error(`获取 ${chain.name} 余额失败:`, error);
    return null;
    }
    });

    const results = await Promise.all(balancePromises);
    setBalances(results.filter(Boolean) as ChainBalance[]);
    setIsLoading(false);
    };

    fetchBalances();
    }, [address]);

    return { balances, isLoading };
    }

    多链资产展示组件

    import React from "react";
    import { Card, Table, Tag } from "antd";
    import { useMultiChainBalance } from "../hooks/useMultiChainBalance";
    import { supportedChains } from "../config/multichain";

    function MultiChainAssets() {
    const { balances, isLoading } = useMultiChainBalance();

    const columns = [
    {
    title: "区块链网络",
    dataIndex: "chainName",
    key: "chainName",
    render: (text: string, record: any) => {
    const chain = supportedChains.find(c => c.id === record.chainId);
    return (
    <div>
    <div>{text}</div>
    <Tag color="blue">{chain?.nativeCurrency.symbol}</Tag>
    </div>
    );
    },
    },
    {
    title: "NFT 数量",
    dataIndex: "balance",
    key: "balance",
    render: (balance: bigint) => balance.toString(),
    },
    {
    title: "合约地址",
    dataIndex: "contractAddress",
    key: "contractAddress",
    render: (address: string) => (
    <code>{address.slice(0, 6)}…{address.slice(-4)}</code>
    ),
    },
    ];

    return (
    <Card title="多链资产概览" loading={isLoading}>
    <Table
    columns={columns}
    dataSource={balances}
    rowKey="chainId"
    pagination={false}
    size="small"
    />
    </Card>
    );
    }

    export default MultiChainAssets;

    跨链桥集成

    跨链转移组件

    import React, { useState } from "react";
    import { Card, Select, Button, InputNumber, message, Steps } from "antd";
    import { ArrowRightOutlined } from "@ant-design/icons";
    import { supportedChains } from "../config/multichain";

    const { Option } = Select;
    const { Step } = Steps;

    interface BridgeTransaction {
    fromChain: number;
    toChain: number;
    tokenId: number;
    status: "pending" | "confirmed" | "failed";
    }

    function CrossChainBridge() {
    const [fromChain, setFromChain] = useState<number>();
    const [toChain, setToChain] = useState<number>();
    const [tokenId, setTokenId] = useState<number>();
    const [isTransferring, setIsTransferring] = useState(false);
    const [currentStep, setCurrentStep] = useState(0);

    const handleTransfer = async () => {
    if (!fromChain || !toChain || !tokenId) {
    message.error("请填写完整的转移信息");
    return;
    }

    if (fromChain === toChain) {
    message.error("源链和目标链不能相同");
    return;
    }

    setIsTransferring(true);
    setCurrentStep(0);

    try {
    // 步骤 1: 锁定源链上的 NFT
    setCurrentStep(1);
    await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟交易

    // 步骤 2: 等待确认
    setCurrentStep(2);
    await new Promise(resolve => setTimeout(resolve, 3000)); // 模拟确认

    // 步骤 3: 在目标链上铸造
    setCurrentStep(3);
    await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟铸造

    setCurrentStep(4);
    message.success("跨链转移成功!");
    } catch (error) {
    console.error("跨链转移失败:", error);
    message.error("跨链转移失败");
    } finally {
    setIsTransferring(false);
    }
    };

    const steps = [
    {
    title: "准备转移",
    description: "验证转移参数",
    },
    {
    title: "锁定资产",
    description: "在源链上锁定 NFT",
    },
    {
    title: "等待确认",
    description: "等待区块确认",
    },
    {
    title: "铸造资产",
    description: "在目标链上铸造 NFT",
    },
    {
    title: "完成",
    description: "跨链转移完成",
    },
    ];

    return (
    <Card title="跨链 NFT 转移">
    <div style={{ marginBottom: "20px" }}>
    <div style={{ display: "flex", alignItems: "center", gap: "16px", marginBottom: "16px" }}>
    <div style={{ flex: 1 }}>
    <label>源链:</label>
    <Select
    style={{ width: "100%" }}
    placeholder="选择源链"
    value={fromChain}
    onChange={setFromChain}
    >
    {supportedChains.map((chain) => (
    <Option key={chain.id} value={chain.id}>
    {chain.name}
    </Option>
    ))}
    </Select>
    </div>

    <ArrowRightOutlined style={{ fontSize: "20px", color: "#1890ff" }} />

    <div style={{ flex: 1 }}>
    <label>目标链:</label>
    <Select
    style={{ width: "100%" }}
    placeholder="选择目标链"
    value={toChain}
    onChange={setToChain}
    >
    {supportedChains.map((chain) => (
    <Option key={chain.id} value={chain.id} disabled={chain.id === fromChain}>
    {chain.name}
    </Option>
    ))}
    </Select>
    </div>
    </div>

    <div style={{ marginBottom: "16px" }}>
    <label>NFT Token ID:</label>
    <InputNumber
    style={{ width: "100%" }}
    placeholder="输入要转移的 NFT Token ID"
    value={tokenId}
    onChange={(value) => setTokenId(value || undefined)}
    min={1}
    />
    </div>

    <Button
    type="primary"
    onClick={handleTransfer}
    loading={isTransferring}
    disabled={!fromChain || !toChain || !tokenId}
    style={{ width: "100%" }}
    >
    {isTransferring ? "转移中…" : "开始跨链转移"}
    </Button>
    </div>

    {isTransferring && (
    <Steps current={currentStep} size="small">
    {steps.map((step, index) => (
    <Step key={index} title={step.title} description={step.description} />
    ))}
    </Steps>
    )}
    </Card>
    );
    }

    export default CrossChainBridge;

    性能优化

    链数据缓存

    interface CacheItem<T> {
    data: T;
    timestamp: number;
    expiry: number;
    }

    class ChainDataCache {
    private cache = new Map<string, CacheItem<any>>();
    private defaultTTL = 5 * 60 * 1000; // 5分钟

    set<T>(key: string, data: T, ttl?: number): void {
    const expiry = Date.now() + (ttl || this.defaultTTL);
    this.cache.set(key, {
    data,
    timestamp: Date.now(),
    expiry,
    });
    }

    get<T>(key: string): T | null {
    const item = this.cache.get(key);
    if (!item) return null;

    if (Date.now() > item.expiry) {
    this.cache.delete(key);
    return null;
    }

    return item.data;
    }

    clear(): void {
    this.cache.clear();
    }

    // 生成缓存键
    static generateKey(chainId: number, method: string, params: any[]): string {
    return `${chainId}:${method}:${JSON.stringify(params)}`;
    }
    }

    export const chainCache = new ChainDataCache();

    批量查询优化

    import { useEffect, useState } from "react";
    import { useAccount } from "wagmi";
    import { supportedChains } from "../config/multichain";
    import { chainCache } from "../utils/chainCache";

    interface BatchQueryResult {
    chainId: number;
    balance: bigint;
    totalSupply: bigint;
    isLoading: boolean;
    error?: string;
    }

    export function useBatchMultiChainData() {
    const { address } = useAccount();
    const [results, setResults] = useState<BatchQueryResult[]>([]);
    const [isLoading, setIsLoading] = useState(false);

    useEffect(() => {
    if (!address) return;

    const fetchBatchData = async () => {
    setIsLoading(true);

    const batchPromises = supportedChains.map(async (chain) => {
    const cacheKey = chainCache.generateKey(chain.id, "batchQuery", [address]);
    const cached = chainCache.get<BatchQueryResult>(cacheKey);

    if (cached) {
    return cached;
    }

    try {
    // 批量查询多个合约方法
    const multicallData = [
    // balanceOf 调用
    {
    target: chain.contractAddress,
    callData: `0x70a08231000000000000000000000000${address.slice(2)}`,
    },
    // totalSupply 调用
    {
    target: chain.contractAddress,
    callData: "0x18160ddd", // totalSupply()
    },
    ];

    // 使用 multicall 合约进行批量查询
    const response = await fetch(chain.rpcUrl, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
    jsonrpc: "2.0",
    method: "eth_call",
    params: [
    {
    to: "0xcA11bde05977b3631167028862bE2a173976CA11", // Multicall3 合约
    data: "0x…", // 编码的 multicall 数据
    },
    "latest",
    ],
    id: 1,
    }),
    });

    const result = await response.json();

    // 解析批量查询结果
    const balance = BigInt(result.result?.slice(0, 66) || "0");
    const totalSupply = BigInt(result.result?.slice(66, 132) || "0");

    const queryResult: BatchQueryResult = {
    chainId: chain.id,
    balance,
    totalSupply,
    isLoading: false,
    };

    // 缓存结果
    chainCache.set(cacheKey, queryResult, 2 * 60 * 1000); // 2分钟缓存

    return queryResult;
    } catch (error) {
    console.error(`批量查询 ${chain.name} 失败:`, error);
    return {
    chainId: chain.id,
    balance: 0n,
    totalSupply: 0n,
    isLoading: false,
    error: error instanceof Error ? error.message : "查询失败",
    };
    }
    });

    const batchResults = await Promise.all(batchPromises);
    setResults(batchResults);
    setIsLoading(false);
    };

    fetchBatchData();
    }, [address]);

    return { results, isLoading };
    }

    错误处理和用户体验

    网络错误处理

    import React from "react";
    import { Alert, Button, Space } from "antd";
    import { useAccount, useProvider } from "@ant-design/web3";
    import { useSwitchChain } from "wagmi";
    import { supportedChains } from "../config/multichain";

    interface NetworkErrorHandlerProps {
    children: React.ReactNode;
    }

    function NetworkErrorHandler({ children }: NetworkErrorHandlerProps) {
    const { account } = useAccount();
    const { chain } = useProvider();
    const { switchChain } = useSwitchChain();

    // 检查是否连接了不支持的网络
    const isUnsupportedNetwork = chain && !supportedChains.some(c => c.id === chain.id);

    // 检查是否未连接钱包
    const isNotConnected = !account;

    if (isNotConnected) {
    return (
    <Alert
    message="请连接钱包"
    description="您需要连接钱包才能使用此 DApp"
    type="warning"
    showIcon
    style={{ margin: "20px" }}
    />
    );
    }

    if (isUnsupportedNetwork) {
    return (
    <Alert
    message="不支持的网络"
    description={
    <div>
    <p>当前连接的网络不受支持。请切换到以下网络之一:</p>
    <Space wrap>
    {supportedChains.map((supportedChain) => (
    <Button
    key={supportedChain.id}
    size="small"
    onClick={() => switchChain({ chainId: supportedChain.id })}
    >
    {supportedChain.name}
    </Button>
    ))}
    </Space>
    </div>
    }
    type="error"
    showIcon
    style={{ margin: "20px" }}
    />
    );
    }

    return <>{children}</>;
    }

    export default NetworkErrorHandler;

    交易状态跟踪

    import { useState, useEffect } from "react";
    import { useWaitForTransactionReceipt } from "wagmi";
    import { message } from "antd";

    interface TransactionState {
    hash?: string;
    status: "idle" | "pending" | "success" | "error";
    error?: string;
    }

    export function useTransactionTracker() {
    const [transactions, setTransactions] = useState<Map<string, TransactionState>>(new Map());

    const addTransaction = (hash: string, chainId: number) => {
    setTransactions(prev => new Map(prev.set(hash, {
    hash,
    status: "pending",
    })));

    message.loading({
    content: `交易提交成功,等待确认… (${hash.slice(0, 10)}…)`,
    key: hash,
    duration: 0,
    });
    };

    const updateTransaction = (hash: string, update: Partial<TransactionState>) => {
    setTransactions(prev => {
    const current = prev.get(hash);
    if (!current) return prev;

    const updated = { …current, …update };
    const newMap = new Map(prev);
    newMap.set(hash, updated);

    // 更新消息提示
    if (updated.status === "success") {
    message.success({
    content: `交易确认成功! (${hash.slice(0, 10)}…)`,
    key: hash,
    });
    } else if (updated.status === "error") {
    message.error({
    content: `交易失败: ${updated.error} (${hash.slice(0, 10)}…)`,
    key: hash,
    });
    }

    return newMap;
    });
    };

    return {
    transactions,
    addTransaction,
    updateTransaction,
    };
    }

    安全注意事项

    1. 合约地址验证

    export function validateContractAddress(address: string, chainId: number): boolean {
    // 验证地址格式
    if (!/^0x[a-fA-F0-9]{40}$/.test(address)) {
    return false;
    }

    // 验证是否为已知的合约地址
    const knownAddress = getContractAddress(chainId);
    return address.toLowerCase() === knownAddress?.toLowerCase();
    }

    2. 交易参数验证

    export function validateMintTransaction(chainId: number, value: bigint): boolean {
    const chainConfig = getChainConfig(chainId);
    if (!chainConfig) return false;

    // 验证支付金额
    const expectedValue = parseEther("0.01");
    if (value !== expectedValue) {
    throw new Error(`错误的支付金额,期望 ${formatEther(expectedValue)} ${chainConfig.nativeCurrency.symbol}`);
    }

    return true;
    }

    3. 网络安全检查

    export function checkNetworkSecurity(chainId: number): {
    isSecure: boolean;
    warnings: string[];
    } {
    const warnings: string[] = [];

    // 检查是否为测试网
    const testnetIds = [5, 11155111]; // Goerli, Sepolia
    if (testnetIds.includes(chainId)) {
    warnings.push("当前为测试网络,请勿使用真实资产");
    }

    // 检查是否为已知的安全网络
    const secureNetworks = [1, 137, 42161, 10]; // Mainnet, Polygon, Arbitrum, Optimism
    const isSecure = secureNetworks.includes(chainId) || testnetIds.includes(chainId);

    if (!isSecure) {
    warnings.push("未知网络,请谨慎操作");
    }

    return { isSecure, warnings };
    }

    总结

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

  • 多链概念:了解了 DApp 支持多链的意义和优势
  • 技术实现:学习了跨链桥、侧链、跨链协议等核心技术
  • 代码实现:实现了完整的多链 NFT DApp,支持多个区块链网络
  • 高级功能:跨链资产查询、跨链转移、批量查询优化
  • 用户体验:网络错误处理、交易状态跟踪、性能优化
  • 安全考虑:合约验证、交易验证、网络安全检查
  • 多链支持是现代 DApp 的重要特性,它能够:

    • 提供更好的用户体验和更低的交易费用
    • 利用不同链的技术优势
    • 扩大用户群体和市场覆盖
    • 提高 DApp 的可扩展性和灵活性

    虽然这可能会增加开发和维护的复杂性,但鉴于其带来的多元化和扩展性优势,越来越多的 DApp 选择支持多个区块链。0

    下一讲预告

    下一讲我们将学习"DApp 安全最佳实践",包括:

    • 智能合约安全审计
    • 前端安全防护
    • 用户资产安全
    • 常见攻击防范

    敬请期待!

    赞(0)
    未经允许不得转载:171主机测评 » web3开发第11讲
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址