前端与智能合约交互

核心知识点讲解

前端与智能合约交互的基本流程

前端应用与智能合约交互的基本流程包括:

  1. 连接钱包:获取用户的以太坊钱包地址
  2. 创建Provider:连接到以太坊网络
  3. 创建合约实例:使用合约ABI和地址创建合约实例
  4. 调用合约方法:调用合约的读取和写入方法
  5. 监听事件:监听合约触发的事件
  6. 处理交易:处理交易的提交、确认和状态更新

前端框架集成

常见的前端框架与Web3集成:

  • React:使用useState、useEffect等钩子管理状态和副作用
  • Vue:使用data、computed、watch等选项管理状态
  • Angular:使用组件、服务等管理状态和逻辑

钱包连接

前端应用需要连接用户的钱包:

  • MetaMask:最流行的以太坊钱包
  • WalletConnect:支持多种钱包的连接协议
  • Coinbase Wallet:Coinbase提供的钱包
  • 其他钱包:如Trust Wallet、Ledger等

交易处理

前端应用需要处理交易的各个阶段:

  • 交易签名:用户确认交易
  • 交易提交:将交易提交到网络
  • 交易确认:等待交易被打包确认
  • 交易状态:更新UI显示交易状态

实用案例分析

React与智能合约交互

// React组件与智能合约交互
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';

// 合约ABI
const contractABI = [
  {
    "inputs": [{"internalType": "uint256", "name": "_value", "type": "uint256"}],
    "name": "setValue",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "getValue",
    "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "anonymous": false,
    "inputs": [{"indexed": false, "internalType": "uint256", "name": "value", "type": "uint256"}],
    "name": "ValueChanged",
    "type": "event"
  }
];

// 合约地址
const contractAddress = '0x1234567890123456789012345678901234567890';

function SmartContractInteraction() {
  const [provider, setProvider] = useState(null);
  const [signer, setSigner] = useState(null);
  const [contract, setContract] = useState(null);
  const [value, setValue] = useState('0');
  const [newValue, setNewValue] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  const [transactionHash, setTransactionHash] = useState('');

  // 连接钱包
  const connectWallet = async () => {
    try {
      if (!window.ethereum) {
        throw new Error('MetaMask not detected');
      }

      // 请求用户授权
      const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
      console.log('Connected accounts:', accounts);

      // 创建Provider和Signer
      const provider = new ethers.BrowserProvider(window.ethereum);
      const signer = await provider.getSigner();

      // 创建合约实例
      const contract = new ethers.Contract(contractAddress, contractABI, signer);

      setProvider(provider);
      setSigner(signer);
      setContract(contract);
      setError('');
    } catch (err) {
      setError('Failed to connect wallet: ' + err.message);
      console.error('Wallet connection error:', err);
    }
  };

  // 加载合约值
  const loadValue = async () => {
    if (!contract) {
      setError('Please connect wallet first');
      return;
    }

    try {
      setLoading(true);
      const contractValue = await contract.getValue();
      setValue(contractValue.toString());
      setError('');
    } catch (err) {
      setError('Failed to load value: ' + err.message);
      console.error('Load value error:', err);
    } finally {
      setLoading(false);
    }
  };

  // 设置合约值
  const updateValue = async () => {
    if (!contract) {
      setError('Please connect wallet first');
      return;
    }

    if (!newValue || isNaN(newValue)) {
      setError('Please enter a valid number');
      return;
    }

    try {
      setLoading(true);
      const tx = await contract.setValue(parseInt(newValue));
      setTransactionHash(tx.hash);
      console.log('Transaction submitted:', tx.hash);

      // 等待交易确认
      await tx.wait();
      console.log('Transaction confirmed');

      // 重新加载值
      await loadValue();
      setNewValue('');
      setError('');
    } catch (err) {
      setError('Failed to update value: ' + err.message);
      console.error('Update value error:', err);
    } finally {
      setLoading(false);
    }
  };

  // 监听合约事件
  useEffect(() => {
    if (contract) {
      // 监听ValueChanged事件
      const listener = contract.on('ValueChanged', (newValue) => {
        console.log('Event received:', newValue.toString());
        setValue(newValue.toString());
      });

      // 清理函数
      return () => {
        if (listener) {
          listener.removeAllListeners();
        }
      };
    }
  }, [contract]);

  // 初始加载
  useEffect(() => {
    if (contract) {
      loadValue();
    }
  }, [contract]);

  return (
    <div className="smart-contract-interaction">
      <h1>智能合约交互</h1>
      
      {!provider ? (
        <button onClick={connectWallet} disabled={loading}>
          {loading ? 'Connecting...' : 'Connect Wallet'}
        </button>
      ) : (
        <div>
          <p>Connected to wallet: {signer && signer.address}</p>
          
          <div className="value-section">
            <h2>Current Value: {value}</h2>
            <button onClick={loadValue} disabled={loading}>
              {loading ? 'Loading...' : 'Load Value'}
            </button>
          </div>
          
          <div className="update-section">
            <h2>Update Value</h2>
            <input
              type="number"
              value={newValue}
              onChange={(e) => setNewValue(e.target.value)}
              placeholder="Enter new value"
            />
            <button onClick={updateValue} disabled={loading}>
              {loading ? 'Updating...' : 'Update Value'}
            </button>
          </div>
          
          {transactionHash && (
            <div className="transaction-section">
              <h3>Transaction Hash:</h3>
              <p>{transactionHash}</p>
            </div>
          )}
        </div>
      )}
      
      {error && (
        <div className="error">
          <p>{error}</p>
        </div>
      )}
    </div>
  );
}

export default SmartContractInteraction;

Vue与智能合约交互

<template>
  <div class="smart-contract-interaction">
    <h1>智能合约交互</h1>
    
    <button v-if="!provider" @click="connectWallet" :disabled="loading">
      {{ loading ? 'Connecting...' : 'Connect Wallet' }}
    </button>
    
    <div v-else>
      <p>Connected to wallet: {{ signerAddress }}</p>
      
      <div class="value-section">
        <h2>Current Value: {{ value }}</h2>
        <button @click="loadValue" :disabled="loading">
          {{ loading ? 'Loading...' : 'Load Value' }}
        </button>
      </div>
      
      <div class="update-section">
        <h2>Update Value</h2>
        <input
          type="number"
          v-model="newValue"
          placeholder="Enter new value"
        />
        <button @click="updateValue" :disabled="loading">
          {{ loading ? 'Updating...' : 'Update Value' }}
        </button>
      </div>
      
      <div v-if="transactionHash" class="transaction-section">
        <h3>Transaction Hash:</h3>
        <p>{{ transactionHash }}</p>
      </div>
    </div>
    
    <div v-if="error" class="error">
      <p>{{ error }}</p>
    </div>
  </div>
</template>

<script>
import { ethers } from 'ethers';

export default {
  name: 'SmartContractInteraction',
  data() {
    return {
      provider: null,
      signer: null,
      contract: null,
      signerAddress: '',
      value: '0',
      newValue: '',
      loading: false,
      error: '',
      transactionHash: ''
    };
  },
  mounted() {
    // 组件挂载时检查是否已有钱包连接
    this.checkWalletConnection();
  },
  methods: {
    async checkWalletConnection() {
      if (window.ethereum) {
        try {
          const accounts = await window.ethereum.request({ method: 'eth_accounts' });
          if (accounts.length > 0) {
            await this.setupProvider(accounts[0]);
          }
        } catch (err) {
          console.error('Error checking wallet connection:', err);
        }
      }
    },
    async connectWallet() {
      try {
        if (!window.ethereum) {
          throw new Error('MetaMask not detected');
        }

        const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
        await this.setupProvider(accounts[0]);
        this.error = '';
      } catch (err) {
        this.error = 'Failed to connect wallet: ' + err.message;
        console.error('Wallet connection error:', err);
      }
    },
    async setupProvider(address) {
      try {
        this.loading = true;
        const provider = new ethers.BrowserProvider(window.ethereum);
        const signer = await provider.getSigner();
        
        // 合约ABI
        const contractABI = [
          {
            "inputs": [{"internalType": "uint256", "name": "_value", "type": "uint256"}],
            "name": "setValue",
            "outputs": [],
            "stateMutability": "nonpayable",
            "type": "function"
          },
          {
            "inputs": [],
            "name": "getValue",
            "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}],
            "stateMutability": "view",
            "type": "function"
          },
          {
            "anonymous": false,
            "inputs": [{"indexed": false, "internalType": "uint256", "name": "value", "type": "uint256"}],
            "name": "ValueChanged",
            "type": "event"
          }
        ];
        
        // 合约地址
        const contractAddress = '0x1234567890123456789012345678901234567890';
        
        const contract = new ethers.Contract(contractAddress, contractABI, signer);
        
        this.provider = provider;
        this.signer = signer;
        this.contract = contract;
        this.signerAddress = address;
        
        // 监听事件
        this.setupEventListeners();
        
        // 加载初始值
        await this.loadValue();
      } catch (err) {
        this.error = 'Failed to setup provider: ' + err.message;
        console.error('Setup provider error:', err);
      } finally {
        this.loading = false;
      }
    },
    async loadValue() {
      if (!this.contract) {
        this.error = 'Please connect wallet first';
        return;
      }

      try {
        this.loading = true;
        const contractValue = await this.contract.getValue();
        this.value = contractValue.toString();
        this.error = '';
      } catch (err) {
        this.error = 'Failed to load value: ' + err.message;
        console.error('Load value error:', err);
      } finally {
        this.loading = false;
      }
    },
    async updateValue() {
      if (!this.contract) {
        this.error = 'Please connect wallet first';
        return;
      }

      if (!this.newValue || isNaN(this.newValue)) {
        this.error = 'Please enter a valid number';
        return;
      }

      try {
        this.loading = true;
        const tx = await this.contract.setValue(parseInt(this.newValue));
        this.transactionHash = tx.hash;
        console.log('Transaction submitted:', tx.hash);

        await tx.wait();
        console.log('Transaction confirmed');

        await this.loadValue();
        this.newValue = '';
        this.error = '';
      } catch (err) {
        this.error = 'Failed to update value: ' + err.message;
        console.error('Update value error:', err);
      } finally {
        this.loading = false;
      }
    },
    setupEventListeners() {
      if (this.contract) {
        this.contract.on('ValueChanged', (newValue) => {
          console.log('Event received:', newValue.toString());
          this.value = newValue.toString();
        });
      }
    }
  }
};
</script>

<style scoped>
.smart-contract-interaction {
  padding: 20px;
  max-width: 600px;
  margin: 0 auto;
}

.value-section,
.update-section,
.transaction-section {
  margin: 20px 0;
  padding: 15px;
  border: 1px solid #ddd;
  border-radius: 5px;
}

.error {
  color: red;
  margin: 10px 0;
  padding: 10px;
  background-color: #ffe6e6;
  border-radius: 5px;
}

button {
  margin: 5px;
  padding: 10px 15px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
}

button:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}

input {
  padding: 10px;
  margin: 5px;
  border: 1px solid #ddd;
  border-radius: 5px;
}
</style>

钱包连接管理

// 钱包连接管理组件
import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';

function WalletConnector() {
  const [provider, setProvider] = useState(null);
  const [signer, setSigner] = useState(null);
  const [address, setAddress] = useState('');
  const [balance, setBalance] = useState('0');
  const [network, setNetwork] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  // 连接钱包
  const connectWallet = async () => {
    try {
      if (!window.ethereum) {
        throw new Error('No wallet detected');
      }

      setLoading(true);
      setError('');

      // 请求账户访问
      const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
      const address = accounts[0];

      // 创建Provider和Signer
      const provider = new ethers.BrowserProvider(window.ethereum);
      const signer = await provider.getSigner();

      // 获取网络信息
      const network = await provider.getNetwork();

      // 获取余额
      const balance = await provider.getBalance(address);
      const balanceInEther = ethers.formatEther(balance);

      setProvider(provider);
      setSigner(signer);
      setAddress(address);
      setBalance(balanceInEther);
      setNetwork(network);
    } catch (err) {
      setError('Failed to connect wallet: ' + err.message);
      console.error('Wallet connection error:', err);
    } finally {
      setLoading(false);
    }
  };

  // 断开钱包
  const disconnectWallet = () => {
    setProvider(null);
    setSigner(null);
    setAddress('');
    setBalance('0');
    setNetwork(null);
    setError('');
  };

  // 监听账户变化
  useEffect(() => {
    if (window.ethereum) {
      const handleAccountsChanged = (accounts) => {
        if (accounts.length === 0) {
          disconnectWallet();
        } else {
          setAddress(accounts[0]);
        }
      };

      const handleNetworkChanged = async (networkId) => {
        if (provider) {
          const network = await provider.getNetwork();
          setNetwork(network);
        }
      };

      window.ethereum.on('accountsChanged', handleAccountsChanged);
      window.ethereum.on('networkChanged', handleNetworkChanged);

      return () => {
        window.ethereum.off('accountsChanged', handleAccountsChanged);
        window.ethereum.off('networkChanged', handleNetworkChanged);
      };
    }
  }, [provider]);

  return (
    <div className="wallet-connector">
      <h2>Wallet Connection</h2>
      
      {!provider ? (
        <button onClick={connectWallet} disabled={loading}>
          {loading ? 'Connecting...' : 'Connect Wallet'}
        </button>
      ) : (
        <div>
          <p><strong>Address:</strong> {address}</p>
          <p><strong>Balance:</strong> {balance} ETH</p>
          <p><strong>Network:</strong> {network?.name} (Chain ID: {network?.chainId})</p>
          <button onClick={disconnectWallet}>Disconnect</button>
        </div>
      )}
      
      {error && (
        <div className="error">
          <p>{error}</p>
        </div>
      )}
    </div>
  );
}

export default WalletConnector;

交易状态管理

// 交易状态管理组件
import React, { useState } from 'react';
import { ethers } from 'ethers';

function TransactionManager({ contract }) {
  const [transactionState, setTransactionState] = useState({
    status: 'idle', // idle, pending, confirmed, failed
    hash: '',
    error: ''
  });

  const executeTransaction = async (transactionFunction, ...args) => {
    if (!contract) {
      setTransactionState({
        status: 'failed',
        hash: '',
        error: 'Contract not initialized'
      });
      return null;
    }

    try {
      setTransactionState({
        status: 'pending',
        hash: '',
        error: ''
      });

      const tx = await transactionFunction(...args);
      setTransactionState(prev => ({
        ...prev,
        hash: tx.hash
      }));

      const receipt = await tx.wait();
      setTransactionState({
        status: 'confirmed',
        hash: tx.hash,
        error: ''
      });

      return receipt;
    } catch (error) {
      setTransactionState({
        status: 'failed',
        hash: '',
        error: error.message
      });
      return null;
    }
  };

  const resetState = () => {
    setTransactionState({
      status: 'idle',
      hash: '',
      error: ''
    });
  };

  return (
    <div className="transaction-manager">
      <h3>Transaction Status</h3>
      
      {transactionState.status === 'idle' && (
        <p>Ready to execute transactions</p>
      )}
      
      {transactionState.status === 'pending' && (
        <div className="pending">
          <p>Transaction pending...</p>
          {transactionState.hash && (
            <p>Transaction hash: {transactionState.hash}</p>
          )}
        </div>
      )}
      
      {transactionState.status === 'confirmed' && (
        <div className="confirmed">
          <p>Transaction confirmed!</p>
          <p>Transaction hash: {transactionState.hash}</p>
          <button onClick={resetState}>Reset</button>
        </div>
      )}
      
      {transactionState.status === 'failed' && (
        <div className="failed">
          <p>Transaction failed</p>
          <p>Error: {transactionState.error}</p>
          <button onClick={resetState}>Reset</button>
        </div>
      )}
    </div>
  );
}

export default TransactionManager;

实践练习

  1. 创建React应用与智能合约交互

    • 初始化React项目
    • 安装Ethers.js
    • 实现钱包连接功能
    • 与智能合约交互
    • 处理交易状态
  2. 创建Vue应用与智能合约交互

    • 初始化Vue项目
    • 安装Ethers.js
    • 实现钱包连接功能
    • 与智能合约交互
    • 处理交易状态
  3. 实现钱包连接管理

    • 检测钱包是否安装
    • 处理账户切换
    • 处理网络切换
    • 显示账户信息和余额
  4. 实现交易状态管理

    • 显示交易提交状态
    • 显示交易确认状态
    • 处理交易失败
    • 提供交易哈希查询
  5. 实现事件监听

    • 监听智能合约事件
    • 实时更新UI
    • 处理事件数据

总结

前端与智能合约交互是Web3开发的核心部分,它使去中心化应用能够与区块链上的智能合约进行通信。通过本集的学习,你应该能够:

  • 连接钱包:使用Ethers.js连接用户的以太坊钱包
  • 创建合约实例:使用合约ABI和地址创建合约实例
  • 调用合约方法:调用合约的读取和写入方法
  • 处理交易:处理交易的提交、确认和状态更新
  • 监听事件:监听合约触发的事件并更新UI

在实际开发中,前端应用与智能合约交互需要考虑用户体验、安全性和错误处理。通过合理的状态管理和错误处理,可以创建出用户友好的Web3应用。

前端框架(如React和Vue)提供了良好的状态管理和UI渲染能力,与Ethers.js结合使用,可以构建出功能完整、用户体验良好的Web3应用。

« 上一篇 Ethers.js基础 下一篇 » 钱包连接与认证