Merge pull request #1 from void-57/main
feat: Complete Hedera Wallet Implementation with Multi-Chain Support
This commit is contained in:
commit
de07fb83fe
453
hederaBlockchainAPI.js
Normal file
453
hederaBlockchainAPI.js
Normal file
@ -0,0 +1,453 @@
|
||||
(function (EXPORTS) {
|
||||
"use strict";
|
||||
const hederaAPI = EXPORTS;
|
||||
|
||||
// API Configuration - Mainnet Only
|
||||
const NETWORK_CONFIG = {
|
||||
mirrorNode: 'https://mainnet-public.mirrornode.hedera.com',
|
||||
jsonRpcRelay: 'https://mainnet.hashio.io/api',
|
||||
chainId: 295, // Hedera Mainnet
|
||||
explorer: 'https://hashscan.io/mainnet'
|
||||
};
|
||||
|
||||
/**
|
||||
* Get network configuration
|
||||
*/
|
||||
function getNetworkConfig() {
|
||||
return NETWORK_CONFIG;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get account balance using Hedera Mirror Node API
|
||||
* @param {string} address - EVM address (0x...) or Account ID (0.0.xxxx)
|
||||
* @returns {Promise<Object>} - Balance information
|
||||
*/
|
||||
hederaAPI.getBalance = async function(address) {
|
||||
try {
|
||||
const config = getNetworkConfig();
|
||||
|
||||
// Clean address
|
||||
address = address.trim();
|
||||
|
||||
// Determine if it's an EVM address or Account ID
|
||||
let endpoint;
|
||||
if (address.startsWith('0x')) {
|
||||
// EVM address format
|
||||
endpoint = `${config.mirrorNode}/api/v1/accounts/${address}`;
|
||||
} else if (address.match(/^\d+\.\d+\.\d+$/)) {
|
||||
// Account ID format (0.0.xxxx)
|
||||
endpoint = `${config.mirrorNode}/api/v1/accounts/${address}`;
|
||||
} else {
|
||||
throw new Error('Invalid address format. Use EVM address (0x...) or Account ID (0.0.xxxx)');
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
throw new Error('Account not found. Make sure the account exists on the network.');
|
||||
}
|
||||
throw new Error(`API Error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Convert balance from tinybars to HBAR (1 HBAR = 100,000,000 tinybars)
|
||||
const balanceInTinybars = parseInt(data.balance.balance);
|
||||
const balanceInHbar = balanceInTinybars / 100000000;
|
||||
|
||||
return {
|
||||
address: address,
|
||||
accountId: data.account,
|
||||
evmAddress: data.evm_address,
|
||||
balance: balanceInHbar,
|
||||
balanceTinybars: balanceInTinybars,
|
||||
autoRenewPeriod: data.auto_renew_period,
|
||||
expiryTimestamp: data.expiry_timestamp,
|
||||
memo: data.memo,
|
||||
key: data.key
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching balance:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get transaction history using Hedera Mirror Node API
|
||||
* @param {string} address - EVM address or Account ID
|
||||
* @param {Object} options - Query options (limit, order, timestamp)
|
||||
* @returns {Promise<Object>} - Transaction history
|
||||
*/
|
||||
hederaAPI.getTransactionHistory = async function(address, options = {}) {
|
||||
try {
|
||||
const config = getNetworkConfig();
|
||||
address = address.trim();
|
||||
|
||||
// Build query parameters for account endpoint
|
||||
const params = new URLSearchParams();
|
||||
params.append('limit', options.limit || 25);
|
||||
params.append('order', options.order || 'desc');
|
||||
|
||||
if (options.timestamp) {
|
||||
params.append('timestamp', options.timestamp);
|
||||
}
|
||||
|
||||
// Use the account endpoint which includes transactions
|
||||
let endpoint;
|
||||
if (address.startsWith('0x')) {
|
||||
endpoint = `${config.mirrorNode}/api/v1/accounts/${address}?${params}`;
|
||||
} else if (address.match(/^\d+\.\d+\.\d+$/)) {
|
||||
endpoint = `${config.mirrorNode}/api/v1/accounts/${address}?${params}`;
|
||||
} else {
|
||||
throw new Error('Invalid address format');
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
throw new Error('Account not found');
|
||||
}
|
||||
throw new Error(`API Error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Check if transactions exist in response
|
||||
if (!data.transactions || data.transactions.length === 0) {
|
||||
return {
|
||||
transactions: [],
|
||||
links: data.links || {}
|
||||
};
|
||||
}
|
||||
|
||||
// Process transactions
|
||||
const transactions = data.transactions.map(tx => {
|
||||
// Determine transaction type and amount
|
||||
let type = 'unknown';
|
||||
let amount = 0;
|
||||
let counterparty = null;
|
||||
|
||||
if (tx.transfers && tx.transfers.length > 0) {
|
||||
// Find transfers involving our address
|
||||
const accountId = data.account; // Use the account ID from response
|
||||
const ourTransfers = tx.transfers.filter(t =>
|
||||
t.account === address ||
|
||||
t.account === accountId
|
||||
);
|
||||
|
||||
if (ourTransfers.length > 0) {
|
||||
const transfer = ourTransfers[0];
|
||||
amount = Math.abs(transfer.amount) / 100000000; // Convert to HBAR
|
||||
|
||||
if (transfer.amount > 0) {
|
||||
type = 'receive';
|
||||
// Find sender
|
||||
const senderTransfer = tx.transfers.find(t => t.amount < 0);
|
||||
if (senderTransfer) counterparty = senderTransfer.account;
|
||||
} else {
|
||||
type = 'send';
|
||||
// Find receiver
|
||||
const receiverTransfer = tx.transfers.find(t => t.amount > 0 && t.account !== address && t.account !== accountId);
|
||||
if (receiverTransfer) counterparty = receiverTransfer.account;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert Base64 transaction hash to hex format
|
||||
let hexHash = tx.transaction_hash;
|
||||
if (hexHash && !hexHash.startsWith('0x')) {
|
||||
try {
|
||||
const binaryString = atob(hexHash);
|
||||
hexHash = '0x' + Array.from(binaryString)
|
||||
.map(char => char.charCodeAt(0).toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
} catch (e) {
|
||||
console.warn('Could not convert hash to hex:', e);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: tx.transaction_id,
|
||||
hash: hexHash, // Add transaction hash in hex format
|
||||
consensusTimestamp: tx.consensus_timestamp,
|
||||
type: type,
|
||||
amount: amount,
|
||||
counterparty: counterparty,
|
||||
result: tx.result,
|
||||
name: tx.name,
|
||||
memo: tx.memo_base64 ? atob(tx.memo_base64) : '',
|
||||
charged_tx_fee: tx.charged_tx_fee / 100000000, // Convert to HBAR
|
||||
max_fee: tx.max_fee ? tx.max_fee / 100000000 : 0,
|
||||
valid_start_timestamp: tx.valid_start_timestamp,
|
||||
node: tx.node,
|
||||
scheduled: tx.scheduled,
|
||||
nonce: tx.nonce,
|
||||
transfers: tx.transfers
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
transactions: transactions,
|
||||
links: data.links || {}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching transaction history:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get transaction details by transaction ID
|
||||
* @param {string} transactionId - Transaction ID
|
||||
* @returns {Promise<Object>} - Transaction details
|
||||
*/
|
||||
hederaAPI.getTransactionById = async function(transactionId) {
|
||||
try {
|
||||
const config = getNetworkConfig();
|
||||
const endpoint = `${config.mirrorNode}/api/v1/transactions/${transactionId}`;
|
||||
|
||||
const response = await fetch(endpoint);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
throw new Error('Transaction not found');
|
||||
}
|
||||
throw new Error(`API Error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.transactions && data.transactions.length > 0) {
|
||||
const tx = data.transactions[0];
|
||||
|
||||
// Convert Base64 transaction hash to hex format
|
||||
let hexHash = tx.transaction_hash;
|
||||
if (hexHash && !hexHash.startsWith('0x')) {
|
||||
try {
|
||||
// Decode Base64 to binary, then convert to hex
|
||||
const binaryString = atob(hexHash);
|
||||
hexHash = '0x' + Array.from(binaryString)
|
||||
.map(char => char.charCodeAt(0).toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
} catch (e) {
|
||||
console.warn('Could not convert hash to hex:', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Fetch block number based on consensus timestamp
|
||||
let blockNumber = null;
|
||||
try {
|
||||
const blockEndpoint = `${config.mirrorNode}/api/v1/blocks?timestamp=gte:${tx.consensus_timestamp}&limit=1&order=asc`;
|
||||
console.log('Fetching block from:', blockEndpoint);
|
||||
const blockResponse = await fetch(blockEndpoint);
|
||||
console.log('Block response status:', blockResponse.status);
|
||||
if (blockResponse.ok) {
|
||||
const blockData = await blockResponse.json();
|
||||
console.log('Block data:', blockData);
|
||||
if (blockData.blocks && blockData.blocks.length > 0) {
|
||||
blockNumber = blockData.blocks[0].number;
|
||||
console.log('Block number found:', blockNumber);
|
||||
} else {
|
||||
console.warn('No blocks found in response');
|
||||
}
|
||||
} else {
|
||||
console.warn('Block fetch failed with status:', blockResponse.status);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not fetch block number:', e);
|
||||
}
|
||||
|
||||
let memo='';
|
||||
|
||||
return {
|
||||
id: tx.transaction_id,
|
||||
hash: hexHash, // Transaction hash in hex format
|
||||
consensusTimestamp: tx.consensus_timestamp,
|
||||
result: tx.result,
|
||||
name: tx.name,
|
||||
memo: memo,
|
||||
charged_tx_fee: tx.charged_tx_fee / 100000000,
|
||||
max_fee: tx.max_fee ? tx.max_fee / 100000000 : 0,
|
||||
valid_start_timestamp: tx.valid_start_timestamp,
|
||||
node: tx.node,
|
||||
transfers: tx.transfers,
|
||||
block_number: blockNumber,
|
||||
raw: tx
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('Transaction not found');
|
||||
} catch (error) {
|
||||
console.error('Error fetching transaction:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Send HBAR using JSON-RPC Relay (EVM-compatible)
|
||||
* @param {string} fromPrivateKey - Sender's private key (hex format)
|
||||
* @param {string} toAddress - Recipient's EVM address (0x...) - Account IDs should be converted to EVM addresses before calling
|
||||
* @param {number} amount - Amount in HBAR
|
||||
* @param {string} memo - Optional memo
|
||||
* @returns {Promise<Object>} - Transaction result
|
||||
*/
|
||||
hederaAPI.sendHBAR = async function(fromPrivateKey, toAddress, amount, memo = '') {
|
||||
try {
|
||||
const config = getNetworkConfig();
|
||||
|
||||
// Validate inputs
|
||||
if (!fromPrivateKey || fromPrivateKey.length !== 64) {
|
||||
throw new Error('Invalid private key format. Expected 64-character hex string.');
|
||||
}
|
||||
|
||||
if (!toAddress || !toAddress.startsWith('0x')) {
|
||||
throw new Error('Invalid recipient address. Expected EVM address (0x...)');
|
||||
}
|
||||
|
||||
if (amount <= 0) {
|
||||
throw new Error('Amount must be greater than 0');
|
||||
}
|
||||
|
||||
// Use Web3.js to create and sign the transaction
|
||||
if (typeof Web3 === 'undefined') {
|
||||
throw new Error('Web3.js is required for sending transactions');
|
||||
}
|
||||
|
||||
const web3 = new Web3(config.jsonRpcRelay);
|
||||
|
||||
// Add 0x prefix to private key if not present
|
||||
const privateKey = fromPrivateKey.startsWith('0x') ? fromPrivateKey : '0x' + fromPrivateKey;
|
||||
|
||||
// Create account from private key
|
||||
const account = web3.eth.accounts.privateKeyToAccount(privateKey);
|
||||
const fromAddress = account.address;
|
||||
|
||||
// Get current gas price
|
||||
const gasPrice = await web3.eth.getGasPrice();
|
||||
|
||||
// Get nonce
|
||||
const nonce = await web3.eth.getTransactionCount(fromAddress, 'pending');
|
||||
|
||||
// Convert HBAR to Wei (1 HBAR = 10^18 Wei in EVM context)
|
||||
|
||||
const amountString = typeof amount === 'number' ? amount.toFixed(18) : amount.toString();
|
||||
const amountInWei = web3.utils.toWei(amountString, 'ether');
|
||||
|
||||
// Prepare transaction object for gas estimation
|
||||
let gasLimit = 21000; // Default for existing accounts
|
||||
|
||||
// Try to estimate gas (will be higher for new accounts)
|
||||
try {
|
||||
const estimatedGas = await web3.eth.estimateGas({
|
||||
from: fromAddress,
|
||||
to: toAddress,
|
||||
value: amountInWei
|
||||
});
|
||||
gasLimit = Math.floor(estimatedGas * 1.2); // Add 20% buffer
|
||||
console.log('Estimated gas:', estimatedGas, 'Using:', gasLimit);
|
||||
} catch (error) {
|
||||
// If estimation fails, use high limit for new account creation
|
||||
console.warn('Gas estimation failed, using high limit for potential new account:', error.message);
|
||||
gasLimit = 800000; // High limit for new account auto-creation
|
||||
}
|
||||
|
||||
// Prepare transaction
|
||||
const tx = {
|
||||
from: fromAddress,
|
||||
to: toAddress,
|
||||
value: amountInWei,
|
||||
gas: gasLimit,
|
||||
gasPrice: gasPrice,
|
||||
nonce: nonce,
|
||||
chainId: config.chainId
|
||||
};
|
||||
|
||||
|
||||
// Sign transaction
|
||||
const signedTx = await account.signTransaction(tx);
|
||||
|
||||
// Send transaction
|
||||
const receipt = await web3.eth.sendSignedTransaction(signedTx.rawTransaction);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
transactionHash: receipt.transactionHash,
|
||||
blockNumber: receipt.blockNumber,
|
||||
from: receipt.from,
|
||||
to: receipt.to,
|
||||
gasUsed: receipt.gasUsed,
|
||||
status: receipt.status,
|
||||
explorerUrl: `${config.explorer}/transaction/${receipt.transactionHash}`
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error sending HBAR:', error);
|
||||
|
||||
// Parse error message
|
||||
let errorMessage = error.message;
|
||||
if (error.message.includes('insufficient funds')) {
|
||||
errorMessage = 'Insufficient balance to complete this transaction';
|
||||
} else if (error.message.includes('nonce')) {
|
||||
errorMessage = 'Transaction nonce error. Please try again.';
|
||||
} else if (error.message.includes('gas')) {
|
||||
errorMessage = 'Gas estimation failed. Please check the transaction details.';
|
||||
}
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Validate address format
|
||||
* @param {string} address - Address to validate
|
||||
* @returns {Object} - Validation result
|
||||
*/
|
||||
hederaAPI.validateAddress = function(address) {
|
||||
address = address.trim();
|
||||
|
||||
// Check EVM address format
|
||||
if (address.startsWith('0x')) {
|
||||
const isValid = /^0x[a-fA-F0-9]{40}$/.test(address);
|
||||
return {
|
||||
valid: isValid,
|
||||
type: 'evm',
|
||||
address: address
|
||||
};
|
||||
}
|
||||
|
||||
// Check Account ID format (0.0.xxxx)
|
||||
if (address.match(/^\d+\.\d+\.\d+$/)) {
|
||||
return {
|
||||
valid: true,
|
||||
type: 'accountId',
|
||||
address: address
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
type: 'unknown',
|
||||
address: address
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Format timestamp to readable date
|
||||
* @param {string} timestamp - Consensus timestamp
|
||||
* @returns {string} - Formatted date
|
||||
*/
|
||||
hederaAPI.formatTimestamp = function(timestamp) {
|
||||
if (!timestamp) return 'N/A';
|
||||
|
||||
// Timestamp format: seconds.nanoseconds
|
||||
const [seconds, nanoseconds] = timestamp.split('.');
|
||||
const date = new Date(parseInt(seconds) * 1000);
|
||||
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
})(typeof module === "object" ? module.exports : (window.hederaAPI = {}));
|
||||
181
hederaCrypto.js
Normal file
181
hederaCrypto.js
Normal file
@ -0,0 +1,181 @@
|
||||
(function (EXPORTS) {
|
||||
"use strict";
|
||||
const hederaCrypto = EXPORTS;
|
||||
|
||||
function hexToBytes(hex) {
|
||||
const bytes = [];
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
bytes.push(parseInt(hex.substr(i, 2), 16));
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function bytesToHex(bytes) {
|
||||
return Array.from(bytes)
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
|
||||
// Generate a new random key
|
||||
function generateNewID() {
|
||||
var key = new Bitcoin.ECKey(false);
|
||||
key.setCompressed(true);
|
||||
return {
|
||||
floID: key.getBitcoinAddress(),
|
||||
pubKey: key.getPubKeyHex(),
|
||||
privKey: key.getBitcoinWalletImportFormat(),
|
||||
};
|
||||
}
|
||||
|
||||
Object.defineProperties(hederaCrypto, {
|
||||
newID: {
|
||||
get: () => generateNewID(),
|
||||
},
|
||||
hashID: {
|
||||
value: (str) => {
|
||||
let bytes = ripemd160(Crypto.SHA256(str, { asBytes: true }), {
|
||||
asBytes: true,
|
||||
});
|
||||
bytes.unshift(bitjs.pub);
|
||||
var hash = Crypto.SHA256(Crypto.SHA256(bytes, { asBytes: true }), {
|
||||
asBytes: true,
|
||||
});
|
||||
var checksum = hash.slice(0, 4);
|
||||
return bitjs.Base58.encode(bytes.concat(checksum));
|
||||
},
|
||||
},
|
||||
tmpID: {
|
||||
get: () => {
|
||||
let bytes = Crypto.util.randomBytes(20);
|
||||
bytes.unshift(bitjs.pub);
|
||||
var hash = Crypto.SHA256(Crypto.SHA256(bytes, { asBytes: true }), {
|
||||
asBytes: true,
|
||||
});
|
||||
var checksum = hash.slice(0, 4);
|
||||
return bitjs.Base58.encode(bytes.concat(checksum));
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// --- Multi-chain Generator (BTC, FLO, HBAR) ---
|
||||
hederaCrypto.generateMultiChain = async function (inputWif) {
|
||||
const versions = {
|
||||
BTC: { pub: 0x00, priv: 0x80 },
|
||||
FLO: { pub: 0x23, priv: 0xa3 },
|
||||
};
|
||||
|
||||
const origBitjsPub = bitjs.pub;
|
||||
const origBitjsPriv = bitjs.priv;
|
||||
const origBitjsCompressed = bitjs.compressed;
|
||||
const origCoinJsCompressed = coinjs.compressed;
|
||||
|
||||
bitjs.compressed = true;
|
||||
coinjs.compressed = true;
|
||||
|
||||
let privKeyHex;
|
||||
let compressed = true;
|
||||
|
||||
// --- Decode input or generate new ---
|
||||
if (typeof inputWif === "string" && inputWif.trim().length > 0) {
|
||||
const trimmedInput = inputWif.trim();
|
||||
const hexOnly = /^[0-9a-fA-F]+$/.test(trimmedInput);
|
||||
|
||||
if (hexOnly && (trimmedInput.length === 64 || trimmedInput.length === 128)) {
|
||||
privKeyHex =
|
||||
trimmedInput.length === 128 ? trimmedInput.substring(0, 64) : trimmedInput;
|
||||
} else {
|
||||
try {
|
||||
const decode = Bitcoin.Base58.decode(trimmedInput);
|
||||
const keyWithVersion = decode.slice(0, decode.length - 4);
|
||||
let key = keyWithVersion.slice(1);
|
||||
if (key.length >= 33 && key[key.length - 1] === 0x01) {
|
||||
key = key.slice(0, key.length - 1);
|
||||
compressed = true;
|
||||
}
|
||||
privKeyHex = bytesToHex(key);
|
||||
} catch (e) {
|
||||
console.warn("Invalid WIF, generating new key:", e);
|
||||
const newKey = generateNewID();
|
||||
const decode = Bitcoin.Base58.decode(newKey.privKey);
|
||||
const keyWithVersion = decode.slice(0, decode.length - 4);
|
||||
let key = keyWithVersion.slice(1);
|
||||
if (key.length >= 33 && key[key.length - 1] === 0x01)
|
||||
key = key.slice(0, key.length - 1);
|
||||
privKeyHex = bytesToHex(key);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Generate new key if no input
|
||||
const newKey = generateNewID();
|
||||
const decode = Bitcoin.Base58.decode(newKey.privKey);
|
||||
const keyWithVersion = decode.slice(0, decode.length - 4);
|
||||
let key = keyWithVersion.slice(1);
|
||||
if (key.length >= 33 && key[key.length - 1] === 0x01)
|
||||
key = key.slice(0, key.length - 1);
|
||||
privKeyHex = bytesToHex(key);
|
||||
}
|
||||
|
||||
// --- Derive addresses for each chain ---
|
||||
const result = { BTC: {}, FLO: {}, HBAR: {} };
|
||||
|
||||
// BTC
|
||||
bitjs.pub = versions.BTC.pub;
|
||||
bitjs.priv = versions.BTC.priv;
|
||||
const pubKeyBTC = bitjs.newPubkey(privKeyHex);
|
||||
result.BTC.address = coinjs.bech32Address(pubKeyBTC).address;
|
||||
result.BTC.privateKey = bitjs.privkey2wif(privKeyHex);
|
||||
|
||||
// FLO
|
||||
bitjs.pub = versions.FLO.pub;
|
||||
bitjs.priv = versions.FLO.priv;
|
||||
const pubKeyFLO = bitjs.newPubkey(privKeyHex);
|
||||
result.FLO.address = bitjs.pubkey2address(pubKeyFLO);
|
||||
result.FLO.privateKey = bitjs.privkey2wif(privKeyHex);
|
||||
|
||||
// HBAR (Hedera)
|
||||
try {
|
||||
const privBytes = hexToBytes(privKeyHex.substring(0, 64));
|
||||
|
||||
// Create ECDSA key from private key bytes
|
||||
const ecKey = new Bitcoin.ECKey(privBytes);
|
||||
ecKey.setCompressed(false); // Uncompressed for EVM address derivation
|
||||
|
||||
// Get uncompressed public key (65 bytes: 04 + 32 bytes X + 32 bytes Y)
|
||||
const pubKeyHex = ecKey.getPubKeyHex();
|
||||
|
||||
|
||||
// Derive EVM address from public key using Keccak-256
|
||||
// Remove '04' prefix and hash the remaining 64 bytes
|
||||
const pubKeyBytes = pubKeyHex.substring(2);
|
||||
|
||||
// Use web3.js for proper Keccak-256 hash (Ethereum standard)
|
||||
const hash = Web3.utils.keccak256('0x' + pubKeyBytes);
|
||||
// hash is '0x...' format, take last 20 bytes (40 hex chars)
|
||||
const evmAddress = '0x' + hash.substring(26);
|
||||
|
||||
// Compressed public key for display
|
||||
ecKey.setCompressed(true);
|
||||
const compressedPubKey = ecKey.getPubKeyHex();
|
||||
|
||||
result.HBAR.evmAddress = evmAddress;
|
||||
result.HBAR.publicKey = compressedPubKey;
|
||||
result.HBAR.privateKey = privKeyHex.substring(0, 64);
|
||||
result.HBAR.address = evmAddress; // EVM address
|
||||
} catch (error) {
|
||||
console.error("Error generating HBAR keys:", error);
|
||||
result.HBAR.evmAddress = "Error generating address";
|
||||
result.HBAR.publicKey = "Error";
|
||||
result.HBAR.privateKey = privKeyHex;
|
||||
result.HBAR.address = "Error";
|
||||
}
|
||||
|
||||
bitjs.pub = origBitjsPub;
|
||||
bitjs.priv = origBitjsPriv;
|
||||
bitjs.compressed = origBitjsCompressed;
|
||||
coinjs.compressed = origCoinJsCompressed;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
})(typeof module === "object" ? module.exports : (window.hederaCrypto = {}));
|
||||
120
hederaSearchDB.js
Normal file
120
hederaSearchDB.js
Normal file
@ -0,0 +1,120 @@
|
||||
class SearchedAddressDB {
|
||||
constructor() {
|
||||
this.dbName = "HederaWalletDB";
|
||||
this.version = 1;
|
||||
this.storeName = "searchedAddresses";
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
async init() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.dbName, this.version);
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result;
|
||||
resolve();
|
||||
};
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
if (!db.objectStoreNames.contains(this.storeName)) {
|
||||
const store = db.createObjectStore(this.storeName, {
|
||||
keyPath: "id",
|
||||
autoIncrement: true
|
||||
});
|
||||
store.createIndex("timestamp", "timestamp", { unique: false });
|
||||
store.createIndex("hbarAddress", "hbarAddress", { unique: false });
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async saveSearchedAddress(
|
||||
hbarAddress,
|
||||
balance,
|
||||
timestamp = Date.now(),
|
||||
sourceInfo = null
|
||||
) {
|
||||
if (!this.db) await this.init();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], "readwrite");
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const index = store.index("hbarAddress");
|
||||
|
||||
// Check if address already exists
|
||||
const getRequest = index.getAll(hbarAddress);
|
||||
getRequest.onsuccess = () => {
|
||||
const existingRecords = getRequest.result;
|
||||
|
||||
if (existingRecords.length > 0) {
|
||||
// Address exists, update the existing record
|
||||
const existingRecord = existingRecords[0];
|
||||
const updatedData = {
|
||||
...existingRecord,
|
||||
balance,
|
||||
timestamp,
|
||||
formattedBalance: `${balance.toFixed(8)} HBAR`,
|
||||
};
|
||||
|
||||
const putRequest = store.put(updatedData);
|
||||
putRequest.onsuccess = () => resolve();
|
||||
putRequest.onerror = () => reject(putRequest.error);
|
||||
} else {
|
||||
// Address doesn't exist, create new record
|
||||
const data = {
|
||||
hbarAddress,
|
||||
btcAddress: sourceInfo?.btcAddress || null,
|
||||
floAddress: sourceInfo?.floAddress || null,
|
||||
balance,
|
||||
timestamp,
|
||||
formattedBalance: `${balance.toFixed(8)} HBAR`,
|
||||
isFromPrivateKey: !!(sourceInfo?.btcAddress || sourceInfo?.floAddress),
|
||||
};
|
||||
|
||||
const addRequest = store.add(data);
|
||||
addRequest.onsuccess = () => resolve();
|
||||
addRequest.onerror = () => reject(addRequest.error);
|
||||
}
|
||||
};
|
||||
getRequest.onerror = () => reject(getRequest.error);
|
||||
});
|
||||
}
|
||||
|
||||
async getSearchedAddresses() {
|
||||
if (!this.db) await this.init();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], "readonly");
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const index = store.index("timestamp");
|
||||
const request = index.getAll();
|
||||
request.onsuccess = () => {
|
||||
const results = request.result.sort(
|
||||
(a, b) => b.timestamp - a.timestamp
|
||||
);
|
||||
resolve(results.slice(0, 10));
|
||||
};
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteSearchedAddress(id) {
|
||||
if (!this.db) await this.init();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], "readwrite");
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.delete(id);
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
async clearAllSearchedAddresses() {
|
||||
if (!this.db) await this.init();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], "readwrite");
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.clear();
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
}
|
||||
BIN
hedera_favicon.png
Normal file
BIN
hedera_favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 386 KiB |
2190
index.html
Normal file
2190
index.html
Normal file
File diff suppressed because it is too large
Load Diff
11473
lib.hedera.js
Normal file
11473
lib.hedera.js
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user