feat: Integrate Stellar blockchain API, enable transaction and send features, and improve Stellar address/key handling.

This commit is contained in:
void-57 2025-12-17 02:52:27 +05:30
parent 7f6e1b02a1
commit 11f16d5ce9
4 changed files with 868 additions and 86 deletions

View File

@ -48,13 +48,13 @@
</a>
</li>
<li>
<a href="#" class="nav-link disabled" style="opacity: 0.5; cursor: not-allowed; pointer-events: none;">
<a href="#" class="nav-link" data-page="transactions">
<i class="fas fa-history"></i>
<span>Transactions</span>
</a>
</li>
<li>
<a href="#" class="nav-link disabled" style="opacity: 0.5; cursor: not-allowed; pointer-events: none;">
<a href="#" class="nav-link" data-page="send">
<i class="fas fa-paper-plane"></i>
<span>Send</span>
</a>
@ -678,19 +678,21 @@
<!-- required libraries -->
<script src="https://cdn.jsdelivr.net/npm/tweetnacl@1.0.3/nacl.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/js-sha512@0.8.0/build/sha512.min.js"></script>
<!-- Stellar SDK for transaction building and signing -->
<script src="https://cdn.jsdelivr.net/npm/stellar-sdk@11.3.0/dist/stellar-sdk.min.js"></script>
<script src="lib.stellar.js"></script>
<script src="stellarCrypto.js"></script>
<!-- Stellar blockchain API integration - Coming soon -->
<!-- <script src="stellarBlockchainAPI.js"></script> -->
<!-- <script src="stellarSearchDB.js"></script> -->
<!-- Stellar blockchain API integration -->
<script src="stellarBlockchainAPI.js"></script>
<script src="stellarSearchDB.js"></script>
<script>
let currentXlmAddress = null;
let currentXlmPrivateKey = null;
let txNextToken = null;
let currentSearchType = 'address';
// SearchDB disabled for now - will be implemented with Stellar API
// let searchDB = new SearchedAddressDB();
// SearchDB for storing recent searches
let searchDB = new SearchedAddressDB();
let currentTxFilter = 'all'; // Transaction filter state
function filterTransactions(type) {
@ -941,39 +943,56 @@
let sourceInfo = null;
try {
// Check if input is an address (58 chars) or private key (hex/WIF format)
if (input.length === 58) {
// It's an xlm address - validate it contains only valid base32 characters
const validxlmChars = /^[A-Z2-7]+$/;
if (!validxlmChars.test(input)) {
showNotification('⚠️ Invalid xlm address format', 'warning');
// Check if input is a Stellar address (56 chars, starts with G)
if (input.length === 56 && input.startsWith('G')) {
// It's a Stellar address - validate it contains only valid base32 characters
const validStellarChars = /^[A-Z2-7]+$/;
if (!validStellarChars.test(input)) {
showNotification('⚠️ Invalid Stellar address format', 'warning');
searchBtn.disabled = false;
searchBtn.innerHTML = originalContent;
return;
}
address = input;
} else if (input.length === 56 && input.startsWith('S')) {
// It's a Stellar secret key - derive the address
const result = await stellarCrypto.generateMultiChain(input);
address = result.XLM.address;
sourceInfo = {
privateKey: input,
btcAddress: result.BTC.address,
floAddress: result.FLO.address
};
} else {
// Check if it's a valid private key format
// Check if it's a valid BTC/FLO private key format
const hexOnly = /^[0-9a-fA-F]+$/.test(input);
const isHexKey = hexOnly && (input.length === 64 || input.length === 128);
const isWifKey = !hexOnly && !(/^[A-Z2-7]+$/.test(input)) && input.length >= 51 && input.length <= 52;
// Check if it's a hex transaction hash (64 hex chars, all lowercase or mixed case)
if (hexOnly && input.length === 64 && /[a-f]/.test(input.toLowerCase())) {
showNotification('⚠️ This looks like a transaction hash. Please use "Transaction Hash" search instead', 'warning');
searchBtn.disabled = false;
searchBtn.innerHTML = originalContent;
return;
}
if (isHexKey || isWifKey) {
// It's a private key (WIF or hex), derive the xlm address
// It's a BTC/FLO private key (WIF or hex), derive the Stellar address
const result = await stellarCrypto.generateMultiChain(input);
address = result.xlm.address;
address = result.XLM.address;
sourceInfo = {
privateKey: input,
btcAddress: result.BTC.address,
floAddress: result.FLO.address
};
} else if (/^[A-Z2-7]+$/.test(input) && input.length === 52) {
showNotification('⚠️ This looks like a transaction ID. Please use "Transaction Hash" search instead', 'warning');
} else if (/^[A-Z2-7]+$/.test(input) && input.length === 64) {
showNotification('⚠️ This looks like a transaction hash. Please use "Transaction Hash" search instead', 'warning');
searchBtn.disabled = false;
searchBtn.innerHTML = originalContent;
return;
} else {
showNotification('⚠️ Invalid format. Enter a valid xlm address (58 chars) or private key ', 'warning');
showNotification('⚠️ Invalid format. Enter a valid Stellar address (56 chars, starts with G) or private key', 'warning');
searchBtn.disabled = false;
searchBtn.innerHTML = originalContent;
return;
@ -988,13 +1007,33 @@
document.getElementById('tx-results').style.display = 'block';
document.getElementById('transactionFilterSection').style.display = 'block';
await refreshxlmData();
// Try to fetch balance and transactions
let balance = 0;
let isInactive = false;
// Save to database after successful balance fetch
const accountInfo = await xlmAPI.getBalance(address);
try {
const accountInfo = await xlmAPI.getBalance(address);
balance = accountInfo.balanceXlm;
const balanceEl = document.getElementById('xlm-balance');
balanceEl.innerHTML = balance.toFixed(6) + ' <span class="currency">xlm</span>';
txNextToken = null;
await loadTransactions(true);
} catch (error) {
console.error('Error fetching data:', error);
isInactive = true;
// Show 0 balance for inactive accounts
const balanceEl = document.getElementById('xlm-balance');
balanceEl.innerHTML = '0 <span class="currency">xlm</span>';
document.getElementById('tx-history').innerHTML = '<div class="tx-empty">Account is inactive or not funded yet</div>';
}
// Save to database regardless of active/inactive status
await searchDB.saveSearchedAddress(
address,
accountInfo.balancexlm,
balance,
Date.now(),
sourceInfo
);
@ -1032,8 +1071,8 @@
document.getElementById('tx-detail-id').textContent = tx.id;
document.getElementById('tx-detail-from').textContent = tx.sender;
document.getElementById('tx-detail-to').textContent = tx.receiver || '-';
document.getElementById('tx-detail-amount').textContent = tx.amountxlm.toFixed(6) + ' xlm';
document.getElementById('tx-detail-fee').textContent = (tx.fee / 1000000).toFixed(6) + ' xlm';
document.getElementById('tx-detail-amount').textContent = (tx.amountXlm || 0).toFixed(7) + ' XLM';
document.getElementById('tx-detail-fee').textContent = (tx.feeXlm || 0).toFixed(7) + ' XLM';
document.getElementById('tx-detail-round').textContent = tx.confirmedRound;
const date = new Date(tx.roundTime * 1000);
@ -1050,7 +1089,7 @@
// Update Explorer Link
const explorerLink = document.getElementById('tx-explorer-link');
explorerLink.href = `https://allo.info/tx/${tx.id}`;
explorerLink.href = `https://stellar.expert/explorer/public/tx/${tx.hash}`;
document.getElementById('txhash-results').style.display = 'block';
@ -1096,26 +1135,27 @@
// Validate private key format
const hexOnly = /^[0-9a-fA-F]+$/.test(privateKey);
const isHexKey = hexOnly && (privateKey.length === 64 || privateKey.length === 128);
const isStellarSecret = privateKey.startsWith('S') && privateKey.length === 56 && /^[A-Z2-7]+$/.test(privateKey);
// Check for WIF key (Base58: 51-52 chars, NOT Base32)
const isBase32 = /^[A-Z2-7]+$/.test(privateKey); // Transaction IDs are Base32
const isWifKey = !hexOnly && !isBase32 && privateKey.length >= 51 && privateKey.length <= 52;
if (!isHexKey && !isWifKey) {
if (!isHexKey && !isWifKey && !isStellarSecret) {
document.getElementById('send-wallet-info').style.display = 'none';
return;
}
try {
const result = await stellarCrypto.generateMultiChain(privateKey);
const address = result.xlm.address;
const address = result.XLM.address;
const accountInfo = await xlmAPI.getBalance(address);
const balanceEl = document.getElementById('send-balance');
balanceEl.innerHTML = accountInfo.balancexlm.toFixed(6) + ' <span class="currency">xlm</span>';
document.getElementById('send-from-address').textContent = address.substring(0, 12) + '...' + address.substring(50);
balanceEl.innerHTML = accountInfo.balanceXlm.toFixed(7) + ' <span class="currency">XLM</span>';
document.getElementById('send-from-address').textContent = address.substring(0, 12) + '...' + address.substring(address.length - 12);
document.getElementById('send-wallet-info').style.display = 'block';
} catch (error) {
document.getElementById('send-wallet-info').style.display = 'none';
@ -1132,7 +1172,7 @@
const accountInfo = await xlmAPI.getBalance(currentxlmAddress);
const balanceEl = document.getElementById('xlm-balance');
balanceEl.innerHTML = accountInfo.balancexlm.toFixed(6) + ' <span class="currency">xlm</span>';
balanceEl.innerHTML = accountInfo.balanceXlm.toFixed(6) + ' <span class="currency">xlm</span>';
txNextToken = null;
await loadTransactions(true);
@ -1274,7 +1314,7 @@
<div class="tx-id" title="${tx.id}">TX ID: ${shortTxId}</div>
</div>
<div class="tx-amount ${isSent ? 'sent' : 'received'}">
${isSent ? '-' : '+'}${tx.amountxlm.toFixed(6)} xlm
${isSent ? '-' : '+'}${tx.amountXlm.toFixed(7)} XLM
</div>
`;
@ -1319,8 +1359,11 @@
item.innerHTML = `
${search.isFromPrivateKey ? `
<div class="recent-chain-buttons">
<button class="chain-btn active" onclick="showAddressForChain(${search.id}, 'xlm', '${search.xlmAddress}')" title="xlm">
<svg class="chain-icon-svg" viewBox="0 0 507.56 509.36" fill="currentColor"><polygon points="88.04 509.36 161.7 381.8 235.37 254.68 308.58 127.12 320.71 106.9 326.1 127.12 348.56 211.11 323.4 254.68 249.74 381.8 176.53 509.36 264.56 509.36 338.23 381.8 376.41 315.77 394.37 381.8 428.51 509.36 507.56 509.36 473.43 381.8 439.29 254.68 430.31 221.89 485.11 127.12 405.15 127.12 402.46 117.68 374.61 13.47 371.02 0 294.21 0 292.41 2.69 220.54 127.12 146.88 254.68 73.66 381.8 0 509.36 88.04 509.36"/></svg>
<button class="chain-btn active" onclick="showAddressForChain(${search.id}, 'xlm', '${search.xlmAddress}')" title="XLM">
<svg class="chain-icon-svg" viewBox="0 0 236.36 200" fill="currentColor">
<path d="M203,26.16l-28.46,14.5-137.43,70a82.49,82.49,0,0,1-.7-10.69A81.87,81.87,0,0,1,158.2,28.6l16.29-8.3,2.43-1.24A100,100,0,0,0,18.18,100q0,3.82.29,7.61a18.19,18.19,0,0,1-9.88,17.58L0,129.57V150l25.29-12.89,0,0,8.19-4.18,8.07-4.11v0L186.43,55l16.28-8.29,33.65-17.15V9.14Z"/>
<path d="M236.36,50,49.78,145,33.5,153.31,0,170.38v20.41l33.27-16.95,28.46-14.5L199.3,89.24A83.45,83.45,0,0,1,200,100,81.87,81.87,0,0,1,78.09,171.36l-1,.53-17.66,9A100,100,0,0,0,218.18,100c0-2.57-.1-5.14-.29-7.68a18.2,18.2,0,0,1,9.87-17.58l8.6-4.38Z"/>
</svg>
</button>
<button class="chain-btn" onclick="showAddressForChain(${search.id}, 'BTC', '${search.btcAddress}')" title="BTC">
<i class="fab fa-bitcoin"></i>
@ -1475,21 +1518,24 @@
// Validate private key format
const hexOnly = /^[0-9a-fA-F]+$/.test(privateKey);
const isHexKey = hexOnly && (privateKey.length === 64 || privateKey.length === 128);
const isStellarSecret = privateKey.startsWith('S') && privateKey.length === 56 && /^[A-Z2-7]+$/.test(privateKey);
// Check for WIF key (Base58: 51-52 chars, NOT Base32)
const isBase32 = /^[A-Z2-7]+$/.test(privateKey); // Transaction IDs are Base32
const isWifKey = !hexOnly && !isBase32 && privateKey.length >= 51 && privateKey.length <= 52;
if (!isHexKey && !isWifKey) {
if (isBase32 && privateKey.length === 52) {
showNotification('⚠️ This looks like a transaction ID, not a private key. Please enter a valid private key.', 'warning');
if (!isHexKey && !isWifKey && !isStellarSecret) {
if (isBase32 && privateKey.length === 64) {
showNotification('⚠️ This looks like a transaction hash, not a private key. Please enter a valid private key.', 'warning');
} else if (privateKey.startsWith('G') && privateKey.length === 56) {
showNotification('⚠️ This is a public address, not a secret key. Please enter a secret key (starts with S).', 'warning');
} else {
showNotification('⚠️ Invalid private key format. Please enter a valid private key', 'warning');
showNotification('⚠️ Invalid private key format. Please enter a valid private key (hex, WIF, or Stellar secret key)', 'warning');
}
return;
}
if (!recipient || recipient.length !== 58) {
showNotification('⚠️ Please enter valid recipient address (58 chars)', 'warning');
if (!recipient || recipient.length !== 56 || !recipient.startsWith('G')) {
showNotification('⚠️ Please enter valid Stellar recipient address (56 chars, starts with G)', 'warning');
return;
}
if (!amount || amount <= 0) {
@ -1504,25 +1550,25 @@
try {
// Derive address from private key
const walletResult = await stellarCrypto.generateMultiChain(privateKey);
const fromAddress = walletResult.xlm.address;
const xlmPrivateKey = walletResult.xlm.privateKey;
const fromAddress = walletResult.XLM.address;
const xlmPrivateKey = walletResult.XLM.privateKey;
// Get current balance
const accountInfo = await xlmAPI.getBalance(fromAddress);
const currentBalance = accountInfo.balancexlm;
const minBalance = accountInfo.minBalance / 1000000; // Convert microxlms to xlm
const currentBalance = accountInfo.balanceXlm;
const minBalance = accountInfo.minBalance; // Already in XLM
// Get transaction parameters
const txParams = await xlmAPI.getTransactionParams();
const feexlm = txParams.fee / 1000000;
const totalxlm = amount + feexlm;
const feeXlm = txParams.fee / 10000000; // Convert stroops to XLM
const totalXlm = amount + feeXlm;
// Calculate remaining balance after transaction
const remainingBalance = currentBalance - totalxlm;
const remainingBalance = currentBalance - totalXlm;
// Check if balance is sufficient (must have enough for amount + fee)
if (totalxlm > currentBalance) {
const errorMsg = `Insufficient balance! You need ${totalxlm.toFixed(6)} xlm (${amount.toFixed(6)} + ${feexlm.toFixed(6)} fee) but only have ${currentBalance.toFixed(6)} xlm available.`;
if (totalXlm > currentBalance) {
const errorMsg = `Insufficient balance! You need ${totalXlm.toFixed(7)} XLM (${amount.toFixed(7)} + ${feeXlm.toFixed(7)} fee) but only have ${currentBalance.toFixed(7)} XLM available.`;
showNotification('❌ ' + errorMsg, 'error');
// Show error in output area as well
@ -1538,23 +1584,23 @@
<div class="tx-details-body">
<div class="tx-detail-row">
<span class="detail-label">Current Balance</span>
<span class="detail-value">${currentBalance.toFixed(6)} xlm</span>
<span class="detail-value">${currentBalance.toFixed(6)} XLM</span>
</div>
<div class="tx-detail-row">
<span class="detail-label">Amount to Send</span>
<span class="detail-value">${amount.toFixed(6)} xlm</span>
<span class="detail-value">${amount.toFixed(6)} XLM</span>
</div>
<div class="tx-detail-row">
<span class="detail-label">Transaction Fee</span>
<span class="detail-value fee">${feexlm.toFixed(6)} xlm</span>
<span class="detail-value fee">${feeXLM.toFixed(6)} XLM</span>
</div>
<div class="tx-detail-row highlight" style="color: var(--error-color);">
<span class="detail-label">Total Required</span>
<span class="detail-value">${totalxlm.toFixed(6)} xlm</span>
<span class="detail-value">${totalXLM.toFixed(6)} XLM</span>
</div>
<div class="tx-detail-row" style="color: var(--error-color); font-weight: 600;">
<span class="detail-label">Shortfall</span>
<span class="detail-value">${(totalxlm - currentBalance).toFixed(6)} xlm</span>
<span class="detail-value">${(totalXLM - currentBalance).toFixed(6)} XLM</span>
</div>
</div>
</div>
@ -1565,8 +1611,8 @@
// Check if remaining balance meets minimum balance requirement
if (remainingBalance < minBalance) {
const maxSendable = currentBalance - minBalance - feexlm;
const errorMsg = `Transaction would leave account below minimum balance! Minimum balance required: ${minBalance.toFixed(6)} xlm. After sending ${amount.toFixed(6)} xlm + ${feexlm.toFixed(6)} fee, only ${remainingBalance.toFixed(6)} xlm would remain.`;
const maxSendable = currentBalance - minBalance - feeXlm;
const errorMsg = `Transaction would leave account below minimum balance! Minimum balance required: ${minBalance.toFixed(7)} XLM. After sending ${amount.toFixed(7)} XLM + ${feeXlm.toFixed(7)} fee, only ${remainingBalance.toFixed(7)} XLM would remain.`;
showNotification('❌ ' + errorMsg, 'error');
// Show error in output area as well
@ -1590,7 +1636,7 @@
</div>
<div class="tx-detail-row">
<span class="detail-label">Transaction Fee</span>
<span class="detail-value fee">${feexlm.toFixed(6)} xlm</span>
<span class="detail-value fee">${feeXlm.toFixed(7)} XLM</span>
</div>
<div class="tx-detail-row highlight" style="color: var(--warning-color);">
<span class="detail-label">Remaining After TX</span>
@ -1618,8 +1664,8 @@
amount: amount,
microAmount: Math.floor(amount * 1000000),
fee: txParams.fee,
feexlm: feexlm,
total: totalxlm,
feeXlm: feeXlm,
total: totalXlm,
privateKey: xlmPrivateKey,
txParams: txParams
};
@ -1639,9 +1685,9 @@
function showConfirmModal() {
document.getElementById('confirm-from').textContent = pendingTx.from.substring(0, 12) + '...' + pendingTx.from.substring(50);
document.getElementById('confirm-to').textContent = pendingTx.to.substring(0, 12) + '...' + pendingTx.to.substring(50);
document.getElementById('confirm-amount').textContent = pendingTx.amount.toFixed(6) + ' xlm';
document.getElementById('confirm-fee').textContent = pendingTx.feexlm.toFixed(6) + ' xlm';
document.getElementById('confirm-total').textContent = pendingTx.total.toFixed(6) + ' xlm';
document.getElementById('confirm-amount').textContent = pendingTx.amount.toFixed(7) + ' XLM';
document.getElementById('confirm-fee').textContent = pendingTx.feeXlm.toFixed(7) + ' XLM';
document.getElementById('confirm-total').textContent = pendingTx.total.toFixed(7) + ' XLM';
document.getElementById('confirm-modal').style.display = 'flex';
}
@ -1654,35 +1700,46 @@
async function confirmAndSend() {
if (!pendingTx) return;
// Check if Stellar SDK is initialized
if (!stellarAPI.isInitialized()) {
showNotification('❌ Stellar SDK not initialized. Attempting to initialize...', 'error');
const initSuccess = stellarAPI.forceInit();
if (!initSuccess) {
showNotification('❌ Failed to initialize Stellar SDK. Please refresh the page.', 'error');
closeConfirmModal();
return;
}
showNotification('Stellar SDK initialized! Please try sending again.', 'success');
closeConfirmModal();
return;
}
const confirmBtn = document.getElementById('confirm-send-btn');
confirmBtn.disabled = true;
confirmBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Sending...';
try {
// Build and sign the transaction
const signedTxBytes = await stellarCrypto.createSignedPaymentTx({
from: pendingTx.from,
to: pendingTx.to,
amount: pendingTx.microAmount,
fee: pendingTx.fee,
firstRound: pendingTx.txParams.firstRound,
lastRound: pendingTx.txParams.lastRound,
genesisId: pendingTx.txParams.genesisId,
genesisHash: pendingTx.txParams.genesisHash
}, pendingTx.privateKey);
// Build and sign the transaction using Stellar SDK
const signedTx = await stellarAPI.buildAndSignTransaction({
sourceAddress: pendingTx.from,
destinationAddress: pendingTx.to,
amount: pendingTx.amount.toString(),
secretKey: pendingTx.privateKey,
memo: null
});
// Broadcast transaction
const result = await xlmAPI.sendTransaction(signedTxBytes);
lastTxId = result.txId;
// Submit transaction to network
const result = await stellarAPI.submitTransaction(signedTx.xdr);
lastTxId = result.hash;
// Save values before closing modal (closeConfirmModal sets pendingTx to null)
const txAmount = pendingTx.amount;
const txTo = pendingTx.to;
const txFee = pendingTx.feexlm;
const txFee = pendingTx.feeXlm;
// Close confirm modal and show success
closeConfirmModal();
showSuccessModal(result.txId, txAmount, txTo, txFee);
showSuccessModal(result.hash, txAmount, txTo, txFee);
// Clear all form fields
document.getElementById('send-privatekey').value = '';
@ -1705,10 +1762,10 @@
}
function showSuccessModal(txId, amount, to, fee) {
document.getElementById('success-txid').textContent = txId;
document.getElementById('success-amount').textContent = amount.toFixed(6) + ' xlm';
document.getElementById('success-to').textContent = to.substring(0, 12) + '...' + to.substring(50);
document.getElementById('success-fee').textContent = fee.toFixed(6) + ' xlm';
document.getElementById('success-txid').textContent = txId || '-';
document.getElementById('success-amount').textContent = (amount ? amount.toFixed(7) : '0.0000000') + ' XLM';
document.getElementById('success-to').textContent = to ? (to.substring(0, 12) + '...' + to.substring(50)) : '-';
document.getElementById('success-fee').textContent = (fee ? fee.toFixed(7) : '0.0000100') + ' XLM';
document.getElementById('success-modal').style.display = 'flex';
}
@ -1719,7 +1776,7 @@
function viewOnExplorer() {
if (lastTxId) {
window.open(`https://allo.info/tx/${lastTxId}`, '_blank');
window.open(`https://stellar.expert/explorer/public/tx/${lastTxId}`, '_blank');
}
}
@ -1886,11 +1943,11 @@
<i class="fas fa-plus-circle"></i>
<span>Generate</span>
</button>
<button class="nav-btn" data-page="transactions" disabled style="opacity: 0.5; cursor: not-allowed;">
<button class="nav-btn" data-page="transactions">
<i class="fas fa-history"></i>
<span>Transactions</span>
</button>
<button class="nav-btn" data-page="send" disabled style="opacity: 0.5; cursor: not-allowed;">
<button class="nav-btn" data-page="send">
<i class="fas fa-paper-plane"></i>
<span>Send</span>
</button>

496
stellarBlockchainAPI.js Normal file
View File

@ -0,0 +1,496 @@
(function(GLOBAL) {
'use strict';
// Stellar Horizon API endpoints
const HORIZON_URL = 'https://horizon.stellar.org'; // Mainnet
const stellarAPI = {};
let StellarSdk = null;
let server = null;
// Initialize Stellar SDK when available
stellarAPI.init = function() {
if (typeof window !== 'undefined') {
const sdkCandidate = window.StellarSdk || window['stellar-sdk'] || window.StellarBase;
if (sdkCandidate) {
let ServerClass = null;
if (sdkCandidate.Server) {
ServerClass = sdkCandidate.Server;
StellarSdk = sdkCandidate;
} else if (sdkCandidate.Horizon && sdkCandidate.Horizon.Server) {
ServerClass = sdkCandidate.Horizon.Server;
StellarSdk = sdkCandidate; // Store the full SDK object
}
if (ServerClass) {
try {
server = new ServerClass(HORIZON_URL);
return true;
} catch (error) {
console.error('❌ Error creating Server instance:', error);
return false;
}
} else {
console.error('❌ Server class not found in StellarSdk');
return false;
}
} else {
console.error('❌ StellarSdk not found on window');
return false;
}
}
console.warn('⚠️ Window object not available');
return false;
};
stellarAPI.forceInit = function() {
return stellarAPI.init();
};
// Get account balance and info
stellarAPI.getBalance = async function(address) {
try {
const response = await fetch(`${HORIZON_URL}/accounts/${address}`);
if (!response.ok) {
if (response.status === 404) {
throw new Error('Account not found. The account may not be funded yet.');
}
throw new Error(`Failed to fetch balance: ${response.status}`);
}
const data = await response.json();
// Find native XLM balance
const nativeBalance = data.balances.find(b => b.asset_type === 'native');
return {
address: data.account_id,
balance: nativeBalance ? parseFloat(nativeBalance.balance) : 0,
balanceXlm: nativeBalance ? parseFloat(nativeBalance.balance) : 0,
sequence: data.sequence,
subentryCount: data.subentry_count,
numSponsoring: data.num_sponsoring || 0,
numSponsored: data.num_sponsored || 0,
balances: data.balances, // All balances including assets
signers: data.signers,
flags: data.flags,
thresholds: data.thresholds,
lastModifiedLedger: data.last_modified_ledger,
// Minimum balance calculation: (2 + subentry_count) * 0.5 XLM
minBalance: (2 + data.subentry_count) * 0.5
};
} catch (error) {
throw error;
}
};
// Get transaction history with pagination
stellarAPI.getTransactions = async function(address, options = {}) {
const limit = options.limit || 10;
const cursor = options.cursor || options.next || null;
const order = options.order || 'desc'; // desc = newest first
let url = `${HORIZON_URL}/accounts/${address}/transactions?limit=${limit}&order=${order}`;
if (cursor) {
url += `&cursor=${cursor}`;
}
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch transactions: ${response.status}`);
}
const data = await response.json();
// Format transactions
const transactions = await Promise.all((data._embedded.records || []).map(async tx => {
// Get operations for this transaction to determine type and details
const opsUrl = `${HORIZON_URL}/transactions/${tx.hash}/operations`;
let operations = [];
try {
const opsResponse = await fetch(opsUrl);
if (opsResponse.ok) {
const opsData = await opsResponse.json();
operations = opsData._embedded.records || [];
}
} catch (error) {
console.warn('Failed to fetch operations for transaction:', tx.hash, error);
}
// Find payment operations
const paymentOp = operations.find(op =>
op.type === 'payment' || op.type === 'create_account'
);
let type = 'other';
let amount = 0;
let amountXlm = 0;
let receiver = null;
let sender = tx.source_account;
if (paymentOp) {
if (paymentOp.type === 'payment') {
type = paymentOp.from === address ? 'sent' : 'received';
amount = parseFloat(paymentOp.amount || 0);
amountXlm = amount;
receiver = paymentOp.to;
sender = paymentOp.from;
} else if (paymentOp.type === 'create_account') {
type = paymentOp.funder === address ? 'sent' : 'received';
amount = parseFloat(paymentOp.starting_balance || 0);
amountXlm = amount;
receiver = paymentOp.account;
sender = paymentOp.funder;
}
}
// Parse timestamp
const timestamp = new Date(tx.created_at).getTime() / 1000;
return {
id: tx.id,
hash: tx.hash,
ledger: tx.ledger,
createdAt: tx.created_at,
sourceAccount: tx.source_account,
fee: parseInt(tx.fee_charged || tx.max_fee),
feeXlm: parseInt(tx.fee_charged || tx.max_fee) / 10000000,
operationCount: tx.operation_count,
successful: tx.successful,
// Payment details
type: type,
sender: sender,
receiver: receiver,
amount: amount,
amountXlm: amountXlm,
memo: tx.memo || null,
memoType: tx.memo_type || null,
// Compatibility fields
roundTime: timestamp,
confirmedRound: tx.ledger
};
}));
return {
transactions,
nextToken: data._embedded.records.length > 0
? data._embedded.records[data._embedded.records.length - 1].paging_token
: null,
hasMore: data._embedded.records.length === limit,
cursor: data._embedded.records.length > 0
? data._embedded.records[data._embedded.records.length - 1].paging_token
: null
};
} catch (error) {
throw error;
}
};
// Get transaction parameters (needed for sending)
stellarAPI.getTransactionParams = async function(sourceAddress) {
try {
// Get latest ledger info for fee stats
const response = await fetch(`${HORIZON_URL}/fee_stats`);
const feeStats = await response.json();
// Base fee in stroops (0.00001 XLM = 100 stroops)
const baseFee = feeStats.last_ledger_base_fee || '100';
return {
fee: parseInt(baseFee),
baseFee: baseFee,
networkPassphrase: StellarSdk ? StellarSdk.Networks.PUBLIC : 'Public Global Stellar Network ; September 2015',
genesisId: 'stellar-mainnet',
genesisHash: 'stellar-mainnet'
};
} catch (error) {
// Fallback to default fee
return {
fee: 100,
baseFee: '100',
networkPassphrase: StellarSdk ? StellarSdk.Networks.PUBLIC : 'Public Global Stellar Network ; September 2015',
genesisId: 'stellar-mainnet',
genesisHash: 'stellar-mainnet'
};
}
};
// Build and sign transaction using Stellar SDK
stellarAPI.buildAndSignTransaction = async function(params) {
const { sourceAddress, destinationAddress, amount, secretKey, memo } = params;
if (!StellarSdk || !server) {
throw new Error('Stellar SDK not initialized. Please refresh the page.');
}
try {
// Load source account
const sourceAccount = await server.loadAccount(sourceAddress);
// Check if destination account exists
let destinationExists = true;
try {
await server.loadAccount(destinationAddress);
} catch (error) {
if (error.response && error.response.status === 404) {
destinationExists = false;
} else {
throw error;
}
}
// Get fee stats
const feeStats = await server.feeStats();
const fee = feeStats.max_fee.mode || (StellarSdk.BASE_FEE || '100');
// Build transaction
let transaction = new StellarSdk.TransactionBuilder(sourceAccount, {
fee: fee,
networkPassphrase: StellarSdk.Networks.PUBLIC
});
// Add operation based on whether destination exists
if (destinationExists) {
// Payment operation
transaction = transaction.addOperation(
StellarSdk.Operation.payment({
destination: destinationAddress,
asset: StellarSdk.Asset.native(),
amount: amount.toString()
})
);
} else {
// Create account operation (requires minimum 1 XLM)
if (parseFloat(amount) < 1) {
throw new Error('Creating a new account requires a minimum of 1 XLM');
}
transaction = transaction.addOperation(
StellarSdk.Operation.createAccount({
destination: destinationAddress,
startingBalance: amount.toString()
})
);
}
// Add memo if provided
if (memo) {
transaction = transaction.addMemo(StellarSdk.Memo.text(memo));
}
// Set timeout and build
transaction = transaction.setTimeout(30).build();
// Sign transaction
const keypair = StellarSdk.Keypair.fromSecret(secretKey);
transaction.sign(keypair);
return {
transaction: transaction,
xdr: transaction.toEnvelope().toXDR('base64'),
hash: transaction.hash().toString('hex'),
destinationExists: destinationExists,
fee: parseInt(fee)
};
} catch (error) {
console.error('Error building transaction:', error);
throw error;
}
};
// Submit signed transaction
stellarAPI.submitTransaction = async function(transactionXDR) {
if (!StellarSdk || !server) {
throw new Error('Stellar SDK not initialized. Please refresh the page.');
}
try {
// Parse the XDR back to a transaction
const transaction = new StellarSdk.Transaction(transactionXDR, StellarSdk.Networks.PUBLIC);
// Submit to network
const result = await server.submitTransaction(transaction);
return {
hash: result.hash,
ledger: result.ledger,
successful: result.successful,
txId: result.hash
};
} catch (error) {
console.error('Error submitting transaction:', error);
// Parse Stellar error
if (error.response && error.response.data) {
const errorData = error.response.data;
let errorMsg = errorData.title || 'Transaction failed';
if (errorData.extras && errorData.extras.result_codes) {
const codes = errorData.extras.result_codes;
errorMsg += ': ' + (codes.transaction || codes.operations?.join(', ') || 'Unknown error');
}
throw new Error(errorMsg);
}
throw error;
}
};
// Get single transaction by hash
stellarAPI.getTransaction = async function(txHash) {
try {
const response = await fetch(`${HORIZON_URL}/transactions/${txHash}`);
if (!response.ok) {
throw new Error(`Transaction not found: ${response.status}`);
}
const tx = await response.json();
// Get operations for this transaction
const opsUrl = `${HORIZON_URL}/transactions/${tx.hash}/operations`;
let operations = [];
try {
const opsResponse = await fetch(opsUrl);
if (opsResponse.ok) {
const opsData = await opsResponse.json();
operations = opsData._embedded.records || [];
}
} catch (error) {
console.warn('Failed to fetch operations for transaction:', tx.hash, error);
}
// Find payment operations
const paymentOp = operations.find(op =>
op.type === 'payment' || op.type === 'create_account'
);
let type = 'other';
let amount = 0;
let amountXlm = 0;
let receiver = null;
let sender = tx.source_account;
if (paymentOp) {
if (paymentOp.type === 'payment') {
type = 'payment';
amount = parseFloat(paymentOp.amount || 0);
amountXlm = amount;
receiver = paymentOp.to;
sender = paymentOp.from;
} else if (paymentOp.type === 'create_account') {
type = 'create_account';
amount = parseFloat(paymentOp.starting_balance || 0);
amountXlm = amount;
receiver = paymentOp.account;
sender = paymentOp.funder;
}
}
// Parse timestamp
const timestamp = new Date(tx.created_at).getTime() / 1000;
return {
id: tx.id,
hash: tx.hash,
ledger: tx.ledger,
createdAt: tx.created_at,
sourceAccount: tx.source_account,
fee: parseInt(tx.fee_charged || tx.max_fee),
feeXlm: parseInt(tx.fee_charged || tx.max_fee) / 10000000,
operationCount: tx.operation_count,
successful: tx.successful,
// Payment details
type: type,
sender: sender,
receiver: receiver,
amount: amount,
amountXlm: amountXlm,
memo: tx.memo || null,
memoType: tx.memo_type || null,
operations: operations,
// Compatibility fields
roundTime: timestamp,
confirmedRound: tx.ledger
};
} catch (error) {
throw error;
}
};
// Format XLM amount for display
stellarAPI.formatXLM = function(amount) {
return parseFloat(amount).toFixed(7);
};
// Parse XLM to stroops (1 XLM = 10,000,000 stroops)
stellarAPI.parseXLM = function(xlm) {
return Math.floor(parseFloat(xlm) * 10000000);
};
// Validate Stellar address
stellarAPI.isValidAddress = function(address) {
// Stellar addresses start with 'G' and are 56 characters long
if (!address || typeof address !== 'string') return false;
if (address.length !== 56) return false;
if (!address.startsWith('G')) return false;
// Check if it's valid Base32
const BASE32_REGEX = /^[A-Z2-7]+$/;
return BASE32_REGEX.test(address);
};
// Validate Stellar secret key
stellarAPI.isValidSecret = function(secret) {
// Stellar secret keys start with 'S' and are 56 characters long
if (!secret || typeof secret !== 'string') return false;
if (secret.length !== 56) return false;
if (!secret.startsWith('S')) return false;
// Check if it's valid Base32
const BASE32_REGEX = /^[A-Z2-7]+$/;
return BASE32_REGEX.test(secret);
};
// Check initialization status
stellarAPI.isInitialized = function() {
return StellarSdk !== null && server !== null;
};
GLOBAL.stellarAPI = stellarAPI;
GLOBAL.xlmAPI = stellarAPI; // Alias for compatibility
// Auto-initialize when SDK is available with retry logic
if (typeof window !== 'undefined') {
let initAttempts = 0;
const maxAttempts = 5;
function tryInit() {
initAttempts++;
const success = stellarAPI.init();
if (success) {
} else if (initAttempts < maxAttempts) {
const delay = initAttempts * 200;
setTimeout(tryInit, delay);
} else {
console.error('❌ Failed to initialize Stellar SDK after', maxAttempts, 'attempts');
}
}
window.addEventListener('load', function() {
setTimeout(tryInit, 100);
});
}
})(typeof window !== 'undefined' ? window : global);

120
stellarSearchDB.js Normal file
View File

@ -0,0 +1,120 @@
class SearchedAddressDB {
constructor() {
this.dbName = "StellarWalletDB";
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("xlmAddress", "xlmAddress", { unique: false });
}
};
});
}
async saveSearchedAddress(
xlmAddress,
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("xlmAddress");
// Check if address already exists
const getRequest = index.getAll(xlmAddress);
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(7)} XLM`,
};
const putRequest = store.put(updatedData);
putRequest.onsuccess = () => resolve();
putRequest.onerror = () => reject(putRequest.error);
} else {
// Address doesn't exist, create new record
const data = {
xlmAddress,
btcAddress: sourceInfo?.btcAddress || null,
floAddress: sourceInfo?.floAddress || null,
balance,
timestamp,
formattedBalance: `${balance.toFixed(7)} XLM`,
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);
});
}
}

111
style.css
View File

@ -3269,4 +3269,113 @@ body:has(.header) .container {
.filter-btn .filter-text {
display: none;
}
}
}
/* Multi-Chain Address Display */
.chain-address-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
border-bottom: 1px solid var(--border);
}
.chain-address-row:last-child {
border-bottom: none;
}
.chain-icon-wrapper {
display: flex;
align-items: center;
gap: 0.5rem;
min-width: 80px;
}
.chain-icon-wrapper .chain-label {
font-weight: 600;
color: var(--text-primary);
font-size: 0.9rem;
}
.chain-address-value {
display: flex;
align-items: center;
gap: 0.5rem;
flex: 1;
justify-content: flex-end;
}
.chain-address-value code {
background: var(--bg-dark);
padding: 0.4rem 0.6rem;
border-radius: 0.35rem;
font-family: 'Courier New', monospace;
font-size: 0.8rem;
color: var(--text-primary);
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Chain Buttons in Recent Searches */
.recent-chain-buttons {
display: flex;
gap: 0.5rem;
margin-bottom: 0.75rem;
justify-content: center;
}
.chain-btn {
width: 44px;
height: 44px;
padding: 0;
background: rgba(51, 65, 85, 0.6);
border: 1px solid rgba(71, 85, 105, 0.4);
border-radius: 0.5rem;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
}
.chain-btn:hover {
background: rgba(71, 85, 105, 0.8);
border-color: rgba(99, 102, 241, 0.3);
color: var(--text-primary);
}
.chain-btn.active {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
border-color: #6366f1;
color: white;
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.4);
}
.chain-btn .chain-icon-svg {
width: 18px;
height: 18px;
}
/* Responsive for chain address rows */
@media (max-width: 768px) {
.chain-address-row {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
padding: 1rem 0;
}
.chain-address-value {
width: 100%;
justify-content: space-between;
}
.chain-address-value code {
max-width: calc(100% - 50px);
font-size: 0.75rem;
}
}