Avalanche 离线签名
本指南将介绍如何在 Avalanche 上进行离线交易签名,包括手动构建交易、离线签名和在线广播的完整流程。
基本概念
1. 离线签名的优势
- 安全性: 私钥可以在离线环境中使用,避免网络攻击
- 灵活性: 可以在不同设备上完成签名和广播
- 批量处理: 可以预先准备多个交易,批量广播
2. 工作流程
- 在线环境:获取 UTXO 和网络信息
- 离线环境:构建和签名交易
- 在线环境:广播已签名的交易
环境设置
const createHash = require('create-hash');
const { Avalanche, BinTools, Buffer, BN, utils, avm, common } = require('avalanche');
const { StandardAmountOutput } = common;
const { UTXOSet, UTXO } = require('avalanche/dist/apis/avm');
const { Defaults, Serialization } = utils;
require('dotenv').config();
// 网络配置
const networkID = 5;
const avalanche = new Avalanche('api.avax-test.network', 443, 'https', networkID);
const xchain = avalanche.XChain();
const keychain = xchain.keyChain();
// 获取默认配置
const avaxAssetID = Defaults.network[networkID].X['avaxAssetID'];
第一步:准备账户信息
1. 导入私钥和获取地址
async function prepareAccount() {
try {
// 从环境变量获取私钥
const { SECRET_KEY_1 } = process.env;
// 导入私钥到密钥链
const keyPair = keychain.importKey(Buffer.from(SECRET_KEY_1, 'hex'));
// 获取地址信息
const addressBuf = keyPair.getAddress();
const publicKey = keyPair.getPublicKey();
const address = keyPair.getAddressString();
console.log('=== 账户信息 ===');
console.log('地址:', address);
console.log('公钥 (hex):', publicKey.toString('hex'));
console.log('地址 Buffer:', addressBuf.toString('hex'));
return {
keyPair,
address,
publicKey,
addressBuf
};
} catch (error) {
console.error('账户准备失败:', error);
throw error;
}
}
2. 查询账户余额
async function getAccountBalance(address) {
try {
// 获取 AVAX 余额
const balanceResponse = await xchain.getBalance(address, avaxAssetID);
const balance = new BN(balanceResponse.balance);
console.log('=== 余额信息 ===');
console.log('AVAX 余额:', balance.toString());
console.log('余额 (AVAX):', balance.div(new BN(1000000000)).toString());
return balance;
} catch (error) {
console.error('获取余额失败:', error);
throw error;
}
}
// 使用示例
const accountInfo = await prepareAccount();
const balance = await getAccountBalance(accountInfo.address);
第二步:准备 UTXO
1. 获取 UTXO 集合
async function getUTXOs(address) {
try {
// 获取地址的 UTXO
const utxoResponse = await xchain.getUTXOs(address);
const utxos = utxoResponse.utxos;
console.log('=== UTXO 信息 ===');
console.log('UTXO 数量:', utxos.getAllUTXOs().length);
// 显示每个 UTXO 的详细信息
utxos.getAllUTXOs().forEach((utxo, index) => {
const output = utxo.getOutput();
const amount = output.getAmount();
console.log(`UTXO ${index}:`);
console.log(` 金额: ${amount.toString()}`);
console.log(` 交易 ID: ${utxo.getTxID()}`);
console.log(` 输出索引: ${utxo.getOutputIdx()}`);
});
return utxos;
} catch (error) {
console.error('获取 UTXO 失败:', error);
throw error;
}
}
2. 手动创建 UTXO 集合(用于测试)
function createTestUTXOs() {
// 创建空的 UTXO 集合
const utxos = new UTXOSet();
// 添加测试 UTXO(实际使用时应该从网络获取)
const testUTXO = '119ddmfJNCFrXuKmjs6teNgMeoFuNkzN2ZGBfYWhHUHnmbat8ZxzehjPjNV4P931cxj3iXtNhU4bmMECBaVT32waJVMVCkBMQbkyBuDBQaQZyceXNwWdovNfVTznZJWGQeDXqSuVhHnwxU6cxBn4mXLi4ST1Jvb5bn';
utxos.addArray([testUTXO], false);
console.log('=== 测试 UTXO 创建 ===');
console.log('UTXO 数量:', utxos.getAllUTXOs().length);
return utxos;
}
// 选择使用真实 UTXO 还是测试 UTXO
const utxos = await getUTXOs(accountInfo.address);
// const utxos = createTestUTXOs(); // 测试环境使用
第三步:计算交易参数
1. 计算交易费用
function calculateTransactionFee() {
// 获取默认交易费用
const fee = xchain.getDefaultTxFee();
console.log('=== 交易费用 ===');
console.log('默认费用:', fee.toString());
console.log('费用 (AVAX):', fee.div(new BN(1000000000)).toString());
return fee;
}
2. 计算转移金额
function calculateTransferAmount(balance, fee) {
// 计算实际转移金额(减去费用)
const amount = balance.sub(fee);
console.log('=== 转移金额计算 ===');
console.log('总余额:', balance.toString());
console.log('交易费用:', fee.toString());
console.log('转移金额:', amount.toString());
console.log('转移金额 (AVAX):', amount.div(new BN(1000000000)).toString());
return amount;
}
// 计算交易参数
const fee = calculateTransactionFee();
const transferAmount = calculateTransferAmount(balance, fee);
第四步:构建交易
1. 准备交易参数
function prepareTransactionParams(fromAddress, toAddress, changeAddress) {
console.log('=== 交易参数 ===');
console.log('发送地址:', fromAddress);
console.log('接收地址:', toAddress);
console.log('找零地址:', changeAddress);
return {
toAddress: [toAddress],
fromAddress: [fromAddress],
changeAddresses: [changeAddress]
};
}
// 准备交易参数
const toAddress = 'X-fuji1rz6uxnat4e9l6ygdu9enx3a79xnjzg2z4763w2';
const changeAddress = 'X-fuji1wngxrrwn665t8xmq93z4dk0y8nne9u44adyhac';
const txParams = prepareTransactionParams(
accountInfo.address,
toAddress,
changeAddress
);
2. 构建未签名交易
async function buildUnsignedTransaction(utxos, amount, assetID, txParams) {
try {
console.log('=== 构建交易 ===');
// 构建基础交易
const unsignedTx = await xchain.buildBaseTx(
utxos, // UTXO 集合
amount, // 转移金额
assetID, // 资产 ID
txParams.toAddress, // 接收地址
txParams.fromAddress, // 发送地址
txParams.changeAddresses // 找零地址
);
console.log('交易构建成功');
// 序列化交易用于查看
const displayData = unsignedTx.serialize('display');
console.log('交易详情:', JSON.stringify(displayData, null, 2));
return unsignedTx;
} catch (error) {
console.error('构建交易失败:', error);
throw error;
}
}
// 构建未签名交易
const unsignedTx = await buildUnsignedTransaction(
utxos,
transferAmount,
avaxAssetID,
txParams
);
第五步:离线签名
1. 签名交易
function signTransaction(unsignedTx, keychain) {
try {
console.log('=== 交易签名 ===');
// 使用密钥链签名交易
const signedTx = unsignedTx.sign(keychain);
console.log('交易签名成功');
// 获取签名后的交易数据
const hexData = signedTx.toBuffer().toString('hex');
console.log('签名后交易 (hex):', hexData);
return signedTx;
} catch (error) {
console.error('交易签名失败:', error);
throw error;
}
}
// 签名交易
const signedTx = signTransaction(unsignedTx, keychain);
2. 生成交易 ID
function generateTransactionID(signedTx) {
try {
console.log('=== 生成交易 ID ===');
// 计算交易哈希
const txBuffer = signedTx.toBuffer();
const txHash = createHash('sha256').update(txBuffer).digest();
// 转换为 CB58 格式
const serialization = Serialization.getInstance();
const txid = serialization.bufferToType(txHash, 'cb58');
console.log('交易哈希 (hex):', txHash.toString('hex'));
console.log('交易 ID (CB58):', txid);
return {
hex: txHash.toString('hex'),
cb58: txid,
buffer: txHash
};
} catch (error) {
console.error('生成交易 ID 失败:', error);
throw error;
}
}
// 生成交易 ID
const txID = generateTransactionID(signedTx);
第六步:广播交易
1. 广播已签名的交易
async function broadcastTransaction(signedTx) {
try {
console.log('=== 广播交易 ===');
// 将已签名的交易发送到网络
const txid = await xchain.issueTx(signedTx);
console.log('交易广播成功');
console.log('网络返回的 TXID:', txid);
return txid;
} catch (error) {
console.error('交易广播失败:', error);
throw error;
}
}
// 广播交易
const broadcastedTxID = await broadcastTransaction(signedTx);
2. 验证交易状态
async function verifyTransactionStatus(txid) {
try {
console.log('=== 验证交易状态 ===');
// 等待一段时间让交易被处理
await new Promise(resolve => setTimeout(resolve, 2000));
// 查询交易状态
const tx = await xchain.getTx(txid);
console.log('交易状态:', tx.status);
console.log('交易详情:', JSON.stringify(tx, null, 2));
return tx;
} catch (error) {
console.error('验证交易状态失败:', error);
throw error;
}
}
// 验证交易状态
const txStatus = await verifyTransactionStatus(broadcastedTxID);
完整离线签名流程
async function completeOfflineSigning() {
try {
console.log('=== 开始离线签名流程 ===');
// 第一步:准备账户
const accountInfo = await prepareAccount();
// 第二步:获取余额
const balance = await getAccountBalance(accountInfo.address);
// 第三步:获取 UTXO
const utxos = await getUTXOs(accountInfo.address);
// 第四步:计算参数
const fee = calculateTransactionFee();
const transferAmount = calculateTransferAmount(balance, fee);
// 第五步:准备交易参数
const txParams = prepareTransactionParams(
accountInfo.address,
'X-fuji1rz6uxnat4e9l6ygdu9enx3a79xnjzg2z4763w2',
'X-fuji1wngxrrwn665t8xmq93z4dk0y8nne9u44adyhac'
);
// 第六步:构建交易
const unsignedTx = await buildUnsignedTransaction(
utxos,
transferAmount,
avaxAssetID,
txParams
);
// 第七步:离线签名(可以在另一台设备上完成)
const signedTx = signTransaction(unsignedTx, keychain);
// 第八步:生成交易 ID
const txID = generateTransactionID(signedTx);
// 第九步:广播交易
const broadcastedTxID = await broadcastTransaction(signedTx);
// 第十步:验证状态
const txStatus = await verifyTransactionStatus(broadcastedTxID);
console.log('=== 离线签名流程完成 ===');
console.log('最终交易 ID:', broadcastedTxID);
console.log('交易状态:', txStatus.status);
return {
txid: broadcastedTxID,
status: txStatus.status,
signedTx: signedTx,
txID: txID
};
} catch (error) {
console.error('离线签名流程失败:', error);
throw error;
}
}
// 运行完整流程
completeOfflineSigning()
.then(result => {
console.log('流程执行成功:', result);
})
.catch(error => {
console.error('流程执行失败:', error);
});
高级功能
1. 批量离线签名
class BatchOfflineSigner {
constructor(keychain) {
this.keychain = keychain;
this.transactions = [];
}
// 添加交易到批次
addTransaction(unsignedTx) {
this.transactions.push(unsignedTx);
console.log(`添加交易到批次,当前数量: ${this.transactions.length}`);
}
// 批量签名
signAllTransactions() {
const signedTxs = [];
this.transactions.forEach((unsignedTx, index) => {
try {
const signedTx = unsignedTx.sign(this.keychain);
signedTxs.push(signedTx);
console.log(`交易 ${index} 签名成功`);
} catch (error) {
console.error(`交易 ${index} 签名失败:`, error.message);
}
});
return signedTxs;
}
// 批量广播
async broadcastAllTransactions(signedTxs) {
const results = [];
for (let i = 0; i < signedTxs.length; i++) {
try {
const txid = await xchain.issueTx(signedTxs[i]);
results.push({ index: i, success: true, txid });
console.log(`交易 ${i} 广播成功: ${txid}`);
} catch (error) {
results.push({ index: i, success: false, error: error.message });
console.error(`交易 ${i} 广播失败:`, error.message);
}
// 添加延迟避免网络拥堵
await new Promise(resolve => setTimeout(resolve, 1000));
}
return results;
}
}
// 使用批量签名器
const batchSigner = new BatchOfflineSigner(keychain);
// 添加多个交易
batchSigner.addTransaction(unsignedTx1);
batchSigner.addTransaction(unsignedTx2);
batchSigner.addTransaction(unsignedTx3);
// 批量签名
const signedTxs = batchSigner.signAllTransactions();
// 批量广播
const broadcastResults = await batchSigner.broadcastAllTransactions(signedTxs);
2. 交易模板
class TransactionTemplate {
constructor(fromAddress, toAddress, changeAddress) {
this.fromAddress = fromAddress;
this.toAddress = toAddress;
this.changeAddress = changeAddress;
}
// 创建转移模板
createTransferTemplate(amount, assetID) {
return {
type: 'transfer',
fromAddress: this.fromAddress,
toAddress: this.toAddress,
changeAddress: this.changeAddress,
amount: amount,
assetID: assetID
};
}
// 应用模板构建交易
async buildFromTemplate(template, utxos) {
if (template.type === 'transfer') {
return await xchain.buildBaseTx(
utxos,
template.amount,
template.assetID,
[template.toAddress],
[template.fromAddress],
[template.changeAddress]
);
}
throw new Error('不支持的交易类型');
}
}
// 使用交易模板
const template = new TransactionTemplate(
accountInfo.address,
toAddress,
changeAddress
);
const transferTemplate = template.createTransferTemplate(
transferAmount,
avaxAssetID
);
const unsignedTxFromTemplate = await template.buildFromTemplate(
transferTemplate,
utxos
);
错误处理和调试
1. 常见错误类型
function handleOfflineSigningError(error) {
if (error.message.includes('insufficient funds')) {
console.log('余额不足,请检查账户余额');
} else if (error.message.includes('Invalid UTXO')) {
console.log('UTXO 无效,请重新获取');
} else if (error.message.includes('Invalid signature')) {
console.log('签名无效,请检查私钥');
} else if (error.message.includes('Network Error')) {
console.log('网络错误,请检查网络连接');
} else {
console.log('未知错误:', error.message);
}
}
2. 调试模式
function enableDebugMode(transaction) {
console.log('=== 调试模式 ===');
// 输出交易结构
console.log('交易类型:', transaction.constructor.name);
console.log('交易方法:', Object.getOwnPropertyNames(Object.getPrototypeOf(transaction)));
// 输出序列化数据
const displayData = transaction.serialize('display');
console.log('交易详情:', JSON.stringify(displayData, null, 2));
// 输出 Buffer 数据
const buffer = transaction.toBuffer();
console.log('交易 Buffer 长度:', buffer.length);
console.log('交易 Hex:', buffer.toString('hex'));
}