feat: Integrate Stellar blockchain API, enable transaction and send features, and improve Stellar address/key handling.
This commit is contained in:
parent
7f6e1b02a1
commit
11f16d5ce9
227
index.html
227
index.html
@ -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
496
stellarBlockchainAPI.js
Normal 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
120
stellarSearchDB.js
Normal 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
111
style.css
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user