Skip to content

自签名交易

本文档详细介绍如何在 React 应用中实现 VeChain 自签名交易功能。自签名交易是指使用用户的私钥在本地对交易进行签名,无需通过钱包插件或外部服务,适用于需要自动化批量交易的场景。

核心概念

  • 自签名:使用私钥直接在代码中对交易进行签名,而非通过钱包插件
  • 代付 Gas:使用指定的代付账户支付交易的 Gas 费用,让用户无需持有 VTHO
  • 批量交易:支持同时处理多个账户的交易,并统一管理状态

useSelfSignature

这是一个自定义 Hook,封装了使用私钥进行交易签名和发送的完整流程,支持多账户并发交易和状态管理。

ts
import { useState } from 'react';
import { ThorClient } from '@vechain/sdk-network';
import { HexUInt, Transaction, TransactionClause } from '@vechain/sdk-core';
import { DEFAULT_DELEGATE_ACCOUNT } from '@/utils/config/accounts';
import { logError } from '@/utils/errorHandler';

// 交易状态枚举
type State = 'Pending' | 'Success' | 'Failed' | 'Cancel';

// 账户接口:包含地址和私钥
export interface Account {
  address: string;
  privateKey: string;
}

// 多账户状态映射:key 为地址,value 为状态
type StateMap = Record<string, State>;

function useSelfSignature() {
  // 维护多个账户的交易状态
  const [states, setStates] = useState<StateMap>({});

  /**
   * 发送自签名交易
   * @param account 发送方账户(包含私钥)
   * @param clauses 交易子句数组
   */
  const sendTransaction = async (account: Account, clauses: TransactionClause[]) => {
    if (!clauses.length) {
      throw new Error('至少需要一个 clause');
    }

    // 设置当前账户状态为等待中
    setStates((prev) => ({ ...prev, [account.address]: 'Pending' }));

    try {
      // 创建 Thor 客户端连接到节点
      const thorClient = ThorClient.at(import.meta.env.VITE_NODE, { isPollingEnabled: false });

      // 1. 预估 Gas 消耗
      const gasResult = await thorClient.gas.estimateGas(clauses, account.address);

      // 2. 构建交易体
      const txBody = await thorClient.transactions.buildTransactionBody(clauses, gasResult.totalGas);

      // 3. 启用代付功能(features: 1 表示启用 VIP-191 代付)
      const transactionBody = { ...txBody, reserved: { features: 1 } };

      // 获取代付账户私钥
      const gasPayerPrivateKey = DEFAULT_DELEGATE_ACCOUNT.privateKey;

      // 4. 双重签名:用户签名 + 代付方签名
      const signedTransaction = Transaction.of(transactionBody).signAsSenderAndGasPayer(
        HexUInt.of(account.privateKey).bytes,  // 发送方私钥
        HexUInt.of(gasPayerPrivateKey).bytes   // 代付方私钥
      );

      // 5. 发送交易到区块链
      const sendTransactionResult = await thorClient.transactions.sendTransaction(signedTransaction);

      // 6. 等待交易被打包确认
      const txReceipt = await thorClient.transactions.waitForTransaction(sendTransactionResult.id);

      // 7. 根据交易结果更新状态
      if (txReceipt?.reverted) {
        setStates((prev) => ({ ...prev, [account.address]: 'Failed' }));
      } else {
        setStates((prev) => ({ ...prev, [account.address]: 'Success' }));
      }
    } catch (e) {
      // 记录错误日志
      logError('sendTransaction', e, {
        module: 'self-signature',
        component: 'useSelfSignature',
        additionalData: { accountAddress: account.address, clausesLength: clauses.length },
      });
      setStates((prev) => ({ ...prev, [account.address]: 'Failed' }));
    }
  };

  /**
   * 手动设置指定地址的状态
   */
  const setWalletState = (address: string, state: State) => {
    setStates((prev) => ({ ...prev, [address]: state }));
  };

  /**
   * 重置所有账户状态
   */
  const resetStates = () => setStates({});

  return { states, sendTransaction, resetStates, setWalletState };
}

export default useSelfSignature;

返回值说明

属性类型说明
statesRecord<string, State>记录每个钱包地址对应的交易状态
sendTransactionFunction执行签名并发送交易的异步函数
setWalletStateFunction手动设置某个地址的状态
resetStatesFunction清空所有状态,通常在关闭模态窗时调用

交易状态流转

初始 → Pending(交易发送中) → Success(成功)/ Failed(失败)

                             Cancel(用户取消)

TransactionSignature 组件

用于展示批量交易的实时状态,包括每个账户的独立状态和整体进度。

tsx
import React from 'react';
import { AlertTriangle, CheckCircle, Loader2, XCircle } from 'lucide-react';
import { BorderLoadingBox } from '@/components/BorderLoadingBox.tsx';

type State = 'Pending' | 'Success' | 'Failed' | 'Cancel';

// 不同状态对应的图标
const STATUS_ICON = {
  Pending: <Loader2 className="animate-spin text-blue-500 w-5 h-5 drop-shadow-sm" />,
  Success: <CheckCircle className="text-green-500 w-5 h-5 drop-shadow-sm" />,
  Failed: <XCircle className="text-red-500 w-5 h-5 drop-shadow-sm" />,
  Cancel: <span className="text-gray-400 w-5 h-5 rounded-full bg-gray-200 flex items-center justify-center">•</span>,
};

// 状态文本
const STATUS_TEXT = {
  Pending: '等待中',
  Success: '成功',
  Failed: '失败',
  Cancel: '取消',
};

/**
 * 计算整体状态
 * - Waiting: 尚未开始
 * - Pending: 有交易正在处理
 * - Success: 全部成功
 * - Failed: 全部失败
 * - Mixed: 部分成功部分失败
 */
function getOverallStatus(states: Record<string, State>) {
  const stateList = Object.values(states);
  if (stateList.length === 0) return 'Waiting';

  const hasPending = stateList.includes('Pending');
  const allSuccess = stateList.length > 0 && stateList.every((s) => s === 'Success');
  const allFailed = stateList.length > 0 && stateList.every((s) => s === 'Failed');

  if (hasPending) return 'Pending';
  if (allSuccess) return 'Success';
  if (allFailed) return 'Failed';
  return 'Mixed';
}

// 整体状态图标(更大尺寸)
const OVERALL_ICON = {
  Waiting: <Loader2 className="w-12 h-12 text-blue-500 animate-spin mx-auto drop-shadow-lg" />,
  Pending: <Loader2 className="w-12 h-12 text-blue-500 animate-spin mx-auto drop-shadow-lg" />,
  Success: (
    <CheckCircle className="w-12 h-12 text-green-500 mx-auto drop-shadow-lg animate-in zoom-in duration-500" />
  ),
  Failed: (
    <XCircle className="w-12 h-12 text-red-500 mx-auto drop-shadow-lg animate-in zoom-in duration-500" />
  ),
  Mixed: (
    <AlertTriangle className="w-12 h-12 text-orange-400 mx-auto drop-shadow-lg animate-in zoom-in duration-500" />
  ),
};

// 整体状态提示文本
const OVERALL_TEXT = {
  Waiting: '正在构建交易子句,请耐心等待...',
  Pending: '交易处理中,请耐心等待...',
  Success: '全部交易成功!',
  Failed: '全部交易失败。',
  Mixed: '部分交易成功,部分失败。',
};

interface Props {
  open: boolean;
  states: Record<string, State>;  // 每个地址对应的状态
  onClose: () => void;
}

function TransactionSignature({ open, states, onClose }: Props) {
  if (!open) return null;

  const walletList = Object.entries(states);
  const overallStatus = getOverallStatus(states);

  return (
    <div className="fixed inset-0 z-[60] bg-black/40 backdrop-blur-sm flex items-center justify-center p-4 animate-in fade-in duration-300">
      <div className="bg-white/95 backdrop-blur-md rounded-3xl shadow-2xl border border-white/20 p-10 w-full max-w-2xl relative text-center animate-in slide-in-from-bottom-4 duration-500">
        {/* 关闭按钮 */}
        <button onClick={onClose} className="absolute top-6 right-6 p-2 rounded-full hover:bg-gray-100/80 transition-all duration-200 group">
          <XCircle className="w-5 h-5 text-gray-400 group-hover:text-gray-600 transition-colors duration-200" />
        </button>

        {/* 整体状态展示 */}
        <div className="font-bold text-2xl center-flex flex-col gap-4 mb-12">
          <div className="relative">{OVERALL_ICON[overallStatus]}</div>
          <div className="bg-gradient-to-r from-gray-800 to-gray-600 bg-clip-text text-transparent">批量交易状态</div>
          <div className="text-sm text-gray-500 font-normal">{OVERALL_TEXT[overallStatus]}</div>
        </div>

        {/* 等待状态或账户列表 */}
        {overallStatus === 'Waiting' ? (
          <div className="center-flex">
            <BorderLoadingBox>{OVERALL_TEXT[overallStatus]}</BorderLoadingBox>
          </div>
        ) : (
          <div className="flex flex-col gap-3 max-h-64 overflow-y-auto">
            {walletList.map(([address, state], index) => (
              <div
                key={address}
                className="flex items-center gap-4 p-4 rounded-2xl bg-gradient-to-r from-gray-50/80 to-gray-100/50 border border-gray-200/50"
                style={{ animationDelay: `${index * 100}ms` }}
              >
                {/* 状态图标 */}
                <div className="flex-shrink-0">{STATUS_ICON[state]}</div>

                {/* 钱包地址 */}
                <div className="flex-1 min-w-0">
                  <div className="font-mono text-sm text-gray-700 truncate">{address}</div>
                </div>

                {/* 状态标签 */}
                <div className="flex-shrink-0">
                  <span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${
                    state === 'Success' ? 'bg-green-100 text-green-800 border border-green-200' :
                    state === 'Failed' ? 'bg-red-100 text-red-800 border border-red-200' :
                    state === 'Pending' ? 'bg-blue-100 text-blue-800 border border-blue-200' :
                    'bg-gray-100 text-gray-800 border border-gray-200'
                  }`}>
                    {STATUS_TEXT[state]}
                  </span>
                </div>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

export default TransactionSignature;

组件特性

  • 整体状态汇总:自动计算所有交易的整体状态
  • 实时更新:交易状态变化时自动刷新 UI
  • 动画效果:每个账户项依次出现,提供流畅的视觉体验
  • 响应式设计:支持滚动显示大量账户

SelfTransactionContext

提供全局上下文管理,统一管理交易状态和模态窗显示,支持在应用的任何地方调用自签名交易。

tsx
import React, { createContext, useContext, useState, ReactNode } from 'react';
import TransactionSignature from '@/components/TransactionSignature';
import useSelfSignature, { Account } from '@/hooks/useSelfSignature';

type State = 'Pending' | 'Success' | 'Failed' | 'Cancel';

interface SelfTransactionContextType {
  states: Record<string, State>;           // 所有账户的状态映射
  isOpen: boolean;                         // 模态窗是否打开
  closeSignature: () => void;              // 关闭模态窗并重置状态
  resetStates: () => void;                 // 仅重置状态,不关闭模态窗
  setWalletState: (address: string, state: State) => void;  // 手动设置某个地址状态
  sendTransaction: (senderAccount: Account, clauses: any[]) => Promise<void>;  // 发送交易
}

const SelfTransactionContext = createContext<SelfTransactionContextType | undefined>(undefined);

/**
 * Provider 组件:将自签名交易功能注入到应用中
 * 应该在应用的顶层使用,如 App.tsx 或 _app.tsx
 */
export function SelfTransactionProvider({ children }: { children: ReactNode }) {
  const [isOpen, setIsOpen] = useState(false);
  const { states, sendTransaction: sendTx, resetStates, setWalletState } = useSelfSignature();

  // 打开模态窗
  const openSignature = () => setIsOpen(true);

  // 关闭模态窗并重置所有状态
  const closeSignature = () => {
    resetStates();
    setIsOpen(false);
  };

  /**
   * 发送交易并自动打开模态窗
   */
  const sendTransaction = async (senderAccount: Account, clauses: any[]) => {
    openSignature();  // 先打开模态窗
    await sendTx(senderAccount, clauses);  // 再发送交易
  };

  return (
    <SelfTransactionContext.Provider
      value={{ states, isOpen, closeSignature, resetStates, setWalletState, sendTransaction }}
    >
      {children}
      {/* 模态窗组件 */}
      <TransactionSignature open={isOpen} onClose={closeSignature} states={states} />
    </SelfTransactionContext.Provider>
  );
}

/**
 * Hook:在组件中使用自签名交易功能
 * 必须在 SelfTransactionProvider 内部使用
 */
export function useSelfTransaction() {
  const context = useContext(SelfTransactionContext);
  if (!context) {
    throw new Error('useSelfTransaction must be used within a SelfTransactionProvider');
  }
  return context;
}

export default SelfTransactionProvider;

使用示例

1. 在应用顶层注入 Provider

tsx
// App.tsx
import { SelfTransactionProvider } from '@/contexts/SelfTransactionContext';

function App() {
  return (
    <SelfTransactionProvider>
      <YourApp />
    </SelfTransactionProvider>
  );
}

2. 在组件中使用

tsx
import { useSelfTransaction } from '@/contexts/SelfTransactionContext';
import { Clause } from '@vechain/sdk-core';

function MyComponent() {
  const { sendTransaction } = useSelfTransaction();

  const handleBatchTransfer = async () => {
    const account = {
      address: '0x...',
      privateKey: '0x...'
    };

    // 构建多个转账子句
    const clauses = [
      Clause.transferVET('0xRecipient1', '1000000000000000000'),  // 1 VET
      Clause.transferVET('0xRecipient2', '2000000000000000000'),  // 2 VET
    ];

    // 发送交易,模态窗会自动打开并显示进度
    await sendTransaction(account, clauses);
  };

  return <button onClick={handleBatchTransfer}>批量转账</button>;
}

3. 批量处理多个账户

tsx
import { useSelfTransaction } from '@/contexts/SelfTransactionContext';

function BatchProcessor() {
  const { sendTransaction, states } = useSelfTransaction();

  const handleBatchProcess = async () => {
    const accounts = [
      { address: '0x...1', privateKey: '0x...' },
      { address: '0x...2', privateKey: '0x...' },
      { address: '0x...3', privateKey: '0x...' },
    ];

    // 并发发送多个交易
    await Promise.allSettled(
      accounts.map(account =>
        sendTransaction(account, [
          /* clauses for this account */
        ])
      )
    );

    // 检查所有交易状态
    console.log('所有交易状态:', states);
  };

  return <button onClick={handleBatchProcess}>批量处理</button>;
}

注意事项

安全性

⚠️ 私钥管理

  • 永远不要在代码中硬编码私钥
  • 不要将私钥提交到版本控制系统
  • 建议使用环境变量或加密存储管理私钥

⚠️ 代付账户

  • 确保代付账户有足够的 VTHO 余额
  • 定期监控代付账户的使用情况
  • 考虑设置代付金额上限

性能优化

  • 批量交易:如需处理大量账户,建议分批次处理,避免同时发起过多请求
  • Gas 预估:Gas 预估可能失败,建议添加重试机制
  • 错误处理:完善的错误日志有助于排查问题

用户体验

  • 交易确认通常需要 10 秒左右,给用户明确的等待提示
  • 失败后提供清晰的错误信息和重试选项
  • 成功后可以提供交易哈希,方便用户在区块浏览器查看

基于 MIT 许可发布