beeline-cli
Version:
A terminal wallet for the Hive blockchain - type, sign, rule the chain
657 lines • 29.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.HiveClient = void 0;
exports.formatTransactionAmount = formatTransactionAmount;
exports.getTransactionDescription = getTransactionDescription;
const dhive_1 = require("@hiveio/dhive");
class HiveClient {
constructor(keyManager, nodeUrl) {
// Default to public Hive API nodes
const nodes = nodeUrl ? [nodeUrl] : [
'https://api.hive.blog',
'https://hived.emre.sh',
'https://rpc.ausbit.dev',
'https://api.openhive.network'
];
this.client = new dhive_1.Client(nodes, {
timeout: 10000,
failoverThreshold: 3,
consoleOnFailover: false
});
this.keyManager = keyManager;
}
async getAccount(username) {
try {
const accounts = await this.client.database.getAccounts([username]);
if (accounts.length > 0) {
const account = accounts[0];
return {
name: account.name,
balance: typeof account.balance === 'string' ? account.balance : account.balance.toString(),
hbd_balance: typeof account.hbd_balance === 'string' ? account.hbd_balance : account.hbd_balance.toString(),
vesting_shares: typeof account.vesting_shares === 'string' ? account.vesting_shares : account.vesting_shares.toString(),
savings_balance: typeof account.savings_balance === 'string' ? account.savings_balance : account.savings_balance.toString(),
savings_hbd_balance: typeof account.savings_hbd_balance === 'string' ? account.savings_hbd_balance : account.savings_hbd_balance.toString(),
delegated_vesting_shares: typeof account.delegated_vesting_shares === 'string' ? account.delegated_vesting_shares : account.delegated_vesting_shares.toString(),
received_vesting_shares: typeof account.received_vesting_shares === 'string' ? account.received_vesting_shares : account.received_vesting_shares.toString(),
reward_hive_balance: typeof account.reward_hive_balance === 'string' ? account.reward_hive_balance : (account.reward_hive_balance ? account.reward_hive_balance.toString() : '0.000 HIVE'),
reward_hbd_balance: typeof account.reward_hbd_balance === 'string' ? account.reward_hbd_balance : (account.reward_hbd_balance ? account.reward_hbd_balance.toString() : '0.000 HBD'),
reward_vesting_balance: typeof account.reward_vesting_balance === 'string' ? account.reward_vesting_balance : (account.reward_vesting_balance ? account.reward_vesting_balance.toString() : '0.000 VESTS'),
// Powerdown/withdrawal fields
vesting_withdraw_rate: typeof account.vesting_withdraw_rate === 'string' ? account.vesting_withdraw_rate : (account.vesting_withdraw_rate ? account.vesting_withdraw_rate.toString() : '0.000000 VESTS'),
next_vesting_withdrawal: account.next_vesting_withdrawal ? account.next_vesting_withdrawal.toString() : '1969-12-31T23:59:59',
withdrawn: account.withdrawn ? parseInt(account.withdrawn.toString()) : 0,
to_withdraw: account.to_withdraw ? parseInt(account.to_withdraw.toString()) : 0
};
}
return null;
}
catch (error) {
throw new Error(`Failed to fetch account: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async getBalance(username) {
const account = await this.getAccount(username);
if (!account) {
throw new Error(`Account ${username} not found`);
}
// Get dynamic global properties for HP calculation
const globalProps = await this.client.database.getDynamicGlobalProperties();
const vestingShares = parseFloat(account.vesting_shares.toString().split(' ')[0]);
const delegatedVesting = parseFloat(account.delegated_vesting_shares.toString().split(' ')[0]);
const receivedVesting = parseFloat(account.received_vesting_shares.toString().split(' ')[0]);
// Calculate effective HP (vesting shares + received - delegated)
const effectiveVesting = vestingShares + receivedVesting - delegatedVesting;
// Convert vesting shares to HIVE Power
const totalVests = parseFloat(globalProps.total_vesting_shares.toString().split(' ')[0]);
const totalHive = parseFloat(globalProps.total_vesting_fund_hive.toString().split(' ')[0]);
const hivepower = (effectiveVesting * totalHive) / totalVests;
return {
hive: account.balance.toString().split(' ')[0],
hbd: account.hbd_balance.toString().split(' ')[0],
hp: hivepower.toFixed(3),
savings_hive: account.savings_balance.toString().split(' ')[0],
savings_hbd: account.savings_hbd_balance.toString().split(' ')[0]
};
}
async transfer(from, to, amount, currency, memo = '', pin) {
try {
// Get the active key for signing
const privateKeyWif = await this.keyManager.getPrivateKey(from, 'active', pin);
if (!privateKeyWif) {
throw new Error(`Active key not found for account ${from}`);
}
const privateKey = dhive_1.PrivateKey.fromString(privateKeyWif);
// Create transfer operation
const operation = [
'transfer',
{
from,
to,
amount: `${amount} ${currency}`,
memo
}
];
// Broadcast transaction
const result = await this.client.broadcast.sendOperations([operation], privateKey);
// Memory scrubbing
this.keyManager.scrubMemory(privateKeyWif);
return result.id;
}
catch (error) {
throw new Error(`Transfer failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async powerUp(from, to, amount, pin) {
try {
const privateKeyWif = await this.keyManager.getPrivateKey(from, 'active', pin);
if (!privateKeyWif) {
throw new Error(`Active key not found for account ${from}`);
}
const privateKey = dhive_1.PrivateKey.fromString(privateKeyWif);
const operation = [
'transfer_to_vesting',
{
from,
to,
amount: `${amount} HIVE`
}
];
const result = await this.client.broadcast.sendOperations([operation], privateKey);
this.keyManager.scrubMemory(privateKeyWif);
return result.id;
}
catch (error) {
throw new Error(`Power up failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async powerDown(account, amount, pin) {
try {
const privateKeyWif = await this.keyManager.getPrivateKey(account, 'active', pin);
if (!privateKeyWif) {
throw new Error(`Active key not found for account ${account}`);
}
const privateKey = dhive_1.PrivateKey.fromString(privateKeyWif);
const operation = [
'withdraw_vesting',
{
account,
vesting_shares: `${amount} VESTS`
}
];
const result = await this.client.broadcast.sendOperations([operation], privateKey);
this.keyManager.scrubMemory(privateKeyWif);
return result.id;
}
catch (error) {
throw new Error(`Power down failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async getNodeInfo() {
try {
const config = await this.client.database.getConfig();
const globalProps = await this.client.database.getDynamicGlobalProperties();
return {
url: Array.isArray(this.client.address) ? this.client.address[0] : this.client.address,
version: typeof config.HIVE_BLOCKCHAIN_VERSION === 'string' ? config.HIVE_BLOCKCHAIN_VERSION : String(config.HIVE_BLOCKCHAIN_VERSION),
lastBlockNum: globalProps.head_block_number
};
}
catch (error) {
throw new Error(`Failed to get node info: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async convertHPToVests(hp) {
try {
const globalProps = await this.client.database.getDynamicGlobalProperties();
const totalVests = parseFloat(globalProps.total_vesting_shares.toString().split(' ')[0]);
const totalHive = parseFloat(globalProps.total_vesting_fund_hive.toString().split(' ')[0]);
return (hp * totalVests) / totalHive;
}
catch (error) {
throw new Error(`Failed to convert HP to VESTS: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async transferToSavings(from, to, amount, currency, memo = '', pin) {
try {
const privateKeyWif = await this.keyManager.getPrivateKey(from, 'active', pin);
if (!privateKeyWif) {
throw new Error(`Active key not found for account ${from}`);
}
const privateKey = dhive_1.PrivateKey.fromString(privateKeyWif);
const operation = [
'transfer_to_savings',
{
from,
to,
amount: `${amount} ${currency}`,
memo
}
];
const result = await this.client.broadcast.sendOperations([operation], privateKey);
this.keyManager.scrubMemory(privateKeyWif);
return result.id;
}
catch (error) {
throw new Error(`Transfer to savings failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async transferFromSavings(from, requestId, to, amount, currency, memo = '', pin) {
try {
const privateKeyWif = await this.keyManager.getPrivateKey(from, 'active', pin);
if (!privateKeyWif) {
throw new Error(`Active key not found for account ${from}`);
}
const privateKey = dhive_1.PrivateKey.fromString(privateKeyWif);
const operation = [
'transfer_from_savings',
{
from,
request_id: requestId,
to,
amount: `${amount} ${currency}`,
memo
}
];
const result = await this.client.broadcast.sendOperations([operation], privateKey);
this.keyManager.scrubMemory(privateKeyWif);
return result.id;
}
catch (error) {
throw new Error(`Transfer from savings failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async claimRewards(account, rewardHive, rewardHbd, rewardVests, pin) {
try {
const privateKeyWif = await this.keyManager.getPrivateKey(account, 'posting', pin);
if (!privateKeyWif) {
throw new Error(`Posting key not found for account ${account}`);
}
const privateKey = dhive_1.PrivateKey.fromString(privateKeyWif);
const operation = [
'claim_reward_balance',
{
account,
reward_hive: `${rewardHive} HIVE`,
reward_hbd: `${rewardHbd} HBD`,
reward_vests: `${rewardVests} VESTS`
}
];
const result = await this.client.broadcast.sendOperations([operation], privateKey);
this.keyManager.scrubMemory(privateKeyWif);
return result.id;
}
catch (error) {
throw new Error(`Claim rewards failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async getResourceCredits(username) {
try {
const rc = await this.client.rc.findRCAccounts([username]);
if (rc.length === 0) {
throw new Error(`RC data not found for ${username}`);
}
const current = parseInt(rc[0].rc_manabar.current_mana);
const max = parseInt(rc[0].max_rc);
const percentage = (current / max) * 100;
return { current, max, percentage };
}
catch (error) {
throw new Error(`Failed to get RC data: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async broadcastCustomJson(account, id, json, requiredAuths = [], requiredPostingAuths = [], pin) {
try {
// Get the appropriate key based on which auth is required
const keyType = requiredAuths.length > 0 ? 'active' : 'posting';
const privateKeyWif = await this.keyManager.getPrivateKey(account, keyType, pin);
if (!privateKeyWif) {
throw new Error(`No ${keyType} key found for @${account}. Please import your ${keyType} key first.`);
}
const privateKey = dhive_1.PrivateKey.fromString(privateKeyWif);
const operation = [
'custom_json',
{
required_auths: requiredAuths,
required_posting_auths: requiredPostingAuths,
id: id,
json: JSON.stringify(json)
}
];
const result = await this.client.broadcast.sendOperations([operation], privateKey);
return result;
}
catch (error) {
throw new Error(`Failed to broadcast custom JSON: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// Transaction History Methods
async getAccountHistory(username, limit = 100, start = -1, filter) {
try {
let operationFilterLow = 0;
let operationFilterHigh = 0;
// Apply operation type filtering - but only if we have specific types
// For now, let's get all transactions and filter client-side
if (filter?.types && filter.types.length > 0) {
const operationBits = this.getOperationBits(filter.types);
operationFilterLow = operationBits.low;
operationFilterHigh = operationBits.high;
}
// Use the simpler 3-parameter version first to debug
const history = await this.client.database.call('get_account_history', [
username,
start,
limit
]);
if (!history || !Array.isArray(history)) {
throw new Error('Invalid response from API');
}
const transactions = history.map((entry) => ({
trx_id: entry[1]?.trx_id || '',
block: entry[1]?.block || 0,
trx_in_block: entry[1]?.trx_in_block || 0,
op_in_trx: entry[1]?.op_in_trx || 0,
virtual_op: entry[1]?.virtual_op === true ? 1 : 0, // Handle boolean virtual_op
timestamp: entry[1]?.timestamp || '',
op: entry[1]?.op || ['unknown', {}]
}));
// Apply additional filters
return this.filterTransactions(transactions, username, filter);
}
catch (error) {
throw new Error(`Failed to fetch account history: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
getOperationBits(operationTypes) {
const operationMap = {
'vote': 0,
'comment': 1,
'transfer': 2,
'transfer_to_vesting': 3,
'withdraw_vesting': 4,
'limit_order_create': 5,
'limit_order_cancel': 6,
'feed_publish': 7,
'convert': 8,
'account_create': 9,
'account_update': 10,
'witness_update': 11,
'account_witness_vote': 12,
'account_witness_proxy': 13,
'pow': 14,
'custom': 15,
'report_over_production': 16,
'delete_comment': 17,
'custom_json': 18,
'comment_options': 19,
'set_withdraw_vesting_route': 20,
'limit_order_create2': 21,
'claim_account': 22,
'create_claimed_account': 23,
'request_account_recovery': 24,
'recover_account': 25,
'change_recovery_account': 26,
'escrow_transfer': 27,
'escrow_dispute': 28,
'escrow_release': 29,
'pow2': 30,
'escrow_approve': 31,
'transfer_to_savings': 32,
'transfer_from_savings': 33,
'cancel_transfer_from_savings': 34,
'custom_binary': 35,
'decline_voting_rights': 36,
'reset_account': 37,
'set_reset_account': 38,
'claim_reward_balance': 39,
'delegate_vesting_shares': 40,
'account_create_with_delegation': 41,
'witness_set_properties': 42,
'account_update2': 43,
'create_proposal': 44,
'update_proposal_votes': 45,
'remove_proposal': 46,
'update_proposal': 47,
'collateralized_convert': 48,
'recurrent_transfer': 49,
// Virtual operations start at higher numbers
'fill_convert_request': 50,
'author_reward': 51,
'curation_reward': 52,
'comment_reward': 53,
'liquidity_reward': 54,
'interest': 55,
'fill_vesting_withdraw': 56,
'fill_order': 57,
'shutdown_witness': 58,
'fill_transfer_from_savings': 59,
'hardfork': 60,
'comment_payout_update': 61,
'return_vesting_delegation': 62,
'comment_benefactor_reward': 63,
'producer_reward': 64
};
let low = 0;
let high = 0;
for (const opType of operationTypes) {
const bitPosition = operationMap[opType];
if (bitPosition !== undefined) {
if (bitPosition < 64) {
low |= (1 << bitPosition);
}
else {
high |= (1 << (bitPosition - 64));
}
}
}
return { low, high };
}
filterTransactions(transactions, account, filter) {
if (!filter)
return transactions;
return transactions.filter(tx => {
const opType = tx.op[0];
const opData = tx.op[1];
const txDate = new Date(tx.timestamp);
// Type filter - most important for default behavior
if (filter.types && filter.types.length > 0) {
if (!filter.types.includes(opType)) {
return false;
}
}
// Date range filter
if (filter.startDate && txDate < filter.startDate)
return false;
if (filter.endDate && txDate > filter.endDate)
return false;
// Direction filter for transfer operations
if (filter.direction && ['transfer', 'transfer_to_vesting', 'transfer_to_savings', 'transfer_from_savings'].includes(opType)) {
const isOutgoing = opData.from === account;
const isIncoming = opData.to === account;
if (filter.direction === 'incoming' && !isIncoming)
return false;
if (filter.direction === 'outgoing' && !isOutgoing)
return false;
}
// Amount filter
if ((filter.minAmount || filter.maxAmount) && opData.amount) {
const amount = parseFloat(opData.amount.split(' ')[0]);
if (filter.minAmount && amount < filter.minAmount)
return false;
if (filter.maxAmount && amount > filter.maxAmount)
return false;
}
// Currency filter
if (filter.currency && opData.amount) {
const currency = opData.amount.split(' ')[1];
if (currency !== filter.currency)
return false;
}
return true;
});
}
async getTransactionAnalytics(username, filter) {
const transactions = await this.getAccountHistory(username, 1000, -1, filter);
const analytics = {
totalTransactions: transactions.length,
totalVolume: { hive: 0, hbd: 0, vests: 0 },
totalFees: 0,
averageAmount: { hive: 0, hbd: 0 },
transactionsByType: {},
transactionsByMonth: {},
topRecipients: [],
topSenders: [],
rewardsSummary: { author: 0, curator: 0, vesting: 0 }
};
const recipients = {};
const senders = {};
let hiveTotal = 0, hbdTotal = 0, hiveCount = 0, hbdCount = 0;
for (const tx of transactions) {
const opType = tx.op[0];
const opData = tx.op[1];
const txDate = new Date(tx.timestamp);
const monthKey = `${txDate.getFullYear()}-${String(txDate.getMonth() + 1).padStart(2, '0')}`;
// Count by type
analytics.transactionsByType[opType] = (analytics.transactionsByType[opType] || 0) + 1;
// Count by month
analytics.transactionsByMonth[monthKey] = (analytics.transactionsByMonth[monthKey] || 0) + 1;
// Process transfer operations
if (['transfer', 'transfer_to_vesting', 'transfer_to_savings', 'transfer_from_savings'].includes(opType) && opData.amount) {
const amount = parseFloat(opData.amount.split(' ')[0]);
const currency = opData.amount.split(' ')[1];
if (currency === 'HIVE') {
analytics.totalVolume.hive += amount;
hiveTotal += amount;
hiveCount++;
}
else if (currency === 'HBD') {
analytics.totalVolume.hbd += amount;
hbdTotal += amount;
hbdCount++;
}
else if (currency === 'VESTS') {
analytics.totalVolume.vests += amount;
}
// Track recipients and senders
if (opData.to && opData.to !== username) {
recipients[opData.to] = recipients[opData.to] || { count: 0, total: 0 };
recipients[opData.to].count++;
recipients[opData.to].total += amount;
}
if (opData.from && opData.from !== username) {
senders[opData.from] = senders[opData.from] || { count: 0, total: 0 };
senders[opData.from].count++;
senders[opData.from].total += amount;
}
}
// Process reward operations
if (['author_reward', 'curation_reward', 'comment_reward'].includes(opType)) {
if (opType === 'author_reward' && opData.hive_payout) {
analytics.rewardsSummary.author += parseFloat(opData.hive_payout.split(' ')[0]);
}
if (opType === 'curation_reward' && opData.reward) {
analytics.rewardsSummary.curator += parseFloat(opData.reward.split(' ')[0]);
}
if (opData.vesting_payout) {
analytics.rewardsSummary.vesting += parseFloat(opData.vesting_payout.split(' ')[0]);
}
}
}
// Calculate averages
analytics.averageAmount.hive = hiveCount > 0 ? hiveTotal / hiveCount : 0;
analytics.averageAmount.hbd = hbdCount > 0 ? hbdTotal / hbdCount : 0;
// Sort and limit top recipients/senders
analytics.topRecipients = Object.entries(recipients)
.map(([account, data]) => ({ account, ...data }))
.sort((a, b) => b.total - a.total)
.slice(0, 10);
analytics.topSenders = Object.entries(senders)
.map(([account, data]) => ({ account, ...data }))
.sort((a, b) => b.total - a.total)
.slice(0, 10);
return analytics;
}
// Governance operations
async witnessVote(voter, witness, approve, pin) {
try {
const privateKeyWif = await this.keyManager.getPrivateKey(voter, 'active', pin);
if (!privateKeyWif) {
throw new Error(`Active key not found for account ${voter}`);
}
const privateKey = dhive_1.PrivateKey.fromString(privateKeyWif);
const operation = [
'account_witness_vote',
{
account: voter,
witness,
approve
}
];
const result = await this.client.broadcast.sendOperations([operation], privateKey);
this.keyManager.scrubMemory(privateKeyWif);
return result.id;
}
catch (error) {
throw new Error(`Witness vote failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async witnessProxy(account, proxy, pin) {
try {
const privateKeyWif = await this.keyManager.getPrivateKey(account, 'active', pin);
if (!privateKeyWif) {
throw new Error(`Active key not found for account ${account}`);
}
const privateKey = dhive_1.PrivateKey.fromString(privateKeyWif);
const operation = [
'account_witness_proxy',
{
account,
proxy
}
];
const result = await this.client.broadcast.sendOperations([operation], privateKey);
this.keyManager.scrubMemory(privateKeyWif);
return result.id;
}
catch (error) {
throw new Error(`Witness proxy failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async getWitnesses(limit = 30, activeOnly = false) {
try {
const witnesses = await this.client.database.call('get_witnesses_by_vote', ['', limit]);
if (activeOnly) {
return witnesses.filter(w => w.signing_key !== 'STM1111111111111111111111111111111114T1Anm');
}
return witnesses;
}
catch (error) {
throw new Error(`Failed to fetch witnesses: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async getGovernanceStatus(account) {
try {
const accountData = await this.client.database.getAccounts([account]);
if (!accountData || accountData.length === 0) {
throw new Error(`Account ${account} not found`);
}
const accountInfo = accountData[0];
return {
proxy: accountInfo.proxy || null,
witnessVotes: accountInfo.witness_votes || [],
votingPower: (accountInfo.vesting_shares && typeof accountInfo.vesting_shares === 'string')
? accountInfo.vesting_shares
: accountInfo.vesting_shares?.toString() || '0.000000 VESTS'
};
}
catch (error) {
throw new Error(`Failed to get governance status: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
}
exports.HiveClient = HiveClient;
// Export utility functions for transaction formatting
function formatTransactionAmount(amount) {
const parts = amount.split(' ');
const value = parseFloat(parts[0]);
const currency = parts[1];
const formatted = value.toLocaleString('en-US', {
minimumFractionDigits: 3,
maximumFractionDigits: 3
});
return { value, currency, formatted };
}
function getTransactionDescription(tx, currentUser) {
const opType = tx.op[0];
const opData = tx.op[1];
switch (opType) {
case 'transfer':
const isOutgoing = opData.from === currentUser;
const otherParty = isOutgoing ? opData.to : opData.from;
const direction = isOutgoing ? 'to' : 'from';
return `Transfer ${direction} @${otherParty}`;
case 'transfer_to_vesting':
return opData.from === opData.to
? 'Power Up'
: `Power Up to @${opData.to}`;
case 'withdraw_vesting':
return 'Power Down';
case 'transfer_to_savings':
return 'Deposit to Savings';
case 'transfer_from_savings':
return 'Withdraw from Savings';
case 'claim_reward_balance':
return 'Claim Rewards';
case 'author_reward':
return 'Author Reward';
case 'curation_reward':
return 'Curation Reward';
case 'interest':
return 'Savings Interest';
case 'fill_vesting_withdraw':
return 'Power Down Payment';
default:
return opType.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
}
}
//# sourceMappingURL=hive.js.map