fix(transaction): use Legacy (Type 0) transaction format for KAVA

KAVA EVM does not support EIP-1559 dynamic fee transactions.
Changed from EIP-1559 (Type 2) to Legacy (Type 0) format:

- prepareTransaction: Use [nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0]
- finalizeTransaction: Use EIP-155 v calculation (chainId * 2 + 35 + recoveryId)
- Remove type prefix (0x02) as Legacy transactions don't need it
- Update Home.tsx and CoSignSession.tsx to use gasPrice instead of maxFeePerGas

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-31 12:45:38 -08:00
parent d18733deb1
commit 0f8e9cf228
3 changed files with 35 additions and 43 deletions

View File

@ -130,11 +130,10 @@ export default function CoSignSession() {
if (storedTxInfo) {
try {
const parsed = JSON.parse(storedTxInfo);
// 恢复 bigint 类型
// 恢复 bigint 类型 (Legacy 交易使用 gasPrice)
if (parsed.preparedTx) {
parsed.preparedTx.gasLimit = BigInt(parsed.preparedTx.gasLimit);
parsed.preparedTx.maxFeePerGas = BigInt(parsed.preparedTx.maxFeePerGas);
parsed.preparedTx.maxPriorityFeePerGas = BigInt(parsed.preparedTx.maxPriorityFeePerGas);
parsed.preparedTx.gasPrice = BigInt(parsed.preparedTx.gasPrice);
parsed.preparedTx.value = BigInt(parsed.preparedTx.value);
}
setTxInfo(parsed);

View File

@ -293,8 +293,7 @@ export default function Home() {
preparedTx: {
...preparedTx,
gasLimit: preparedTx.gasLimit.toString(),
maxFeePerGas: preparedTx.maxFeePerGas.toString(),
maxPriorityFeePerGas: preparedTx.maxPriorityFeePerGas.toString(),
gasPrice: preparedTx.gasPrice.toString(),
value: preparedTx.value.toString(),
},
to: transferTo,

View File

@ -50,21 +50,24 @@ export interface TransactionParams {
/**
* ()
* 使 Legacy (Type 0) KAVA EIP-1559
*/
export interface PreparedTransaction {
chainId: number;
nonce: number;
maxPriorityFeePerGas: bigint;
maxFeePerGas: bigint;
gasPrice: bigint; // Legacy 使用 gasPrice 而不是 maxFeePerGas
gasLimit: bigint;
to: string;
value: bigint;
data: string;
accessList: unknown[];
// 用于签名的哈希
signHash: string;
// 原始交易数据 (用于签名后广播)
rawTxForSigning: string;
// 为了兼容性保留这些字段
maxPriorityFeePerGas?: bigint;
maxFeePerGas?: bigint;
accessList?: unknown[];
}
/**
@ -392,7 +395,8 @@ export async function estimateGas(params: { from: string; to: string; value: str
}
/**
* EIP-1559
* Legacy (Type 0)
* KAVA EIP-1559使 Legacy
*
*/
export async function prepareTransaction(params: TransactionParams): Promise<PreparedTransaction> {
@ -400,9 +404,8 @@ export async function prepareTransaction(params: TransactionParams): Promise<Pre
// 获取或使用提供的参数
const nonce = params.nonce ?? await getNonce(params.from);
const gasPrice = await getGasPrice();
const maxFeePerGas = params.maxFeePerGas ?? gasPrice.maxFeePerGas;
const maxPriorityFeePerGas = params.maxPriorityFeePerGas ?? gasPrice.maxPriorityFeePerGas;
const gasPriceData = await getGasPrice();
const gasPrice = params.maxFeePerGas ?? gasPriceData.maxFeePerGas;
const gasLimit = params.gasLimit ?? await estimateGas({
from: params.from,
to: params.to,
@ -412,81 +415,72 @@ export async function prepareTransaction(params: TransactionParams): Promise<Pre
const value = kavaToWei(params.value);
const data = params.data || '0x';
const accessList: unknown[] = [];
// EIP-1559 交易字段顺序
// [chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList]
// Legacy 交易字段顺序 (EIP-155)
// [nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0]
// 最后三个字段用于 EIP-155 replay protection
const txFields = [
chainId,
nonce,
maxPriorityFeePerGas,
maxFeePerGas,
gasPrice,
gasLimit,
params.to.toLowerCase(),
value,
data,
accessList,
chainId,
0, // EIP-155: v placeholder
0, // EIP-155: r placeholder
];
// RLP 编码交易字段
const encodedTx = rlpEncode(txFields);
// EIP-1559 交易类型前缀: 0x02
const txWithType = new Uint8Array(1 + encodedTx.length);
txWithType[0] = 0x02;
txWithType.set(encodedTx, 1);
// 计算签名哈希
const signHashBytes = await keccak256(txWithType);
// 计算签名哈希 (不需要类型前缀Legacy 交易直接哈希)
const signHashBytes = await keccak256(encodedTx);
const signHash = bytesToHex(signHashBytes);
return {
chainId,
nonce,
maxPriorityFeePerGas,
maxFeePerGas,
gasPrice,
gasLimit,
to: params.to.toLowerCase(),
value,
data,
accessList,
signHash,
rawTxForSigning: bytesToHex(txWithType),
rawTxForSigning: bytesToHex(encodedTx),
};
}
/**
* 使广
* 使 Legacy (Type 0)
*/
export function finalizeTransaction(
preparedTx: PreparedTransaction,
signature: { r: string; s: string; v: number }
): string {
// EIP-1559 签名后的交易字段
// [chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList, v, r, s]
// EIP-155: v = chainId * 2 + 35 + recovery_id
// recovery_id 是 0 或 1
const v = preparedTx.chainId * 2 + 35 + signature.v;
// Legacy 签名后的交易字段
// [nonce, gasPrice, gasLimit, to, value, data, v, r, s]
const signedTxFields = [
preparedTx.chainId,
preparedTx.nonce,
preparedTx.maxPriorityFeePerGas,
preparedTx.maxFeePerGas,
preparedTx.gasPrice,
preparedTx.gasLimit,
preparedTx.to,
preparedTx.value,
preparedTx.data,
preparedTx.accessList,
signature.v, // recovery id (0 or 1 for EIP-1559)
v,
'0x' + signature.r,
'0x' + signature.s,
];
const encodedSignedTx = rlpEncode(signedTxFields);
// 添加类型前缀
const signedTxWithType = new Uint8Array(1 + encodedSignedTx.length);
signedTxWithType[0] = 0x02;
signedTxWithType.set(encodedSignedTx, 1);
return '0x' + bytesToHex(signedTxWithType);
// Legacy 交易不需要类型前缀
return '0x' + bytesToHex(encodedSignedTx);
}
/**