UNPKG

ecash-quicksend

Version:

A unified transaction manager for eCash (XEC), SLP, and ALP token transactions

238 lines 9.85 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getUtxos = getUtxos; exports.selectUtxos = selectUtxos; exports.selectSlpUtxos = selectSlpUtxos; exports.getAddressBalance = getAddressBalance; const ecashaddrjs_1 = require("ecashaddrjs"); const chronik_client_1 = require("../client/chronik-client"); /** * Get UTXOs for a given address * @param address - eCash address * @param chronikClient - Optional chronik client instance (uses default if not provided) * @returns Array of UTXOs */ async function getUtxos(address, chronikClient) { const { type, hash } = (0, ecashaddrjs_1.decodeCashAddress)(address); const client = chronikClient || chronik_client_1.chronik; // 使用传入的chronik客户端或默认的 try { const utxosResponse = await client.script(type, hash).utxos(); if (!utxosResponse || !utxosResponse.utxos) { throw new Error('utxosResponse does not contain utxos property'); } const utxos = utxosResponse.utxos.map(utxo => ({ txid: utxo.outpoint.txid.toLowerCase(), vout: utxo.outpoint.outIdx, value: Number(utxo.sats), address: address, slpToken: utxo.token, })); return utxos; } catch (err) { console.log(`Error fetching UTXOs for hash160: ${hash}`); return []; } } /** * 选择合适的UTXOs用于普通XEC交易 * @param utxos - 所有可用的UTXOs * @param sendAmount - 要发送的金额 * @param strategy - 选择策略 * @returns 包含选择的UTXOs和相关信息 */ function selectUtxos(utxos, sendAmount, strategy = 'all') { // 过滤掉SLP代币UTXOs const nonSlpUtxos = utxos.filter(utxo => !utxo.slpToken); if (nonSlpUtxos.length === 0) { throw new Error('没有可用的非SLP UTXOs'); } let selectedUtxos = []; switch (strategy) { case 'all': // 使用所有非SLP UTXOs,避免产生碎片 selectedUtxos = nonSlpUtxos; break; case 'minimal': // 选择最少数量的UTXOs来满足金额需求 const sortedUtxos = nonSlpUtxos.sort((a, b) => b.value - a.value); // 从大到小排序 let accumulatedValue = 0; for (const utxo of sortedUtxos) { selectedUtxos.push(utxo); accumulatedValue += utxo.value; // 粗略估算手续费(每个输入约150 sats + 基础费用250 sats) const estimatedFee = selectedUtxos.length * 150 + 250; if (accumulatedValue >= sendAmount + estimatedFee) { break; } } break; case 'largest_first': // 优先选择最大的UTXOs selectedUtxos = nonSlpUtxos.sort((a, b) => b.value - a.value); break; default: throw new Error(`未知的UTXO选择策略: ${strategy}`); } const totalInputValue = selectedUtxos.reduce((sum, utxo) => sum + utxo.value, 0); const estimatedFee = selectedUtxos.length * 150 + 250; // 检查余额是否足够 if (totalInputValue < sendAmount + estimatedFee) { throw new Error(`余额不足。总输入: ${totalInputValue}, 需要: ${sendAmount + estimatedFee} (包含预估手续费 ${estimatedFee})`); } return { selectedUtxos, totalInputValue, estimatedFee, changeAmount: totalInputValue - sendAmount - estimatedFee, utxoCount: selectedUtxos.length }; } /** * 为SLP交易选择UTXOs * @param utxos - 所有可用的UTXOs * @param tokenId - SLP代币ID * @param recipients - 接收方数组,包含address和amount * @param tokenDecimals - 代币小数位数 * @param options - 选项 * @returns 包含选择的UTXOs和相关信息 */ function selectSlpUtxos(utxos, tokenId, recipients, tokenDecimals, options = {}) { const { dustLimit = 546, feeStrategy = 'all', // 手续费UTXO选择策略 tokenStrategy = 'largest' // 代币UTXO选择策略 } = options; // 分离SLP和非SLP UTXOs const slpUtxos = utxos.filter(utxo => utxo.slpToken && utxo.slpToken.tokenId === tokenId); const nonSlpUtxos = utxos.filter(utxo => !utxo.slpToken); if (slpUtxos.length === 0) { throw new Error(`没有可用的 ${tokenId} SLP UTXOs`); } if (nonSlpUtxos.length === 0) { throw new Error('没有可用的非SLP UTXOs用于支付手续费'); } // 计算需要发送的代币总量(直接使用基础单位,不进行小数转换) const sendAmounts = recipients.map(r => BigInt(r.amount)); const totalSendTokens = sendAmounts.reduce((acc, val) => acc + val, 0n); // 选择代币UTXOs let selectedTokenUtxos = []; let totalTokens = 0n; if (tokenStrategy === 'all') { // 选择所有相同代币ID的SLP UTXOs,减少UTXO碎片 selectedTokenUtxos = slpUtxos; totalTokens = slpUtxos.reduce((sum, utxo) => sum + BigInt(utxo.slpToken.atoms), 0n); } else if (tokenStrategy === 'largest') { // 选择最大的SLP UTXO const selectedTokenUtxo = slpUtxos.reduce((max, current) => { const currentAmount = BigInt(current.slpToken.atoms); const maxAmount = BigInt(max.slpToken.atoms); return currentAmount > maxAmount ? current : max; }, slpUtxos[0]); selectedTokenUtxos = [selectedTokenUtxo]; totalTokens = BigInt(selectedTokenUtxo.slpToken.atoms); } else if (tokenStrategy === 'minimal') { // 选择刚好够用的最小UTXO const sortedSlpUtxos = slpUtxos .filter(utxo => BigInt(utxo.slpToken.atoms) >= totalSendTokens) .sort((a, b) => { const aAmount = BigInt(a.slpToken.atoms); const bAmount = BigInt(b.slpToken.atoms); return aAmount < bAmount ? -1 : aAmount > bAmount ? 1 : 0; }); if (sortedSlpUtxos.length === 0) { throw new Error('没有足够的代币余额'); } selectedTokenUtxos = [sortedSlpUtxos[0]]; totalTokens = BigInt(sortedSlpUtxos[0].slpToken.atoms); } if (totalSendTokens > totalTokens) { throw new Error(`代币余额不足。可用: ${totalTokens.toString()}, 需要: ${totalSendTokens.toString()}`); } const tokenChange = totalTokens - totalSendTokens; // 选择手续费UTXOs let selectedFeeUtxos; if (feeStrategy === 'all') { selectedFeeUtxos = nonSlpUtxos; } else { // 使用普通UTXO选择逻辑来选择手续费UTXOs const requiredSats = recipients.length * dustLimit + (tokenChange > 0n ? dustLimit : 0) + 500; // 预估需要的sats const feeSelection = selectUtxos(utxos, requiredSats, feeStrategy); selectedFeeUtxos = feeSelection.selectedUtxos; } const totalFeeInputValue = selectedFeeUtxos.reduce((sum, utxo) => sum + utxo.value, 0); const estimatedFee = (selectedFeeUtxos.length + selectedTokenUtxos.length) * 150 + 50; // 更新手续费计算 // 计算所需的最小金额 const requiredAmount = recipients.length * dustLimit + (tokenChange > 0n ? dustLimit : 0) + estimatedFee; if (totalFeeInputValue < requiredAmount) { throw new Error(`手续费余额不足。总输入: ${totalFeeInputValue}, 需要: ${requiredAmount} (包含预估手续费 ${estimatedFee})`); } // 构建最终的发送金额数组 const finalSendAmounts = [...sendAmounts]; if (tokenChange > 0n) { finalSendAmounts.push(tokenChange); } return { selectedTokenUtxos, // 改为数组 selectedFeeUtxos, totalFeeInputValue, estimatedFee, feeChangeAmount: totalFeeInputValue - requiredAmount + estimatedFee, tokenChange, finalSendAmounts, totalSendTokens, totalTokens, dustLimit, summary: { tokenUtxoCount: selectedTokenUtxos.length, // 更新计数 feeUtxoCount: selectedFeeUtxos.length, recipientCount: recipients.length, hasTokenChange: tokenChange > 0n, totalOutputs: 1 + recipients.length + (tokenChange > 0n ? 1 : 0) + 1 // OP_RETURN + recipients + token_change + fee_change } }; } /** * 获取地址余额信息 * @param address - eCash地址 * @param chronikClient - Optional chronik client instance (uses default if not provided) * @returns 余额信息 */ async function getAddressBalance(address, chronikClient) { try { const utxos = await getUtxos(address, chronikClient); const nonSlpUtxos = utxos.filter(utxo => !utxo.slpToken); const slpUtxos = utxos.filter(utxo => utxo.slpToken); const totalBalance = nonSlpUtxos.reduce((sum, utxo) => sum + utxo.value, 0); // 按代币ID分组SLP UTXOs const tokenBalances = {}; slpUtxos.forEach(utxo => { const tokenId = utxo.slpToken.tokenId; if (!tokenBalances[tokenId]) { tokenBalances[tokenId] = { tokenId, totalTokens: 0n, utxoCount: 0, utxos: [] }; } tokenBalances[tokenId].totalTokens += BigInt(utxo.slpToken.atoms); tokenBalances[tokenId].utxoCount++; tokenBalances[tokenId].utxos.push(utxo); }); return { totalBalance, utxoCount: nonSlpUtxos.length, slpUtxoCount: slpUtxos.length, nonSlpUtxos, slpUtxos, tokenBalances: Object.values(tokenBalances) }; } catch (error) { console.error('获取地址余额失败:', error); throw error; } } //# sourceMappingURL=utxo-utils.js.map