mirror of
https://github.com/basicswap/basicswap.git
synced 2025-11-05 10:28:10 +01:00
2668 lines
117 KiB
JavaScript
2668 lines
117 KiB
JavaScript
const AmmTablesManager = (function() {
|
|
const config = {
|
|
refreshInterval: 30000,
|
|
debug: false
|
|
};
|
|
|
|
let refreshTimer = null;
|
|
let stateData = null;
|
|
let coinData = {};
|
|
|
|
const offersTab = document.getElementById('offers-tab');
|
|
const bidsTab = document.getElementById('bids-tab');
|
|
const offersContent = document.getElementById('offers-content');
|
|
const bidsContent = document.getElementById('bids-content');
|
|
const offersCount = document.getElementById('offers-count');
|
|
const bidsCount = document.getElementById('bids-count');
|
|
const offersBody = document.getElementById('amm-offers-body');
|
|
const bidsBody = document.getElementById('amm-bids-body');
|
|
const refreshButton = document.getElementById('refreshAmmTables');
|
|
|
|
function isDebugEnabled() {
|
|
return localStorage.getItem('amm_debug_enabled') === 'true' || config.debug;
|
|
}
|
|
|
|
function debugLog(message, data) {
|
|
|
|
}
|
|
|
|
function initializeTabs() {
|
|
if (offersTab && bidsTab && offersContent && bidsContent) {
|
|
offersTab.addEventListener('click', function() {
|
|
offersContent.classList.remove('hidden');
|
|
offersContent.classList.add('block');
|
|
bidsContent.classList.add('hidden');
|
|
bidsContent.classList.remove('block');
|
|
|
|
offersTab.classList.add('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
|
|
bidsTab.classList.remove('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
|
|
});
|
|
|
|
bidsTab.addEventListener('click', function() {
|
|
offersContent.classList.add('hidden');
|
|
offersContent.classList.remove('block');
|
|
bidsContent.classList.remove('hidden');
|
|
bidsContent.classList.add('block');
|
|
|
|
bidsTab.classList.add('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
|
|
offersTab.classList.remove('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
|
|
});
|
|
|
|
offersTab.click();
|
|
}
|
|
}
|
|
|
|
function getImageFilename(coinSymbol) {
|
|
if (!coinSymbol) return 'Unknown.png';
|
|
|
|
const icon = window.CoinManager.getCoinIcon(coinSymbol);
|
|
debugLog(`CoinManager returned icon: ${icon} for ${coinSymbol}`);
|
|
return icon || 'Unknown.png';
|
|
}
|
|
|
|
function getCoinDisplayName(coinId) {
|
|
if (window.CoinManager && window.CoinManager.getDisplayName) {
|
|
return window.CoinManager.getDisplayName(coinId) || coinId;
|
|
}
|
|
return coinId;
|
|
}
|
|
|
|
function createSwapColumn(coinFrom, coinTo) {
|
|
const fromImage = getImageFilename(coinFrom);
|
|
const toImage = getImageFilename(coinTo);
|
|
const fromDisplayName = getCoinDisplayName(coinFrom);
|
|
const toDisplayName = getCoinDisplayName(coinTo);
|
|
|
|
return `
|
|
<td class="py-0 px-0 text-right text-sm">
|
|
<div class="flex items-center justify-center monospace">
|
|
<span class="inline-flex mr-3 ml-3 align-middle items-center justify-center w-18 h-20 rounded">
|
|
<img class="h-12" src="/static/images/coins/${toImage}" alt="${toDisplayName}">
|
|
</span>
|
|
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
|
<path fill-rule="evenodd" d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
<span class="inline-flex mr-3 ml-3 align-middle items-center justify-center w-18 h-20 rounded">
|
|
<img class="h-12" src="/static/images/coins/${fromImage}" alt="${fromDisplayName}">
|
|
</span>
|
|
</div>
|
|
</td>
|
|
`;
|
|
}
|
|
|
|
function createActiveCount(templateName, activeItems) {
|
|
const count = activeItems && activeItems[templateName] ? activeItems[templateName].length : 0;
|
|
|
|
return `
|
|
<td class="py-3 px-4 text-center">
|
|
<span class="inline-flex items-center px-3 py-1 text-xs font-medium rounded-full
|
|
${count > 0
|
|
? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300'
|
|
: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'}">
|
|
${count}
|
|
</span>
|
|
</td>
|
|
`;
|
|
}
|
|
|
|
function renderOffersTable(stateData) {
|
|
if (!offersBody) return;
|
|
|
|
debugLog('Rendering offers table with data:', stateData);
|
|
|
|
let offers = [];
|
|
if (stateData && stateData.config) {
|
|
if (Array.isArray(stateData.config.offers)) {
|
|
offers = stateData.config.offers;
|
|
} else if (typeof stateData.config.offers === 'object') {
|
|
offers = [stateData.config.offers];
|
|
}
|
|
}
|
|
|
|
const activeOffers = stateData && stateData.state && stateData.state.offers ? stateData.state.offers : {};
|
|
|
|
if (offers.length === 0) {
|
|
offersBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="7" class="py-8 px-4 text-center text-gray-500 dark:text-gray-400">
|
|
<div class="flex flex-col items-center justify-center">
|
|
<svg class="w-12 h-12 mb-4 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
|
</svg>
|
|
<p class="text-lg font-medium">No offers configured</p>
|
|
<p class="text-sm text-gray-400 dark:text-gray-500 mt-1">Edit the AMM configuration to add offers</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
offersCount.textContent = '(0)';
|
|
return;
|
|
}
|
|
|
|
offersCount.textContent = `(${offers.length})`;
|
|
|
|
let tableHtml = '';
|
|
|
|
offers.forEach(offer => {
|
|
const name = offer.name || 'Unnamed Offer';
|
|
const coinFrom = offer.coin_from || '';
|
|
const coinTo = offer.coin_to || '';
|
|
const amount = parseFloat(offer.amount || 0);
|
|
const minrate = parseFloat(offer.minrate || 0);
|
|
const enabled = offer.enabled !== undefined ? offer.enabled : false;
|
|
const amountVariable = offer.amount_variable !== undefined ? offer.amount_variable : false;
|
|
const minCoinFromAmt = parseFloat(offer.min_coin_from_amt || 0);
|
|
const offerValidSeconds = parseInt(offer.offer_valid_seconds || 3600);
|
|
const rateTweakPercent = parseFloat(offer.ratetweakpercent || 0);
|
|
const adjustRatesValue = offer.adjust_rates_based_on_market || 'false';
|
|
const adjustRates = adjustRatesValue !== 'false';
|
|
const amountStep = offer.amount_step || 'N/A';
|
|
|
|
const amountToReceive = amount * minrate;
|
|
|
|
const activeOffersCount = activeOffers[name] && Array.isArray(activeOffers[name]) ?
|
|
activeOffers[name].length : 0;
|
|
|
|
tableHtml += `
|
|
<tr class="relative opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
|
<td class="py-3 pl-4 text-center">
|
|
<div class="font-medium">${name}</div>
|
|
</td>
|
|
${createSwapColumn(coinFrom, coinTo)}
|
|
<td class="py-3 px-4 text-right">
|
|
<div class="text-sm font-semibold dark:text-white">${coinFrom == "Bitcoin" ? amount.toFixed(8) : amount.toFixed(4)}</div>
|
|
<div class="text-sm text-gray-500 dark:text-gray-300">${getCoinDisplayName(coinFrom)}</div>
|
|
<div class="text-xs text-gray-500 dark:text-gray-300 mt-1">
|
|
Min bal: ${coinFrom == "Bitcoin" ? minCoinFromAmt.toFixed(8) : minCoinFromAmt.toFixed(4)}
|
|
</div>
|
|
</td>
|
|
<td class="py-3 px-4 text-right">
|
|
<div class="text-xs font-semibold dark:text-white">${minrate.toFixed(8)}</div>
|
|
<div class="text-xs text-gray-500 dark:text-gray-300">
|
|
Tweak: ${rateTweakPercent > 0 ? '+' : ''}${rateTweakPercent}%
|
|
</div>
|
|
<div class="text-xs text-gray-500 dark:text-gray-300 mt-1">
|
|
Receive: ~${(amountToReceive * (rateTweakPercent / 100 + 1)).toFixed(4)} ${getCoinDisplayName(coinTo)}
|
|
${(() => {
|
|
const usdValue = calculateUSDPrice(amountToReceive, coinTo);
|
|
return usdValue ? `<br/><span class="text-green-600 dark:text-green-400">${formatUSDPrice(usdValue)}</span>` : '<br/><span class="text-gray-400">USD: N/A</span>';
|
|
})()}
|
|
</div>
|
|
</td>
|
|
<td class="py-3 px-4 text-center">
|
|
<div class="space-y-1">
|
|
<div class="flex flex-wrap gap-1 justify-center">
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${amountVariable ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300' : 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'}">
|
|
${amountVariable ? 'Variable' : 'Fixed'}
|
|
</span>
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300">
|
|
<svg class="w-3 h-3 mr-1 text-gray-600 dark:text-gray-300" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
${formatDuration(offerValidSeconds)}
|
|
</span>
|
|
</div>
|
|
<div class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full ${adjustRatesValue != 'static' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300' : 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'}">
|
|
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
Rates: ${adjustRatesValue === 'static' ? 'Static'
|
|
: adjustRatesValue === 'only' ? 'Market'
|
|
: adjustRatesValue === 'minrate' ? 'Market (fallback)'
|
|
: adjustRatesValue === 'false' ? 'CoinGecko'
|
|
: adjustRatesValue === 'all' ? 'Auto (all)'
|
|
: adjustRates ? 'Auto (any)'
|
|
: 'Off'}
|
|
</div>
|
|
<div class="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300">
|
|
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
Step: ${amountStep}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="py-3 px-4 text-center">
|
|
<div class="flex flex-col items-center">
|
|
<span class="inline-flex items-center px-3 py-1 text-xs font-medium rounded-full
|
|
${enabled
|
|
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300'
|
|
: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300'}">
|
|
${enabled ? 'Enabled' : 'Disabled'}
|
|
</span>
|
|
<span class="mt-1 inline-flex items-center px-3 py-1 text-xs font-medium rounded-full hidden
|
|
${activeOffersCount > 0
|
|
? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300'
|
|
: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'}">
|
|
${activeOffersCount} Running Offer${activeOffersCount !== 1 ? 's' : ''}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td class="py-3 px-4 text-center">
|
|
<div class="flex items-center justify-center space-x-2">
|
|
<button type="button" class="edit-amm-item text-gray-500 hover:text-gray-700 dark:text-white dark:hover:text-gray-300 focus:ring-0 focus:outline-none"
|
|
data-type="offer" data-id="${offer.id || ''}" data-name="${name}">
|
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"></path>
|
|
</svg>
|
|
</button>
|
|
<button type="button" class="delete-amm-item text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 focus:ring-0 focus:outline-none"
|
|
data-type="offer" data-id="${offer.id || ''}" data-name="${name}">
|
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
|
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
|
|
if (offersBody.innerHTML.trim() !== tableHtml.trim()) {
|
|
offersBody.innerHTML = tableHtml;
|
|
}
|
|
}
|
|
|
|
function renderBidsTable(stateData) {
|
|
if (!bidsBody) return;
|
|
|
|
debugLog('Rendering bids table with data:', stateData);
|
|
|
|
let bids = [];
|
|
if (stateData && stateData.config) {
|
|
if (Array.isArray(stateData.config.bids)) {
|
|
bids = stateData.config.bids;
|
|
} else if (typeof stateData.config.bids === 'object') {
|
|
bids = [stateData.config.bids];
|
|
}
|
|
}
|
|
|
|
const activeBids = stateData && stateData.state && stateData.state.bids ? stateData.state.bids : {};
|
|
|
|
if (bids.length === 0) {
|
|
bidsBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="7" class="py-8 px-4 text-center text-gray-500 dark:text-gray-400">
|
|
<div class="flex flex-col items-center justify-center">
|
|
<svg class="w-12 h-12 mb-4 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
|
</svg>
|
|
<p class="text-lg font-medium">No bids configured</p>
|
|
<p class="text-sm text-gray-400 dark:text-gray-500 mt-1">Edit the AMM configuration to add bids</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
bidsCount.textContent = '(0)';
|
|
return;
|
|
}
|
|
|
|
bidsCount.textContent = `(${bids.length})`;
|
|
|
|
let tableHtml = '';
|
|
|
|
bids.forEach(bid => {
|
|
const name = bid.name || 'Unnamed Bid';
|
|
const coinFrom = bid.coin_from || '';
|
|
const coinTo = bid.coin_to || '';
|
|
const amount = parseFloat(bid.amount || 0);
|
|
const maxRate = parseFloat(bid.max_rate || 0);
|
|
const enabled = bid.enabled !== undefined ? bid.enabled : false;
|
|
const amountVariable = bid.amount_variable !== undefined ? bid.amount_variable : false;
|
|
const minCoinToBalance = parseFloat(bid.min_coin_to_balance || 0);
|
|
const maxConcurrent = parseInt(bid.max_concurrent || 1);
|
|
const amountToSend = amount * maxRate;
|
|
const activeBidsCount = activeBids[name] && Array.isArray(activeBids[name]) ?
|
|
activeBids[name].length : 0;
|
|
const useBalanceBidding = bid.use_balance_bidding !== undefined ? bid.use_balance_bidding : false;
|
|
|
|
tableHtml += `
|
|
<tr class="relative opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
|
|
<td class="py-3 px-4">
|
|
<div class="font-medium">${name}</div>
|
|
</td>
|
|
${createSwapColumn(coinTo, coinFrom)}
|
|
<td class="py-3 px-4 text-right">
|
|
<div class="text-sm font-semibold dark:text-white">${amount.toFixed(8)}</div>
|
|
<div class="text-sm text-gray-500 dark:text-gray-300">${getCoinDisplayName(coinFrom)}</div>
|
|
<div class="text-xs text-gray-500 dark:text-gray-300 mt-1">
|
|
Min ${getCoinDisplayName(coinTo)} Balance: ${minCoinToBalance.toFixed(8)}
|
|
</div>
|
|
</td>
|
|
<td class="py-3 px-4 text-right">
|
|
<div class="text-sm font-semibold dark:text-white">${maxRate.toFixed(8)}</div>
|
|
<div class="text-xs text-gray-500 dark:text-gray-300 mt-1">
|
|
Send: ~${amountToSend.toFixed(8)} ${getCoinDisplayName(coinTo)}
|
|
${(() => {
|
|
const usdValue = calculateUSDPrice(amountToSend, coinTo);
|
|
return usdValue ? `<br/><span class="text-red-600 dark:text-red-400">${formatUSDPrice(usdValue)}</span>` : '<br/><span class="text-gray-400">USD: N/A</span>';
|
|
})()}
|
|
</div>
|
|
</td>
|
|
<td class="py-3 px-4 text-center">
|
|
<div class="space-y-1">
|
|
<div class="flex flex-wrap gap-1 justify-center">
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${amountVariable ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300' : 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'}">
|
|
${amountVariable ? 'Variable' : 'Fixed'}
|
|
</span>
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300">
|
|
Max: ${maxConcurrent}
|
|
</span>
|
|
${useBalanceBidding ? `
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300">
|
|
Balance Bidding
|
|
</span>
|
|
` : ''}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="py-3 px-4 text-center">
|
|
<div class="flex flex-col items-center">
|
|
<span class="inline-flex items-center px-3 py-1 text-xs font-medium rounded-full
|
|
${enabled
|
|
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300'
|
|
: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300'}">
|
|
${enabled ? 'Enabled' : 'Disabled'}
|
|
</span>
|
|
<span class="mt-1 inline-flex items-center px-3 py-1 text-xs font-medium rounded-full hidden
|
|
${activeBidsCount > 0
|
|
? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300'
|
|
: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'}">
|
|
${activeBidsCount} Running Bid${activeBidsCount !== 1 ? 's' : ''}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td class="py-3 px-4 text-center">
|
|
<div class="flex items-center justify-center space-x-2">
|
|
<button type="button" class="edit-amm-item text-gray-500 hover:text-gray-700 dark:text-white dark:hover:text-gray-300 focus:ring-0 focus:outline-none"
|
|
data-type="bid" data-id="${bid.id || ''}" data-name="${name}">
|
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"></path>
|
|
</svg>
|
|
</button>
|
|
<button type="button" class="delete-amm-item text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 focus:ring-0 focus:outline-none"
|
|
data-type="bid" data-id="${bid.id || ''}" data-name="${name}">
|
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
|
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
|
|
if (bidsBody.innerHTML.trim() !== tableHtml.trim()) {
|
|
bidsBody.innerHTML = tableHtml;
|
|
}
|
|
}
|
|
|
|
function formatDuration(seconds) {
|
|
if (seconds < 60) return `${seconds}s`;
|
|
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
|
|
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
|
|
return `${Math.floor(seconds / 86400)}d`;
|
|
}
|
|
|
|
function getPriceKey(coin) {
|
|
if (!coin) return null;
|
|
|
|
const lowerCoin = coin.toLowerCase();
|
|
|
|
const coinToTicker = {
|
|
'particl': 'PART',
|
|
'particl anon': 'PART',
|
|
'particl blind': 'PART',
|
|
'part': 'PART',
|
|
'part anon': 'PART',
|
|
'part blind': 'PART',
|
|
'bitcoin': 'BTC',
|
|
'btc': 'BTC',
|
|
'bitcoin cash': 'BCH',
|
|
'bitcoincash': 'BCH',
|
|
'bch': 'BCH',
|
|
'decred': 'DCR',
|
|
'dcr': 'DCR',
|
|
'dogecoin': 'DOGE',
|
|
'doge': 'DOGE',
|
|
'monero': 'XMR',
|
|
'xmr': 'XMR',
|
|
'litecoin': 'LTC',
|
|
'ltc': 'LTC',
|
|
'namecoin': 'NMC',
|
|
'nmc': 'NMC',
|
|
'wownero': 'WOW',
|
|
'wow': 'WOW',
|
|
'dash': 'DASH',
|
|
'pivx': 'PIVX',
|
|
'firo': 'FIRO',
|
|
'xzc': 'FIRO',
|
|
'zcoin': 'FIRO',
|
|
'BTC': 'BTC',
|
|
'BCH': 'BCH',
|
|
'DCR': 'DCR',
|
|
'DOGE': 'DOGE',
|
|
'LTC': 'LTC',
|
|
'NMC': 'NMC',
|
|
'XMR': 'XMR',
|
|
'PART': 'PART',
|
|
'WOW': 'WOW',
|
|
'FIRO': 'FIRO',
|
|
'DASH': 'DASH',
|
|
'PIVX': 'PIVX'
|
|
};
|
|
|
|
if (coinToTicker[lowerCoin]) {
|
|
return coinToTicker[lowerCoin];
|
|
}
|
|
|
|
if (coinToTicker[coin.toUpperCase()]) {
|
|
return coinToTicker[coin.toUpperCase()];
|
|
}
|
|
|
|
for (const [key, value] of Object.entries(coinToTicker)) {
|
|
if (lowerCoin.includes(key.toLowerCase())) {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
if (lowerCoin.includes('particl') || lowerCoin.includes('part')) {
|
|
return 'PART';
|
|
}
|
|
|
|
return coin.toUpperCase();
|
|
}
|
|
|
|
function calculateUSDPrice(amount, coinName) {
|
|
if (!window.latestPrices || !coinName || !amount) {
|
|
return null;
|
|
}
|
|
|
|
const ticker = getPriceKey(coinName);
|
|
let coinPrice = null;
|
|
|
|
if (typeof window.latestPrices[ticker] === 'number') {
|
|
coinPrice = window.latestPrices[ticker];
|
|
}
|
|
|
|
else if (typeof window.latestPrices[coinName] === 'number') {
|
|
coinPrice = window.latestPrices[coinName];
|
|
}
|
|
|
|
else if (typeof window.latestPrices[coinName.toUpperCase()] === 'number') {
|
|
coinPrice = window.latestPrices[coinName.toUpperCase()];
|
|
}
|
|
|
|
if (!coinPrice || isNaN(coinPrice)) {
|
|
return null;
|
|
}
|
|
|
|
return amount * coinPrice;
|
|
}
|
|
|
|
function formatUSDPrice(usdValue) {
|
|
if (!usdValue || isNaN(usdValue)) return '';
|
|
if (window.config && window.config.utils && window.config.utils.formatPrice) {
|
|
return `($${window.config.utils.formatPrice('USD', usdValue)} USD)`;
|
|
}
|
|
return `($${usdValue.toFixed(2)} USD)`;
|
|
}
|
|
|
|
async function fetchLatestPrices() {
|
|
try {
|
|
const coins = 'BTC,BCH,DCR,DOGE,LTC,NMC,XMR,PART,WOW,FIRO,DASH,PIVX';
|
|
|
|
const response = await fetch('/json/coinprices', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: `coins=${encodeURIComponent(coins)}¤cy_to=USD&source=coingecko.com&match_input_key=true`
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const data = await response.json();
|
|
|
|
if (data && data.rates) {
|
|
return data.rates;
|
|
}
|
|
return data;
|
|
} catch (error) {
|
|
console.error('Error fetching prices:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function initializePrices() {
|
|
|
|
if (window.priceManager && typeof window.priceManager.getLatestPrices === 'function') {
|
|
const prices = window.priceManager.getLatestPrices();
|
|
if (prices && Object.keys(prices).length > 0) {
|
|
window.latestPrices = prices;
|
|
setTimeout(() => {
|
|
updateTables();
|
|
}, 100);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const prices = await fetchLatestPrices();
|
|
if (prices) {
|
|
window.latestPrices = prices;
|
|
setTimeout(() => {
|
|
updateTables();
|
|
}, 100);
|
|
}
|
|
}
|
|
|
|
function getInitialData() {
|
|
if (window.ammTablesConfig) {
|
|
const stateData = window.ammTablesConfig.stateData || {};
|
|
let configData = window.ammTablesConfig.configData || {};
|
|
|
|
if (!configData || Object.keys(configData).length === 0) {
|
|
try {
|
|
if (window.ammTablesConfig.configContent) {
|
|
if (typeof window.ammTablesConfig.configContent === 'string') {
|
|
configData = JSON.parse(window.ammTablesConfig.configContent);
|
|
} else if (typeof window.ammTablesConfig.configContent === 'object') {
|
|
configData = window.ammTablesConfig.configContent;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
debugLog('Error parsing config content:', error);
|
|
}
|
|
}
|
|
|
|
debugLog('Initial state data:', stateData);
|
|
debugLog('Initial config data:', configData);
|
|
|
|
return {
|
|
state: stateData,
|
|
config: configData
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function parseStateData() {
|
|
const stateContent = document.querySelector('.font-mono.bg-gray-50.overflow-y-auto');
|
|
if (!stateContent) return null;
|
|
|
|
try {
|
|
const stateText = stateContent.textContent.trim();
|
|
if (!stateText) return null;
|
|
|
|
const parsedState = JSON.parse(stateText);
|
|
return { state: parsedState };
|
|
} catch (error) {
|
|
debugLog('Error parsing state data:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function parseConfigData() {
|
|
const configTextarea = document.querySelector('textarea[name="config_content"]');
|
|
if (!configTextarea) return null;
|
|
|
|
try {
|
|
const configText = configTextarea.value.trim();
|
|
if (!configText) return null;
|
|
|
|
const parsedConfig = JSON.parse(configText);
|
|
return { config: parsedConfig };
|
|
} catch (error) {
|
|
debugLog('Error parsing config data:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getCombinedData() {
|
|
const initialData = getInitialData();
|
|
if (initialData) {
|
|
return initialData;
|
|
}
|
|
|
|
const stateData = parseStateData();
|
|
const configData = parseConfigData();
|
|
|
|
return {
|
|
...stateData,
|
|
...configData
|
|
};
|
|
}
|
|
function updateTables() {
|
|
const data = getCombinedData();
|
|
if (!data) {
|
|
debugLog('No data available for tables');
|
|
return;
|
|
}
|
|
|
|
stateData = data;
|
|
debugLog('Updated state data:', stateData);
|
|
|
|
renderOffersTable(stateData);
|
|
renderBidsTable(stateData);
|
|
}
|
|
|
|
function startRefreshTimer() {
|
|
if (refreshTimer) {
|
|
clearInterval(refreshTimer);
|
|
}
|
|
|
|
refreshTimer = setInterval(function() {
|
|
updateTables();
|
|
}, config.refreshInterval);
|
|
|
|
return refreshTimer;
|
|
}
|
|
|
|
function stopRefreshTimer() {
|
|
if (refreshTimer) {
|
|
clearInterval(refreshTimer);
|
|
refreshTimer = null;
|
|
}
|
|
}
|
|
|
|
function setupConfigFormListener() {
|
|
const configForm = document.querySelector('form[method="post"]');
|
|
if (configForm) {
|
|
configForm.addEventListener('submit', function() {
|
|
localStorage.setItem('amm_update_tables', 'true');
|
|
});
|
|
|
|
if (localStorage.getItem('amm_update_tables') === 'true') {
|
|
localStorage.removeItem('amm_update_tables');
|
|
setTimeout(updateTables, 500);
|
|
}
|
|
}
|
|
}
|
|
|
|
function shouldDropdownOptionsShowBalance(select) {
|
|
const isMakerDropdown = select.id.includes('coin-from');
|
|
const isTakerDropdown = select.id.includes('coin-to');
|
|
|
|
const addModal = document.getElementById('add-amm-modal');
|
|
const editModal = document.getElementById('edit-amm-modal');
|
|
const addModalVisible = addModal && !addModal.classList.contains('hidden');
|
|
const editModalVisible = editModal && !editModal.classList.contains('hidden');
|
|
|
|
let isBidModal = false;
|
|
if (addModalVisible) {
|
|
const dataType = addModal.getAttribute('data-amm-type');
|
|
if (dataType) {
|
|
isBidModal = dataType === 'bid';
|
|
} else {
|
|
|
|
const modalTitle = document.getElementById('add-modal-title');
|
|
isBidModal = modalTitle && modalTitle.textContent && modalTitle.textContent.includes('Bid');
|
|
}
|
|
} else if (editModalVisible) {
|
|
const dataType = editModal.getAttribute('data-amm-type');
|
|
if (dataType) {
|
|
isBidModal = dataType === 'bid';
|
|
} else {
|
|
|
|
const modalTitle = document.getElementById('edit-modal-title');
|
|
isBidModal = modalTitle && modalTitle.textContent && modalTitle.textContent.includes('Bid');
|
|
}
|
|
}
|
|
|
|
const result = isBidModal ? isTakerDropdown : isMakerDropdown;
|
|
|
|
console.log(`[DEBUG] shouldDropdownOptionsShowBalance: ${select.id}, isBidModal=${isBidModal}, isMaker=${isMakerDropdown}, isTaker=${isTakerDropdown}, result=${result}`);
|
|
|
|
return result;
|
|
}
|
|
|
|
function refreshDropdownOptions() {
|
|
const dropdownIds = ['add-amm-coin-from', 'add-amm-coin-to', 'edit-amm-coin-from', 'edit-amm-coin-to'];
|
|
|
|
dropdownIds.forEach(dropdownId => {
|
|
const select = document.getElementById(dropdownId);
|
|
if (!select || select.style.display !== 'none') return;
|
|
|
|
const wrapper = select.parentNode.querySelector('.relative');
|
|
if (!wrapper) return;
|
|
|
|
const dropdown = wrapper.querySelector('[role="listbox"]');
|
|
if (!dropdown) return;
|
|
|
|
const options = dropdown.querySelectorAll('[data-value]');
|
|
options.forEach(optionElement => {
|
|
const coinValue = optionElement.getAttribute('data-value');
|
|
const originalOption = Array.from(select.options).find(opt => opt.value === coinValue);
|
|
if (!originalOption) return;
|
|
|
|
const textContainer = optionElement.querySelector('div.flex.flex-col, div.flex.items-center');
|
|
if (!textContainer) return;
|
|
|
|
textContainer.innerHTML = '';
|
|
|
|
const shouldShowBalance = shouldDropdownOptionsShowBalance(select);
|
|
const fullText = originalOption.textContent.trim();
|
|
const balance = originalOption.getAttribute('data-balance') || '0.00000000';
|
|
|
|
console.log(`[DEBUG] refreshDropdownOptions: ${select.id}, option=${coinValue}, shouldShowBalance=${shouldShowBalance}, balance=${balance}`);
|
|
|
|
if (shouldShowBalance) {
|
|
|
|
textContainer.className = 'flex flex-col';
|
|
|
|
const coinName = fullText.includes(' - Balance: ') ? fullText.split(' - Balance: ')[0] : fullText;
|
|
|
|
const coinNameSpan = document.createElement('span');
|
|
coinNameSpan.textContent = coinName;
|
|
coinNameSpan.className = 'text-gray-900 dark:text-white';
|
|
|
|
const balanceSpan = document.createElement('span');
|
|
balanceSpan.textContent = `Balance: ${balance}`;
|
|
balanceSpan.className = 'text-gray-500 dark:text-gray-400 text-xs';
|
|
|
|
textContainer.appendChild(coinNameSpan);
|
|
textContainer.appendChild(balanceSpan);
|
|
} else {
|
|
|
|
textContainer.className = 'flex items-center';
|
|
|
|
const coinNameSpan = document.createElement('span');
|
|
const coinName = fullText.includes(' - Balance: ') ? fullText.split(' - Balance: ')[0] : fullText;
|
|
coinNameSpan.textContent = coinName;
|
|
coinNameSpan.className = 'text-gray-900 dark:text-white';
|
|
|
|
textContainer.appendChild(coinNameSpan);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function refreshDropdownBalances() {
|
|
const dropdownIds = ['add-amm-coin-from', 'add-amm-coin-to', 'edit-amm-coin-from', 'edit-amm-coin-to'];
|
|
|
|
dropdownIds.forEach(dropdownId => {
|
|
const select = document.getElementById(dropdownId);
|
|
if (!select || select.style.display !== 'none') return;
|
|
|
|
const wrapper = select.parentNode.querySelector('.relative');
|
|
if (!wrapper) return;
|
|
|
|
const dropdownItems = wrapper.querySelectorAll('[data-value]');
|
|
dropdownItems.forEach(item => {
|
|
const value = item.getAttribute('data-value');
|
|
const option = select.querySelector(`option[value="${value}"]`);
|
|
if (option) {
|
|
const balance = option.getAttribute('data-balance') || '0.00000000';
|
|
const pendingBalance = option.getAttribute('data-pending-balance') || '';
|
|
|
|
const balanceDiv = item.querySelector('.text-xs');
|
|
if (balanceDiv) {
|
|
balanceDiv.textContent = `Balance: ${balance}`;
|
|
|
|
let pendingDiv = item.querySelector('.text-green-500');
|
|
if (pendingBalance && parseFloat(pendingBalance) > 0) {
|
|
if (!pendingDiv) {
|
|
|
|
pendingDiv = document.createElement('div');
|
|
pendingDiv.className = 'text-green-500 text-xs';
|
|
balanceDiv.parentNode.appendChild(pendingDiv);
|
|
}
|
|
pendingDiv.textContent = `+${pendingBalance} pending`;
|
|
} else if (pendingDiv) {
|
|
|
|
pendingDiv.remove();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
const selectedOption = select.options[select.selectedIndex];
|
|
if (selectedOption) {
|
|
const textContainer = wrapper.querySelector('button .flex-grow');
|
|
const balanceDiv = textContainer ? textContainer.querySelector('.text-xs') : null;
|
|
if (balanceDiv) {
|
|
const balance = selectedOption.getAttribute('data-balance') || '0.00000000';
|
|
const pendingBalance = selectedOption.getAttribute('data-pending-balance') || '';
|
|
|
|
balanceDiv.textContent = `Balance: ${balance}`;
|
|
|
|
let pendingDiv = textContainer.querySelector('.text-green-500');
|
|
if (pendingBalance && parseFloat(pendingBalance) > 0) {
|
|
if (!pendingDiv) {
|
|
|
|
pendingDiv = document.createElement('div');
|
|
pendingDiv.className = 'text-green-500 text-xs';
|
|
textContainer.appendChild(pendingDiv);
|
|
}
|
|
pendingDiv.textContent = `+${pendingBalance} pending`;
|
|
} else if (pendingDiv) {
|
|
|
|
pendingDiv.remove();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function refreshOfferDropdownBalanceDisplay() {
|
|
refreshDropdownBalances();
|
|
}
|
|
|
|
function refreshBidDropdownBalanceDisplay() {
|
|
refreshDropdownBalances();
|
|
}
|
|
|
|
function refreshDropdownBalanceDisplay(modalType = null) {
|
|
if (modalType === 'offer') {
|
|
refreshOfferDropdownBalanceDisplay();
|
|
} else if (modalType === 'bid') {
|
|
refreshBidDropdownBalanceDisplay();
|
|
} else {
|
|
|
|
const addModal = document.getElementById('add-amm-modal');
|
|
const editModal = document.getElementById('edit-amm-modal');
|
|
const addModalVisible = addModal && !addModal.classList.contains('hidden');
|
|
const editModalVisible = editModal && !editModal.classList.contains('hidden');
|
|
|
|
let detectedType = null;
|
|
if (addModalVisible) {
|
|
detectedType = addModal.getAttribute('data-amm-type');
|
|
} else if (editModalVisible) {
|
|
detectedType = editModal.getAttribute('data-amm-type');
|
|
}
|
|
|
|
if (detectedType === 'offer') {
|
|
refreshOfferDropdownBalanceDisplay();
|
|
} else if (detectedType === 'bid') {
|
|
refreshBidDropdownBalanceDisplay();
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateDropdownsForModalType(modalPrefix) {
|
|
const coinFromSelect = document.getElementById(`${modalPrefix}-amm-coin-from`);
|
|
const coinToSelect = document.getElementById(`${modalPrefix}-amm-coin-to`);
|
|
|
|
if (!coinFromSelect || !coinToSelect) return;
|
|
|
|
const balanceData = {};
|
|
|
|
Array.from(coinFromSelect.options).forEach(option => {
|
|
const balance = option.getAttribute('data-balance');
|
|
if (balance) {
|
|
balanceData[option.value] = balance;
|
|
}
|
|
});
|
|
|
|
Array.from(coinToSelect.options).forEach(option => {
|
|
const balance = option.getAttribute('data-balance');
|
|
if (balance) {
|
|
balanceData[option.value] = balance;
|
|
}
|
|
});
|
|
|
|
updateDropdownOptions(coinFromSelect, balanceData);
|
|
updateDropdownOptions(coinToSelect, balanceData);
|
|
}
|
|
|
|
function updateDropdownOptions(select, balanceData, pendingData = {}) {
|
|
Array.from(select.options).forEach(option => {
|
|
const coinName = option.value;
|
|
const balance = balanceData[coinName] || '0.00000000';
|
|
const pending = pendingData[coinName] || '0.0';
|
|
|
|
option.setAttribute('data-balance', balance);
|
|
option.setAttribute('data-pending-balance', pending);
|
|
|
|
option.textContent = coinName;
|
|
});
|
|
}
|
|
|
|
function createSimpleDropdown(select, showBalance = false) {
|
|
if (!select) return;
|
|
|
|
const existingWrapper = select.parentNode.querySelector('.relative');
|
|
if (existingWrapper) {
|
|
existingWrapper.remove();
|
|
select.style.display = '';
|
|
}
|
|
|
|
select.style.display = 'none';
|
|
|
|
const wrapper = document.createElement('div');
|
|
wrapper.className = 'relative';
|
|
|
|
const button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.className = 'flex items-center justify-between w-full p-2.5 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:text-white';
|
|
button.style.minHeight = '60px';
|
|
|
|
const displayContent = document.createElement('div');
|
|
displayContent.className = 'flex items-center';
|
|
|
|
const icon = document.createElement('img');
|
|
icon.className = 'w-5 h-5 mr-2';
|
|
icon.alt = '';
|
|
|
|
const textContainer = document.createElement('div');
|
|
textContainer.className = 'flex-grow text-left';
|
|
|
|
const arrow = document.createElement('div');
|
|
arrow.innerHTML = `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>`;
|
|
|
|
displayContent.appendChild(icon);
|
|
displayContent.appendChild(textContainer);
|
|
button.appendChild(displayContent);
|
|
button.appendChild(arrow);
|
|
|
|
const dropdown = document.createElement('div');
|
|
dropdown.className = 'absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg hidden dark:bg-gray-700 dark:border-gray-600 max-h-60 overflow-y-auto';
|
|
|
|
Array.from(select.options).forEach(option => {
|
|
const item = document.createElement('div');
|
|
item.className = 'flex items-center p-2 hover:bg-gray-100 dark:hover:bg-gray-600 cursor-pointer';
|
|
item.setAttribute('data-value', option.value);
|
|
|
|
const itemIcon = document.createElement('img');
|
|
itemIcon.className = 'w-5 h-5 mr-2';
|
|
itemIcon.src = `/static/images/coins/${getImageFilename(option.value)}`;
|
|
itemIcon.alt = '';
|
|
|
|
const itemText = document.createElement('div');
|
|
const coinName = option.textContent.trim();
|
|
const balance = option.getAttribute('data-balance') || '0.00000000';
|
|
const pendingBalance = option.getAttribute('data-pending-balance') || '';
|
|
|
|
if (showBalance) {
|
|
itemText.className = 'flex flex-col';
|
|
|
|
let html = `
|
|
<div class="text-gray-900 dark:text-white">${coinName}</div>
|
|
<div class="text-gray-500 dark:text-gray-400 text-xs">Balance: ${balance}</div>
|
|
`;
|
|
|
|
if (pendingBalance && parseFloat(pendingBalance) > 0) {
|
|
html += `<div class="text-green-500 text-xs">+${pendingBalance} pending</div>`;
|
|
}
|
|
|
|
itemText.innerHTML = html;
|
|
} else {
|
|
itemText.className = 'text-gray-900 dark:text-white';
|
|
itemText.textContent = coinName;
|
|
}
|
|
|
|
item.appendChild(itemIcon);
|
|
item.appendChild(itemText);
|
|
|
|
item.addEventListener('click', function() {
|
|
select.value = this.getAttribute('data-value');
|
|
|
|
const selectedOption = select.options[select.selectedIndex];
|
|
const selectedCoinName = selectedOption.textContent.trim();
|
|
const selectedBalance = selectedOption.getAttribute('data-balance') || '0.00000000';
|
|
const selectedPendingBalance = selectedOption.getAttribute('data-pending-balance') || '';
|
|
|
|
icon.src = itemIcon.src;
|
|
|
|
if (showBalance) {
|
|
let html = `
|
|
<div class="text-gray-900 dark:text-white">${selectedCoinName}</div>
|
|
<div class="text-gray-500 dark:text-gray-400 text-xs">Balance: ${selectedBalance}</div>
|
|
`;
|
|
|
|
if (selectedPendingBalance && parseFloat(selectedPendingBalance) > 0) {
|
|
html += `<div class="text-green-500 text-xs">+${selectedPendingBalance} pending</div>`;
|
|
}
|
|
|
|
textContainer.innerHTML = html;
|
|
textContainer.className = 'flex-grow text-left flex flex-col justify-center';
|
|
} else {
|
|
textContainer.textContent = selectedCoinName;
|
|
textContainer.className = 'flex-grow text-left';
|
|
}
|
|
|
|
dropdown.classList.add('hidden');
|
|
|
|
const event = new Event('change', { bubbles: true });
|
|
select.dispatchEvent(event);
|
|
});
|
|
|
|
dropdown.appendChild(item);
|
|
});
|
|
|
|
const selectedOption = select.options[select.selectedIndex];
|
|
if (selectedOption) {
|
|
const selectedCoinName = selectedOption.textContent.trim();
|
|
const selectedBalance = selectedOption.getAttribute('data-balance') || '0.00000000';
|
|
const selectedPendingBalance = selectedOption.getAttribute('data-pending-balance') || '';
|
|
|
|
icon.src = `/static/images/coins/${getImageFilename(selectedOption.value)}`;
|
|
|
|
if (showBalance) {
|
|
let html = `
|
|
<div class="text-gray-900 dark:text-white">${selectedCoinName}</div>
|
|
<div class="text-gray-500 dark:text-gray-400 text-xs">Balance: ${selectedBalance}</div>
|
|
`;
|
|
|
|
if (selectedPendingBalance && parseFloat(selectedPendingBalance) > 0) {
|
|
html += `<div class="text-green-500 text-xs">+${selectedPendingBalance} pending</div>`;
|
|
}
|
|
|
|
textContainer.innerHTML = html;
|
|
textContainer.className = 'flex-grow text-left flex flex-col justify-center';
|
|
} else {
|
|
textContainer.textContent = selectedCoinName;
|
|
textContainer.className = 'flex-grow text-left';
|
|
}
|
|
}
|
|
|
|
button.addEventListener('click', function() {
|
|
dropdown.classList.toggle('hidden');
|
|
});
|
|
|
|
document.addEventListener('click', function(e) {
|
|
if (!wrapper.contains(e.target)) {
|
|
dropdown.classList.add('hidden');
|
|
}
|
|
});
|
|
|
|
wrapper.appendChild(button);
|
|
wrapper.appendChild(dropdown);
|
|
select.parentNode.insertBefore(wrapper, select);
|
|
|
|
}
|
|
|
|
function setupButtonHandlers() {
|
|
const addOfferButton = document.getElementById('add-new-offer-btn');
|
|
if (addOfferButton) {
|
|
addOfferButton.addEventListener('click', function() {
|
|
openAddModal('offer');
|
|
});
|
|
}
|
|
|
|
const addBidButton = document.getElementById('add-new-bid-btn');
|
|
if (addBidButton) {
|
|
addBidButton.addEventListener('click', function() {
|
|
openAddModal('bid');
|
|
});
|
|
}
|
|
|
|
const addCancelButton = document.getElementById('add-amm-cancel');
|
|
if (addCancelButton) {
|
|
addCancelButton.addEventListener('click', closeAddModal);
|
|
}
|
|
|
|
const addSaveButton = document.getElementById('add-amm-save');
|
|
if (addSaveButton) {
|
|
addSaveButton.addEventListener('click', saveNewItem);
|
|
}
|
|
|
|
const editCancelButton = document.getElementById('edit-amm-cancel');
|
|
if (editCancelButton) {
|
|
editCancelButton.addEventListener('click', closeEditModal);
|
|
}
|
|
|
|
const editSaveButton = document.getElementById('edit-amm-save');
|
|
if (editSaveButton) {
|
|
editSaveButton.addEventListener('click', saveEditedItem);
|
|
}
|
|
|
|
document.addEventListener('click', function(e) {
|
|
if (e.target && (e.target.classList.contains('delete-amm-item') || e.target.closest('.delete-amm-item'))) {
|
|
const button = e.target.classList.contains('delete-amm-item') ? e.target : e.target.closest('.delete-amm-item');
|
|
const type = button.getAttribute('data-type');
|
|
const id = button.getAttribute('data-id');
|
|
const name = button.getAttribute('data-name');
|
|
|
|
if (!id && !name) {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Error', 'Could not identify the item to delete.');
|
|
} else {
|
|
alert('Error: Could not identify the item to delete.');
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (window.showConfirmModal) {
|
|
window.showConfirmModal(
|
|
'Confirm Deletion',
|
|
`Are you sure you want to delete this ${type}?\n\nName: ${name || 'Unnamed'}\n\nThis action cannot be undone.`,
|
|
function() {
|
|
deleteAmmItem(type, id, name);
|
|
}
|
|
);
|
|
} else {
|
|
if (confirm(`Are you sure you want to delete this ${type}?`)) {
|
|
deleteAmmItem(type, id, name);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (e.target && (e.target.classList.contains('edit-amm-item') || e.target.closest('.edit-amm-item'))) {
|
|
const button = e.target.classList.contains('edit-amm-item') ? e.target : e.target.closest('.edit-amm-item');
|
|
const type = button.getAttribute('data-type');
|
|
const id = button.getAttribute('data-id');
|
|
const name = button.getAttribute('data-name');
|
|
|
|
if (!id && !name) {
|
|
alert('Error: Could not identify the item to edit.');
|
|
return;
|
|
}
|
|
|
|
openEditModal(type, id, name);
|
|
}
|
|
});
|
|
|
|
const addModal = document.getElementById('add-amm-modal');
|
|
if (addModal) {
|
|
addModal.addEventListener('click', function(e) {
|
|
if (e.target === addModal) {
|
|
closeAddModal();
|
|
}
|
|
});
|
|
}
|
|
|
|
const editModal = document.getElementById('edit-amm-modal');
|
|
if (editModal) {
|
|
editModal.addEventListener('click', function(e) {
|
|
if (e.target === editModal) {
|
|
closeEditModal();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function openAddModal(type) {
|
|
debugLog(`Opening add modal for ${type}`);
|
|
|
|
const coinFromCheck = document.getElementById('add-amm-coin-from');
|
|
const coinToCheck = document.getElementById('add-amm-coin-to');
|
|
|
|
if (!coinFromCheck || !coinToCheck || coinFromCheck.options.length < 2 || coinToCheck.options.length < 2) {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Configuration Error', 'At least 2 different coins must be configured in BasicSwap to create AMM offers/bids. Please configure additional coins first.');
|
|
} else {
|
|
alert('At least 2 different coins must be configured in BasicSwap to create AMM offers/bids. Please configure additional coins first.');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const modalTitle = document.getElementById('add-modal-title');
|
|
if (modalTitle) {
|
|
modalTitle.textContent = `Add New ${type.charAt(0).toUpperCase() + type.slice(1)}`;
|
|
}
|
|
|
|
const modal = document.getElementById('add-amm-modal');
|
|
if (modal) {
|
|
modal.classList.remove('hidden');
|
|
|
|
modal.setAttribute('data-amm-type', type);
|
|
}
|
|
|
|
setTimeout(() => {
|
|
|
|
updateDropdownsForModalType('add');
|
|
|
|
initializeCustomSelects(type);
|
|
|
|
refreshDropdownBalanceDisplay(type);
|
|
|
|
if (typeof fetchBalanceData === 'function') {
|
|
fetchBalanceData()
|
|
.then(balanceData => {
|
|
if (type === 'offer' && typeof updateOfferDropdownBalances === 'function') {
|
|
updateOfferDropdownBalances(balanceData);
|
|
} else if (type === 'bid' && typeof updateBidDropdownBalances === 'function') {
|
|
updateBidDropdownBalances(balanceData);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error updating dropdown balances:', error);
|
|
});
|
|
}
|
|
}, 50);
|
|
|
|
document.getElementById('add-amm-type').value = type;
|
|
|
|
document.getElementById('add-amm-name').value = 'Unnamed Offer';
|
|
document.getElementById('add-amm-enabled').checked = true;
|
|
|
|
const coinFromSelect = document.getElementById('add-amm-coin-from');
|
|
const coinToSelect = document.getElementById('add-amm-coin-to');
|
|
|
|
if (coinFromSelect && coinFromSelect.options.length > 0) {
|
|
coinFromSelect.selectedIndex = 0;
|
|
coinFromSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}
|
|
|
|
if (coinToSelect && coinToSelect.options.length > 1) {
|
|
coinToSelect.selectedIndex = 1;
|
|
coinToSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
|
} else if (coinToSelect && coinToSelect.options.length > 0) {
|
|
coinToSelect.selectedIndex = 0;
|
|
coinToSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}
|
|
|
|
document.getElementById('add-amm-amount').value = '';
|
|
|
|
const adjustRatesSelect = document.getElementById('add-offer-adjust-rates');
|
|
if (adjustRatesSelect) {
|
|
adjustRatesSelect.value = 'false';
|
|
}
|
|
|
|
if (type === 'offer') {
|
|
const offerFields = document.getElementById('add-offer-fields');
|
|
if (offerFields) {
|
|
offerFields.classList.remove('hidden');
|
|
}
|
|
|
|
const bidFields = document.getElementById('add-bid-fields');
|
|
if (bidFields) {
|
|
bidFields.classList.add('hidden');
|
|
}
|
|
|
|
document.getElementById('add-amm-rate-label').textContent = 'Minimum Rate';
|
|
document.getElementById('add-amm-rate').value = '0.0001';
|
|
document.getElementById('add-offer-ratetweakpercent').value = '0';
|
|
document.getElementById('add-offer-min-coin-from-amt').value = '';
|
|
document.getElementById('add-offer-valid-seconds').value = '3600';
|
|
document.getElementById('add-offer-address').value = 'auto';
|
|
document.getElementById('add-offer-min-swap-amount').value = '0.001';
|
|
document.getElementById('add-offer-amount-step').value = '0.001';
|
|
|
|
const coinFrom = document.getElementById('add-amm-coin-from');
|
|
const coinTo = document.getElementById('add-amm-coin-to');
|
|
const swapType = document.getElementById('add-offer-swap-type');
|
|
|
|
if (coinFrom && coinTo && swapType) {
|
|
updateSwapTypeOptions(coinFrom.value, coinTo.value, swapType);
|
|
}
|
|
} else if (type === 'bid') {
|
|
const offerFields = document.getElementById('add-offer-fields');
|
|
if (offerFields) {
|
|
offerFields.classList.add('hidden');
|
|
}
|
|
|
|
const bidFields = document.getElementById('add-bid-fields');
|
|
if (bidFields) {
|
|
bidFields.classList.remove('hidden');
|
|
|
|
document.getElementById('add-amm-rate-label').textContent = 'Max Rate';
|
|
document.getElementById('add-amm-rate').value = '10000.0';
|
|
document.getElementById('add-bid-min-coin-to-balance').value = '1.0';
|
|
document.getElementById('add-bid-max-concurrent').value = '1';
|
|
document.getElementById('add-bid-address').value = 'auto';
|
|
document.getElementById('add-bid-min-swap-amount').value = '0.001';
|
|
}
|
|
}
|
|
|
|
if (coinFromSelect && coinToSelect) {
|
|
const handleCoinChange = function() {
|
|
const fromValue = coinFromSelect.value;
|
|
const toValue = coinToSelect.value;
|
|
|
|
if (fromValue && toValue && fromValue === toValue) {
|
|
for (let i = 0; i < coinToSelect.options.length; i++) {
|
|
if (coinToSelect.options[i].value !== fromValue) {
|
|
coinToSelect.selectedIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
coinFromSelect.addEventListener('change', handleCoinChange);
|
|
coinToSelect.addEventListener('change', handleCoinChange);
|
|
}
|
|
|
|
if (type === 'offer') {
|
|
setupBiddingControls('add');
|
|
}
|
|
}
|
|
|
|
function closeAddModal() {
|
|
const modal = document.getElementById('add-amm-modal');
|
|
if (modal) {
|
|
modal.classList.add('hidden');
|
|
closeAllDropdowns();
|
|
}
|
|
}
|
|
|
|
function saveNewItem() {
|
|
const type = document.getElementById('add-amm-type').value;
|
|
|
|
debugLog(`Saving new ${type}`);
|
|
|
|
const configTextarea = document.querySelector('textarea[name="config_content"]');
|
|
if (!configTextarea) {
|
|
alert('Error: Could not find the configuration textarea.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const configText = configTextarea.value.trim();
|
|
if (!configText) {
|
|
alert('Error: Configuration is empty.');
|
|
return;
|
|
}
|
|
|
|
const config = JSON.parse(configText);
|
|
|
|
const uniqueId = `${type}_${Date.now()}`;
|
|
|
|
const name = document.getElementById('add-amm-name').value.trim();
|
|
|
|
if (!name || name === '') {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Validation Error', 'Name is required and cannot be empty.');
|
|
} else {
|
|
alert('Name is required and cannot be empty.');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const coinFrom = document.getElementById('add-amm-coin-from').value;
|
|
const coinTo = document.getElementById('add-amm-coin-to').value;
|
|
|
|
if (!coinFrom || !coinTo) {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Validation Error', 'Please select both Coin From and Coin To.');
|
|
} else {
|
|
alert('Please select both Coin From and Coin To.');
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (coinFrom === coinTo) {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Validation Error', 'Coin From and Coin To must be different.');
|
|
} else {
|
|
alert('Coin From and Coin To must be different.');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const newItem = {
|
|
id: uniqueId,
|
|
name: name,
|
|
enabled: document.getElementById('add-amm-enabled').checked,
|
|
coin_from: document.getElementById('add-amm-coin-from').value,
|
|
coin_to: document.getElementById('add-amm-coin-to').value,
|
|
amount: parseFloat(document.getElementById('add-amm-amount').value),
|
|
amount_variable: true
|
|
};
|
|
|
|
if (type === 'offer') {
|
|
newItem.minrate = parseFloat(document.getElementById('add-amm-rate').value);
|
|
newItem.ratetweakpercent = parseFloat(document.getElementById('add-offer-ratetweakpercent').value || '0');
|
|
newItem.adjust_rates_based_on_market = document.getElementById('add-offer-adjust-rates').value;
|
|
newItem.swap_type = document.getElementById('add-offer-swap-type').value || 'adaptor_sig';
|
|
const automationStrategyElement = document.getElementById('add-offer-automation-strategy');
|
|
newItem.automation_strategy = automationStrategyElement ? automationStrategyElement.value : 'accept_all';
|
|
|
|
const minCoinFromAmt = document.getElementById('add-offer-min-coin-from-amt').value;
|
|
if (minCoinFromAmt) {
|
|
newItem.min_coin_from_amt = parseFloat(minCoinFromAmt);
|
|
}
|
|
|
|
const validSeconds = document.getElementById('add-offer-valid-seconds').value;
|
|
if (validSeconds) {
|
|
const seconds = parseInt(validSeconds);
|
|
if (seconds < 600) {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Validation Error', 'Offer valid seconds must be at least 600 (10 minutes)');
|
|
} else {
|
|
alert('Offer valid seconds must be at least 600 (10 minutes)');
|
|
}
|
|
return;
|
|
}
|
|
newItem.offer_valid_seconds = seconds;
|
|
}
|
|
|
|
const address = document.getElementById('add-offer-address').value;
|
|
if (address) {
|
|
newItem.address = address;
|
|
}
|
|
|
|
const minSwapAmount = document.getElementById('add-offer-min-swap-amount').value;
|
|
if (minSwapAmount) {
|
|
newItem.min_swap_amount = parseFloat(minSwapAmount);
|
|
}
|
|
|
|
const amountStep = parseFloat(document.getElementById('add-offer-amount-step').value);
|
|
const offerAmount = parseFloat(document.getElementById('add-amm-amount').value);
|
|
|
|
if (!amountStep) {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Validation Error', 'Offer Size Increment is required. This privacy feature prevents revealing your exact wallet balance.');
|
|
} else {
|
|
alert('Offer Size Increment is required. This privacy feature prevents revealing your exact wallet balance.');
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (/^[0-9]*\.?[0-9]*$/.test(amountStep)) {
|
|
if (amountStep <= 0) {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Validation Error', 'Offer Size Increment must be greater than zero.');
|
|
} else {
|
|
alert('Offer Size Increment must be greater than zero.');
|
|
}
|
|
return;
|
|
}
|
|
if (amountStep < 0.001) {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Validation Error', 'Offer Size Increment must be at least 0.001.');
|
|
} else {
|
|
alert('Offer Size Increment must be at least 0.001.');
|
|
}
|
|
return;
|
|
}
|
|
if (amountStep > offerAmount) {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Validation Error', `Offer Size Increment (${amountStep}) cannot be greater than the offer amount (${offerAmount}).`);
|
|
} else {
|
|
alert(`Offer Size Increment (${amountStep}) cannot be greater than the offer amount (${offerAmount}).`);
|
|
}
|
|
return;
|
|
}
|
|
newItem.amount_step = amountStep;
|
|
console.log(`Offer Size Increment set to: ${newItem.amount_step}`);
|
|
} else {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Validation Error', 'Invalid Offer Size Increment value. Please enter a valid decimal number.');
|
|
} else {
|
|
alert('Invalid Offer Size Increment value. Please enter a valid decimal number.');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const attemptBidsFirst = document.getElementById('add-offer-attempt-bids-first');
|
|
if (attemptBidsFirst && attemptBidsFirst.checked) {
|
|
newItem.attempt_bids_first = true;
|
|
|
|
const bidStrategy = document.getElementById('add-offer-bid-strategy').value;
|
|
if (bidStrategy) {
|
|
newItem.bid_strategy = bidStrategy;
|
|
}
|
|
|
|
const maxBidPercentage = document.getElementById('add-offer-max-bid-percentage').value;
|
|
if (maxBidPercentage) {
|
|
newItem.max_bid_percentage = parseInt(maxBidPercentage);
|
|
}
|
|
|
|
const bidRateTolerance = document.getElementById('add-offer-bid-rate-tolerance').value;
|
|
if (bidRateTolerance) {
|
|
newItem.bid_rate_tolerance = parseFloat(bidRateTolerance);
|
|
}
|
|
|
|
const minRemainingOffer = document.getElementById('add-offer-min-remaining-offer').value;
|
|
if (minRemainingOffer) {
|
|
newItem.min_remaining_offer = parseFloat(minRemainingOffer);
|
|
}
|
|
}
|
|
} else if (type === 'bid') {
|
|
newItem.max_rate = parseFloat(document.getElementById('add-amm-rate').value);
|
|
newItem.offers_to_bid_on = document.getElementById('add-bid-offers-to-bid-on').value || 'all';
|
|
|
|
const minCoinToBalance = document.getElementById('add-bid-min-coin-to-balance').value;
|
|
if (minCoinToBalance) {
|
|
newItem.min_coin_to_balance = parseFloat(minCoinToBalance);
|
|
}
|
|
|
|
const maxConcurrent = document.getElementById('add-bid-max-concurrent').value;
|
|
if (maxConcurrent) {
|
|
newItem.max_concurrent = parseInt(maxConcurrent);
|
|
}
|
|
|
|
const address = document.getElementById('add-bid-address').value;
|
|
if (address) {
|
|
newItem.address = address;
|
|
}
|
|
|
|
const minSwapAmount = document.getElementById('add-bid-min-swap-amount').value;
|
|
if (minSwapAmount) {
|
|
newItem.min_swap_amount = parseFloat(minSwapAmount);
|
|
}
|
|
|
|
const useBalanceBidding = document.getElementById('add-bid-use-balance-bidding').checked;
|
|
if (useBalanceBidding) {
|
|
newItem.use_balance_bidding = true;
|
|
}
|
|
}
|
|
|
|
if (type === 'offer') {
|
|
if (!Array.isArray(config.offers)) {
|
|
config.offers = [];
|
|
}
|
|
config.offers.push(newItem);
|
|
} else if (type === 'bid') {
|
|
if (!Array.isArray(config.bids)) {
|
|
config.bids = [];
|
|
}
|
|
config.bids.push(newItem);
|
|
} else {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Error', `Invalid type ${type}`);
|
|
} else {
|
|
alert(`Error: Invalid type ${type}`);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const wasReadonly = configTextarea.hasAttribute('readonly');
|
|
if (wasReadonly) {
|
|
configTextarea.removeAttribute('readonly');
|
|
}
|
|
|
|
configTextarea.value = JSON.stringify(config, null, 4);
|
|
|
|
closeAddModal();
|
|
|
|
const saveButton = document.getElementById('save_config_btn');
|
|
if (saveButton && !saveButton.disabled) {
|
|
saveButton.click();
|
|
|
|
setTimeout(() => {
|
|
if (window.AmmCounterManager && window.AmmCounterManager.fetchAmmStatus) {
|
|
window.AmmCounterManager.fetchAmmStatus();
|
|
}
|
|
if (window.SummaryManager && window.SummaryManager.fetchSummaryData) {
|
|
window.SummaryManager.fetchSummaryData();
|
|
}
|
|
}, 1000);
|
|
} else {
|
|
const form = configTextarea.closest('form');
|
|
if (form) {
|
|
const hiddenInput = document.createElement('input');
|
|
hiddenInput.type = 'hidden';
|
|
hiddenInput.name = 'save_config';
|
|
hiddenInput.value = 'true';
|
|
form.appendChild(hiddenInput);
|
|
form.submit();
|
|
} else {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Error', 'Could not save the configuration. Please try using the Settings tab instead.');
|
|
} else {
|
|
alert('Error: Could not save the configuration.');
|
|
}
|
|
}
|
|
}
|
|
|
|
if (wasReadonly) {
|
|
configTextarea.setAttribute('readonly', '');
|
|
}
|
|
} catch (error) {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Configuration Error', `Error processing the configuration: ${error.message}`);
|
|
} else {
|
|
alert(`Error processing the configuration: ${error.message}`);
|
|
}
|
|
debugLog('Error saving new item:', error);
|
|
}
|
|
}
|
|
|
|
function openEditModal(type, id, name) {
|
|
debugLog(`Opening edit modal for ${type} with id: ${id}, name: ${name}`);
|
|
|
|
const configTextarea = document.querySelector('textarea[name="config_content"]');
|
|
if (!configTextarea) {
|
|
alert('Error: Could not find the configuration textarea.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const configText = configTextarea.value.trim();
|
|
if (!configText) {
|
|
alert('Error: Configuration is empty.');
|
|
return;
|
|
}
|
|
|
|
const config = JSON.parse(configText);
|
|
|
|
let item = null;
|
|
|
|
if (type === 'offer' && Array.isArray(config.offers)) {
|
|
item = config.offers.find(offer =>
|
|
(id && offer.id === id) || (!id && offer.name === name)
|
|
);
|
|
} else if (type === 'bid' && Array.isArray(config.bids)) {
|
|
item = config.bids.find(bid =>
|
|
(id && bid.id === id) || (!id && bid.name === name)
|
|
);
|
|
}
|
|
|
|
if (!item) {
|
|
alert(`Could not find the ${type} to edit.`);
|
|
return;
|
|
}
|
|
|
|
const modalTitle = document.getElementById('edit-modal-title');
|
|
if (modalTitle) {
|
|
modalTitle.textContent = `Edit ${type.charAt(0).toUpperCase() + type.slice(1)}`;
|
|
}
|
|
|
|
const modal = document.getElementById('edit-amm-modal');
|
|
if (modal) {
|
|
modal.classList.remove('hidden');
|
|
|
|
modal.setAttribute('data-amm-type', type);
|
|
}
|
|
|
|
setTimeout(() => {
|
|
|
|
updateDropdownsForModalType('edit');
|
|
|
|
initializeCustomSelects(type);
|
|
|
|
refreshDropdownBalanceDisplay(type);
|
|
|
|
if (typeof fetchBalanceData === 'function') {
|
|
fetchBalanceData()
|
|
.then(balanceData => {
|
|
if (type === 'offer' && typeof updateOfferDropdownBalances === 'function') {
|
|
updateOfferDropdownBalances(balanceData);
|
|
} else if (type === 'bid' && typeof updateBidDropdownBalances === 'function') {
|
|
updateBidDropdownBalances(balanceData);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error updating dropdown balances:', error);
|
|
});
|
|
}
|
|
}, 50);
|
|
|
|
document.getElementById('edit-amm-type').value = type;
|
|
document.getElementById('edit-amm-id').value = id || '';
|
|
document.getElementById('edit-amm-original-name').value = name;
|
|
|
|
document.getElementById('edit-amm-name').value = item.name || '';
|
|
document.getElementById('edit-amm-enabled').checked = item.enabled || false;
|
|
|
|
const coinFromSelect = document.getElementById('edit-amm-coin-from');
|
|
const coinToSelect = document.getElementById('edit-amm-coin-to');
|
|
|
|
coinFromSelect.value = item.coin_from || '';
|
|
coinToSelect.value = item.coin_to || '';
|
|
|
|
if (coinFromSelect) {
|
|
coinFromSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}
|
|
if (coinToSelect) {
|
|
coinToSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}
|
|
|
|
document.getElementById('edit-amm-amount').value = item.amount || '';
|
|
|
|
if (type === 'offer') {
|
|
const offerFields = document.getElementById('edit-offer-fields');
|
|
if (offerFields) {
|
|
offerFields.classList.remove('hidden');
|
|
}
|
|
|
|
const bidFields = document.getElementById('edit-bid-fields');
|
|
if (bidFields) {
|
|
bidFields.classList.add('hidden');
|
|
}
|
|
|
|
document.getElementById('edit-amm-rate').value = item.minrate || '';
|
|
document.getElementById('edit-offer-ratetweakpercent').value = item.ratetweakpercent || '0';
|
|
document.getElementById('edit-offer-min-coin-from-amt').value = item.min_coin_from_amt || '';
|
|
document.getElementById('edit-offer-valid-seconds').value = item.offer_valid_seconds || '3600';
|
|
document.getElementById('edit-offer-address').value = item.address || 'auto';
|
|
document.getElementById('edit-offer-adjust-rates').value = item.adjust_rates_based_on_market || 'false';
|
|
document.getElementById('edit-offer-swap-type').value = item.swap_type || 'adaptor_sig';
|
|
document.getElementById('edit-offer-min-swap-amount').value = item.min_swap_amount || '0.001';
|
|
document.getElementById('edit-offer-amount-step').value = item.amount_step || '0.001';
|
|
const editAutomationStrategyElement = document.getElementById('edit-offer-automation-strategy');
|
|
if (editAutomationStrategyElement) {
|
|
editAutomationStrategyElement.value = item.automation_strategy || 'accept_all';
|
|
}
|
|
|
|
const coinFrom = document.getElementById('edit-amm-coin-from');
|
|
const coinTo = document.getElementById('edit-amm-coin-to');
|
|
const swapType = document.getElementById('edit-offer-swap-type');
|
|
|
|
if (coinFrom && coinTo && swapType) {
|
|
updateSwapTypeOptions(coinFrom.value, coinTo.value, swapType);
|
|
}
|
|
} else if (type === 'bid') {
|
|
const offerFields = document.getElementById('edit-offer-fields');
|
|
if (offerFields) {
|
|
offerFields.classList.add('hidden');
|
|
}
|
|
|
|
const bidFields = document.getElementById('edit-bid-fields');
|
|
if (bidFields) {
|
|
bidFields.classList.remove('hidden');
|
|
|
|
document.getElementById('edit-amm-rate-label').textContent = 'Max Rate';
|
|
|
|
document.getElementById('edit-amm-rate').value = item.max_rate || '';
|
|
document.getElementById('edit-bid-min-coin-to-balance').value = item.min_coin_to_balance || '';
|
|
document.getElementById('edit-bid-max-concurrent').value = item.max_concurrent || '1';
|
|
document.getElementById('edit-bid-address').value = item.address || 'auto';
|
|
document.getElementById('edit-bid-min-swap-amount').value = item.min_swap_amount || '';
|
|
document.getElementById('edit-bid-offers-to-bid-on').value = item.offers_to_bid_on || 'all';
|
|
document.getElementById('edit-bid-use-balance-bidding').checked = item.use_balance_bidding || false;
|
|
}
|
|
}
|
|
|
|
const editCoinFromSelect = coinFromSelect;
|
|
const editCoinToSelect = coinToSelect;
|
|
|
|
if (editCoinFromSelect && editCoinToSelect) {
|
|
const handleEditCoinChange = function() {
|
|
const fromValue = editCoinFromSelect.value;
|
|
const toValue = editCoinToSelect.value;
|
|
|
|
if (fromValue && toValue && fromValue === toValue) {
|
|
for (let i = 0; i < editCoinToSelect.options.length; i++) {
|
|
if (editCoinToSelect.options[i].value !== fromValue) {
|
|
editCoinToSelect.selectedIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
editCoinFromSelect.removeEventListener('change', handleEditCoinChange);
|
|
editCoinToSelect.removeEventListener('change', handleEditCoinChange);
|
|
|
|
editCoinFromSelect.addEventListener('change', handleEditCoinChange);
|
|
editCoinToSelect.addEventListener('change', handleEditCoinChange);
|
|
}
|
|
|
|
if (type === 'offer') {
|
|
setupBiddingControls('edit');
|
|
populateBiddingControls('edit', item);
|
|
}
|
|
} catch (error) {
|
|
alert(`Error processing the configuration: ${error.message}`);
|
|
debugLog('Error opening edit modal:', error);
|
|
}
|
|
}
|
|
|
|
function closeAllDropdowns() {
|
|
const dropdowns = document.querySelectorAll('.absolute.z-50');
|
|
dropdowns.forEach(dropdown => {
|
|
dropdown.classList.add('hidden');
|
|
});
|
|
}
|
|
|
|
function closeEditModal() {
|
|
const modal = document.getElementById('edit-amm-modal');
|
|
if (modal) {
|
|
modal.classList.add('hidden');
|
|
closeAllDropdowns();
|
|
}
|
|
}
|
|
|
|
function saveEditedItem() {
|
|
const type = document.getElementById('edit-amm-type').value;
|
|
const id = document.getElementById('edit-amm-id').value;
|
|
const originalName = document.getElementById('edit-amm-original-name').value;
|
|
|
|
debugLog(`Saving edited ${type} with id: ${id}, original name: ${originalName}`);
|
|
|
|
const configTextarea = document.querySelector('textarea[name="config_content"]');
|
|
if (!configTextarea) {
|
|
alert('Error: Could not find the configuration textarea.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const configText = configTextarea.value.trim();
|
|
if (!configText) {
|
|
alert('Error: Configuration is empty.');
|
|
return;
|
|
}
|
|
|
|
const config = JSON.parse(configText);
|
|
|
|
const name = document.getElementById('edit-amm-name').value.trim();
|
|
|
|
if (!name || name === '') {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Validation Error', 'Name is required and cannot be empty.');
|
|
} else {
|
|
alert('Name is required and cannot be empty.');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const coinFrom = document.getElementById('edit-amm-coin-from').value;
|
|
const coinTo = document.getElementById('edit-amm-coin-to').value;
|
|
|
|
if (!coinFrom || !coinTo) {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Validation Error', 'Please select both Coin From and Coin To.');
|
|
} else {
|
|
alert('Please select both Coin From and Coin To.');
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (coinFrom === coinTo) {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Validation Error', 'Coin From and Coin To must be different.');
|
|
} else {
|
|
alert('Coin From and Coin To must be different.');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const updatedItem = {
|
|
name: name,
|
|
enabled: document.getElementById('edit-amm-enabled').checked,
|
|
coin_from: document.getElementById('edit-amm-coin-from').value,
|
|
coin_to: document.getElementById('edit-amm-coin-to').value,
|
|
amount: parseFloat(document.getElementById('edit-amm-amount').value),
|
|
amount_variable: true
|
|
};
|
|
|
|
if (id) {
|
|
updatedItem.id = id;
|
|
}
|
|
|
|
if (type === 'offer') {
|
|
updatedItem.minrate = parseFloat(document.getElementById('edit-amm-rate').value);
|
|
updatedItem.ratetweakpercent = parseFloat(document.getElementById('edit-offer-ratetweakpercent').value || '0');
|
|
updatedItem.adjust_rates_based_on_market = document.getElementById('edit-offer-adjust-rates').value;
|
|
updatedItem.swap_type = document.getElementById('edit-offer-swap-type').value || 'adaptor_sig';
|
|
const editAutomationStrategyElement = document.getElementById('edit-offer-automation-strategy');
|
|
updatedItem.automation_strategy = editAutomationStrategyElement ? editAutomationStrategyElement.value : 'accept_all';
|
|
|
|
const minCoinFromAmt = document.getElementById('edit-offer-min-coin-from-amt').value;
|
|
if (minCoinFromAmt) {
|
|
updatedItem.min_coin_from_amt = parseFloat(minCoinFromAmt);
|
|
}
|
|
|
|
const validSeconds = document.getElementById('edit-offer-valid-seconds').value;
|
|
if (validSeconds) {
|
|
const seconds = parseInt(validSeconds);
|
|
if (seconds < 600) {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Validation Error', 'Offer valid seconds must be at least 600 (10 minutes)');
|
|
} else {
|
|
alert('Offer valid seconds must be at least 600 (10 minutes)');
|
|
}
|
|
return;
|
|
}
|
|
updatedItem.offer_valid_seconds = seconds;
|
|
}
|
|
|
|
const address = document.getElementById('edit-offer-address').value;
|
|
if (address) {
|
|
updatedItem.address = address;
|
|
}
|
|
|
|
const minSwapAmount = document.getElementById('edit-offer-min-swap-amount').value;
|
|
if (minSwapAmount) {
|
|
updatedItem.min_swap_amount = parseFloat(minSwapAmount);
|
|
}
|
|
|
|
const amountStep = parseFloat(document.getElementById('edit-offer-amount-step').value);
|
|
const offerAmount = parseFloat(document.getElementById('edit-amm-amount').value);
|
|
|
|
if (!amountStep) {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Validation Error', 'Offer Size Increment is required. This privacy feature prevents revealing your exact wallet balance.');
|
|
} else {
|
|
alert('Offer Size Increment is required. This privacy feature prevents revealing your exact wallet balance.');
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (/^[0-9]*\.?[0-9]*$/.test(amountStep)) {
|
|
if (amountStep <= 0) {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Validation Error', 'Offer Size Increment must be greater than zero.');
|
|
} else {
|
|
alert('Offer Size Increment must be greater than zero.');
|
|
}
|
|
return;
|
|
}
|
|
if (amountStep < 0.001) {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Validation Error', 'Offer Size Increment must be at least 0.001.');
|
|
} else {
|
|
alert('Offer Size Increment must be at least 0.001.');
|
|
}
|
|
return;
|
|
}
|
|
if (amountStep > offerAmount) {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Validation Error', `Offer Size Increment (${amountStep}) cannot be greater than the offer amount (${offerAmount}).`);
|
|
} else {
|
|
alert(`Offer Size Increment (${amountStep}) cannot be greater than the offer amount (${offerAmount}).`);
|
|
}
|
|
return;
|
|
}
|
|
updatedItem.amount_step = amountStep;
|
|
console.log(`Offer Size Increment set to: ${updatedItem.amount_step}`);
|
|
} else {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Validation Error', 'Invalid Offer Size Increment value. Please enter a valid decimal number.');
|
|
} else {
|
|
alert('Invalid Offer Size Increment value. Please enter a valid decimal number.');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const attemptBidsFirst = document.getElementById('edit-offer-attempt-bids-first');
|
|
if (attemptBidsFirst && attemptBidsFirst.checked) {
|
|
updatedItem.attempt_bids_first = true;
|
|
|
|
const bidStrategy = document.getElementById('edit-offer-bid-strategy').value;
|
|
if (bidStrategy) {
|
|
updatedItem.bid_strategy = bidStrategy;
|
|
}
|
|
|
|
const maxBidPercentage = document.getElementById('edit-offer-max-bid-percentage').value;
|
|
if (maxBidPercentage) {
|
|
updatedItem.max_bid_percentage = parseInt(maxBidPercentage);
|
|
}
|
|
|
|
const bidRateTolerance = document.getElementById('edit-offer-bid-rate-tolerance').value;
|
|
if (bidRateTolerance) {
|
|
updatedItem.bid_rate_tolerance = parseFloat(bidRateTolerance);
|
|
}
|
|
|
|
const minRemainingOffer = document.getElementById('edit-offer-min-remaining-offer').value;
|
|
if (minRemainingOffer) {
|
|
updatedItem.min_remaining_offer = parseFloat(minRemainingOffer);
|
|
}
|
|
} else {
|
|
updatedItem.attempt_bids_first = false;
|
|
}
|
|
} else if (type === 'bid') {
|
|
updatedItem.max_rate = parseFloat(document.getElementById('edit-amm-rate').value);
|
|
updatedItem.offers_to_bid_on = document.getElementById('edit-bid-offers-to-bid-on').value || 'all';
|
|
|
|
const minCoinToBalance = document.getElementById('edit-bid-min-coin-to-balance').value;
|
|
if (minCoinToBalance) {
|
|
updatedItem.min_coin_to_balance = parseFloat(minCoinToBalance);
|
|
}
|
|
|
|
const maxConcurrent = document.getElementById('edit-bid-max-concurrent').value;
|
|
if (maxConcurrent) {
|
|
updatedItem.max_concurrent = parseInt(maxConcurrent);
|
|
}
|
|
|
|
const address = document.getElementById('edit-bid-address').value;
|
|
if (address) {
|
|
updatedItem.address = address;
|
|
}
|
|
|
|
const minSwapAmount = document.getElementById('edit-bid-min-swap-amount').value;
|
|
if (minSwapAmount) {
|
|
updatedItem.min_swap_amount = parseFloat(minSwapAmount);
|
|
}
|
|
|
|
const useBalanceBidding = document.getElementById('edit-bid-use-balance-bidding').checked;
|
|
if (useBalanceBidding) {
|
|
updatedItem.use_balance_bidding = true;
|
|
} else {
|
|
delete updatedItem.use_balance_bidding;
|
|
}
|
|
}
|
|
|
|
if (type === 'offer' && Array.isArray(config.offers)) {
|
|
const index = config.offers.findIndex(item =>
|
|
(id && item.id === id) || (!id && item.name === originalName)
|
|
);
|
|
|
|
if (index !== -1) {
|
|
config.offers[index] = updatedItem;
|
|
debugLog(`Updated offer at index ${index}`);
|
|
} else {
|
|
alert(`Could not find the offer to update.`);
|
|
return;
|
|
}
|
|
} else if (type === 'bid' && Array.isArray(config.bids)) {
|
|
const index = config.bids.findIndex(item =>
|
|
(id && item.id === id) || (!id && item.name === originalName)
|
|
);
|
|
|
|
if (index !== -1) {
|
|
config.bids[index] = updatedItem;
|
|
debugLog(`Updated bid at index ${index}`);
|
|
} else {
|
|
alert(`Could not find the bid to update.`);
|
|
return;
|
|
}
|
|
} else {
|
|
alert(`Error: Invalid type or no ${type}s found in config.`);
|
|
return;
|
|
}
|
|
|
|
const wasReadonly = configTextarea.hasAttribute('readonly');
|
|
if (wasReadonly) {
|
|
configTextarea.removeAttribute('readonly');
|
|
}
|
|
|
|
configTextarea.value = JSON.stringify(config, null, 4);
|
|
|
|
closeEditModal();
|
|
|
|
const saveButton = document.getElementById('save_config_btn');
|
|
if (saveButton && !saveButton.disabled) {
|
|
saveButton.click();
|
|
|
|
setTimeout(() => {
|
|
if (window.AmmCounterManager && window.AmmCounterManager.fetchAmmStatus) {
|
|
window.AmmCounterManager.fetchAmmStatus();
|
|
}
|
|
if (window.SummaryManager && window.SummaryManager.fetchSummaryData) {
|
|
window.SummaryManager.fetchSummaryData();
|
|
}
|
|
}, 1000);
|
|
} else {
|
|
const form = configTextarea.closest('form');
|
|
if (form) {
|
|
const hiddenInput = document.createElement('input');
|
|
hiddenInput.type = 'hidden';
|
|
hiddenInput.name = 'save_config';
|
|
hiddenInput.value = 'true';
|
|
form.appendChild(hiddenInput);
|
|
form.submit();
|
|
} else {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Error', 'Could not save the configuration. Please try using the Settings tab instead.');
|
|
} else {
|
|
alert('Error: Could not save the configuration.');
|
|
}
|
|
}
|
|
}
|
|
|
|
if (wasReadonly) {
|
|
configTextarea.setAttribute('readonly', '');
|
|
}
|
|
} catch (error) {
|
|
alert(`Error processing the configuration: ${error.message}`);
|
|
debugLog('Error saving edited item:', error);
|
|
}
|
|
}
|
|
|
|
function deleteAmmItem(type, id, name) {
|
|
debugLog(`Deleting ${type} with id: ${id}, name: ${name}`);
|
|
|
|
const configTextarea = document.querySelector('textarea[name="config_content"]');
|
|
if (!configTextarea) {
|
|
alert('Error: Could not find the configuration textarea.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const configText = configTextarea.value.trim();
|
|
if (!configText) {
|
|
alert('Error: Configuration is empty.');
|
|
return;
|
|
}
|
|
|
|
const config = JSON.parse(configText);
|
|
|
|
if (type === 'offer' && Array.isArray(config.offers)) {
|
|
const index = config.offers.findIndex(item =>
|
|
(id && item.id === id) || (!id && item.name === name)
|
|
);
|
|
|
|
if (index !== -1) {
|
|
config.offers.splice(index, 1);
|
|
debugLog(`Removed offer at index ${index}`);
|
|
} else {
|
|
alert(`Could not find the offer to delete.`);
|
|
return;
|
|
}
|
|
} else if (type === 'bid' && Array.isArray(config.bids)) {
|
|
const index = config.bids.findIndex(item =>
|
|
(id && item.id === id) || (!id && item.name === name)
|
|
);
|
|
|
|
if (index !== -1) {
|
|
config.bids.splice(index, 1);
|
|
debugLog(`Removed bid at index ${index}`);
|
|
} else {
|
|
alert(`Could not find the bid to delete.`);
|
|
return;
|
|
}
|
|
} else {
|
|
alert(`Error: Invalid type or no ${type}s found in config.`);
|
|
return;
|
|
}
|
|
|
|
const wasReadonly = configTextarea.hasAttribute('readonly');
|
|
if (wasReadonly) {
|
|
configTextarea.removeAttribute('readonly');
|
|
}
|
|
|
|
configTextarea.value = JSON.stringify(config, null, 4);
|
|
|
|
const saveButton = document.getElementById('save_config_btn');
|
|
if (saveButton && !saveButton.disabled) {
|
|
saveButton.click();
|
|
|
|
setTimeout(() => {
|
|
if (window.AmmCounterManager && window.AmmCounterManager.fetchAmmStatus) {
|
|
window.AmmCounterManager.fetchAmmStatus();
|
|
}
|
|
if (window.SummaryManager && window.SummaryManager.fetchSummaryData) {
|
|
window.SummaryManager.fetchSummaryData();
|
|
}
|
|
}, 1000);
|
|
} else {
|
|
const form = configTextarea.closest('form');
|
|
if (form) {
|
|
const hiddenInput = document.createElement('input');
|
|
hiddenInput.type = 'hidden';
|
|
hiddenInput.name = 'save_config';
|
|
hiddenInput.value = 'true';
|
|
form.appendChild(hiddenInput);
|
|
form.submit();
|
|
} else {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Error', 'Could not save the configuration. Please try using the Settings tab instead.');
|
|
} else {
|
|
alert('Error: Could not save the configuration.');
|
|
}
|
|
}
|
|
}
|
|
|
|
if (wasReadonly) {
|
|
configTextarea.setAttribute('readonly', '');
|
|
}
|
|
} catch (error) {
|
|
alert(`Error processing the configuration: ${error.message}`);
|
|
debugLog('Error deleting item:', error);
|
|
}
|
|
}
|
|
|
|
const adaptor_sig_only_coins = ['6', 'Monero', '7', 'Particl Blind', '8', 'Particl Anon', '9', 'Wownero', '13', 'Firo', '16', 'Zano', '17', 'Bitcoin Cash', '18', 'Dogecoin'];
|
|
const secret_hash_only_coins = ['11', 'PIVX', '12', 'Dash'];
|
|
|
|
function updateSwapTypeOptions(coinFromValue, coinToValue, swapTypeSelect) {
|
|
if (!swapTypeSelect) return;
|
|
|
|
coinFromValue = String(coinFromValue);
|
|
coinToValue = String(coinToValue);
|
|
|
|
let disableSelect = false;
|
|
|
|
if (adaptor_sig_only_coins.includes(coinFromValue) || adaptor_sig_only_coins.includes(coinToValue)) {
|
|
swapTypeSelect.value = 'adaptor_sig';
|
|
disableSelect = true;
|
|
} else if (secret_hash_only_coins.includes(coinFromValue) || secret_hash_only_coins.includes(coinToValue)) {
|
|
swapTypeSelect.value = 'seller_first';
|
|
disableSelect = true;
|
|
} else {
|
|
swapTypeSelect.value = 'adaptor_sig';
|
|
disableSelect = false;
|
|
}
|
|
|
|
swapTypeSelect.disabled = disableSelect;
|
|
|
|
if (disableSelect) {
|
|
swapTypeSelect.classList.add('bg-gray-200', 'dark:bg-gray-600', 'cursor-not-allowed');
|
|
} else {
|
|
swapTypeSelect.classList.remove('bg-gray-200', 'dark:bg-gray-600', 'cursor-not-allowed');
|
|
}
|
|
}
|
|
|
|
function initializeCustomSelects(modalType = null) {
|
|
const coinSelects = [
|
|
document.getElementById('add-amm-coin-from'),
|
|
document.getElementById('add-amm-coin-to'),
|
|
document.getElementById('edit-amm-coin-from'),
|
|
document.getElementById('edit-amm-coin-to')
|
|
];
|
|
|
|
const swapTypeSelects = [
|
|
document.getElementById('add-offer-swap-type'),
|
|
document.getElementById('edit-offer-swap-type')
|
|
];
|
|
|
|
function createSwapTypeDropdown(select) {
|
|
if (!select) return;
|
|
|
|
if (select.style.display === 'none' && select.parentNode.querySelector('.relative')) {
|
|
return;
|
|
}
|
|
|
|
const wrapper = document.createElement('div');
|
|
wrapper.className = 'relative';
|
|
|
|
const display = document.createElement('div');
|
|
display.className = 'flex items-center w-full p-2.5 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white cursor-pointer';
|
|
|
|
const text = document.createElement('span');
|
|
text.className = 'flex-grow';
|
|
|
|
const arrow = document.createElement('span');
|
|
arrow.className = 'ml-2';
|
|
arrow.innerHTML = `
|
|
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
</svg>
|
|
`;
|
|
|
|
display.appendChild(text);
|
|
display.appendChild(arrow);
|
|
|
|
const dropdown = document.createElement('div');
|
|
dropdown.className = 'absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg hidden dark:bg-gray-700 dark:border-gray-600 max-h-60 overflow-y-auto';
|
|
|
|
Array.from(select.options).forEach(option => {
|
|
const item = document.createElement('div');
|
|
item.className = 'flex items-center p-2 hover:bg-gray-100 dark:hover:bg-gray-600 cursor-pointer text-gray-900 dark:text-white';
|
|
item.setAttribute('data-value', option.value);
|
|
|
|
const optionText = document.createElement('span');
|
|
const displayText = option.getAttribute('data-desc') || option.textContent.trim();
|
|
optionText.textContent = displayText;
|
|
|
|
item.appendChild(optionText);
|
|
|
|
item.addEventListener('click', function() {
|
|
if (select.disabled) return;
|
|
|
|
select.value = this.getAttribute('data-value');
|
|
text.textContent = displayText;
|
|
dropdown.classList.add('hidden');
|
|
|
|
const event = new Event('change', { bubbles: true });
|
|
select.dispatchEvent(event);
|
|
});
|
|
|
|
dropdown.appendChild(item);
|
|
});
|
|
|
|
const selectedOption = select.options[select.selectedIndex];
|
|
if (selectedOption) {
|
|
text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim();
|
|
}
|
|
|
|
display.addEventListener('click', function(e) {
|
|
if (select.disabled) return;
|
|
e.stopPropagation();
|
|
dropdown.classList.toggle('hidden');
|
|
});
|
|
|
|
document.addEventListener('click', function() {
|
|
dropdown.classList.add('hidden');
|
|
});
|
|
|
|
wrapper.appendChild(display);
|
|
wrapper.appendChild(dropdown);
|
|
select.parentNode.insertBefore(wrapper, select);
|
|
|
|
select.style.display = 'none';
|
|
|
|
select.addEventListener('change', function() {
|
|
const selectedOption = this.options[this.selectedIndex];
|
|
if (selectedOption) {
|
|
text.textContent = selectedOption.getAttribute('data-desc') || selectedOption.textContent.trim();
|
|
}
|
|
});
|
|
|
|
const observer = new MutationObserver(function(mutations) {
|
|
mutations.forEach(function(mutation) {
|
|
if (mutation.attributeName === 'disabled') {
|
|
if (select.disabled) {
|
|
display.classList.add('bg-gray-200', 'dark:bg-gray-600', 'cursor-not-allowed');
|
|
} else {
|
|
display.classList.remove('bg-gray-200', 'dark:bg-gray-600', 'cursor-not-allowed');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
observer.observe(select, { attributes: true });
|
|
|
|
if (select.disabled) {
|
|
display.classList.add('bg-gray-200', 'dark:bg-gray-600', 'cursor-not-allowed');
|
|
}
|
|
}
|
|
|
|
coinSelects.forEach(select => {
|
|
if (!select) return;
|
|
|
|
let showBalance = false;
|
|
if (modalType === 'offer' && select.id.includes('coin-from')) {
|
|
showBalance = true;
|
|
} else if (modalType === 'bid' && select.id.includes('coin-to')) {
|
|
showBalance = true;
|
|
}
|
|
|
|
createSimpleDropdown(select, showBalance);
|
|
});
|
|
|
|
swapTypeSelects.forEach(select => createSwapTypeDropdown(select));
|
|
}
|
|
|
|
function setupBiddingControls(modalType) {
|
|
const checkbox = document.getElementById(`${modalType}-offer-attempt-bids-first`);
|
|
const optionsDiv = document.getElementById(`${modalType}-offer-bidding-options`);
|
|
|
|
if (checkbox && optionsDiv) {
|
|
checkbox.addEventListener('change', function() {
|
|
if (this.checked) {
|
|
optionsDiv.classList.remove('hidden');
|
|
} else {
|
|
optionsDiv.classList.add('hidden');
|
|
}
|
|
});
|
|
|
|
if (checkbox.checked) {
|
|
optionsDiv.classList.remove('hidden');
|
|
} else {
|
|
optionsDiv.classList.add('hidden');
|
|
}
|
|
}
|
|
}
|
|
|
|
function populateBiddingControls(modalType, item) {
|
|
if (!item) return;
|
|
|
|
const attemptBidsFirst = document.getElementById(`${modalType}-offer-attempt-bids-first`);
|
|
const bidStrategy = document.getElementById(`${modalType}-offer-bid-strategy`);
|
|
const maxBidPercentage = document.getElementById(`${modalType}-offer-max-bid-percentage`);
|
|
const bidRateTolerance = document.getElementById(`${modalType}-offer-bid-rate-tolerance`);
|
|
const minRemainingOffer = document.getElementById(`${modalType}-offer-min-remaining-offer`);
|
|
|
|
if (attemptBidsFirst) {
|
|
attemptBidsFirst.checked = item.attempt_bids_first || false;
|
|
}
|
|
|
|
if (bidStrategy) {
|
|
bidStrategy.value = item.bid_strategy || 'balanced';
|
|
}
|
|
|
|
if (maxBidPercentage) {
|
|
maxBidPercentage.value = item.max_bid_percentage || '50';
|
|
}
|
|
|
|
if (bidRateTolerance) {
|
|
bidRateTolerance.value = item.bid_rate_tolerance || '2.0';
|
|
}
|
|
|
|
if (minRemainingOffer) {
|
|
minRemainingOffer.value = item.min_remaining_offer || '0.001';
|
|
}
|
|
|
|
if (attemptBidsFirst) {
|
|
attemptBidsFirst.dispatchEvent(new Event('change'));
|
|
}
|
|
}
|
|
|
|
function getRateFromCoinGecko(coinFromSelect, coinToSelect, rateInput) {
|
|
const coinFromOption = coinFromSelect.options[coinFromSelect.selectedIndex];
|
|
const coinToOption = coinToSelect.options[coinToSelect.selectedIndex];
|
|
|
|
if (!coinFromOption || !coinToOption) {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Rate Lookup Error', 'Please select both coins before getting the rate.');
|
|
} else {
|
|
alert('Coins from and to must be set first.');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const coinFromSymbol = coinFromOption.getAttribute('data-symbol');
|
|
const coinToSymbol = coinToOption.getAttribute('data-symbol');
|
|
|
|
if (!coinFromSymbol || !coinToSymbol) {
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Rate Lookup Error', 'Coin information is incomplete. Please try selecting the coins again.');
|
|
} else {
|
|
alert('Coin symbols not found.');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const originalValue = rateInput.value;
|
|
rateInput.value = 'Loading...';
|
|
rateInput.disabled = true;
|
|
|
|
const getRateButton = rateInput.parentElement.querySelector('button');
|
|
let originalButtonText = '';
|
|
if (getRateButton) {
|
|
originalButtonText = getRateButton.textContent;
|
|
getRateButton.disabled = true;
|
|
getRateButton.innerHTML = `
|
|
<svg class="animate-spin -ml-1 mr-2 h-3 w-3 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
Loading...
|
|
`;
|
|
}
|
|
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open('POST', '/json/rates');
|
|
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
|
xhr.onload = function() {
|
|
if (xhr.status === 200) {
|
|
try {
|
|
const response = JSON.parse(xhr.responseText);
|
|
debugLog('Rate response:', response);
|
|
|
|
if (response.coingecko && response.coingecko.rate_inferred) {
|
|
rateInput.value = response.coingecko.rate_inferred;
|
|
|
|
if (getRateButton && originalButtonText) {
|
|
getRateButton.disabled = false;
|
|
getRateButton.textContent = originalButtonText;
|
|
}
|
|
} else if (response.error) {
|
|
console.error('API error:', response.error);
|
|
rateInput.value = originalValue || '';
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Rate Service Error', `Unable to retrieve rate information: ${response.error}\n\nThis could be due to:\n• Temporary service unavailability\n• Network connectivity issues\n• Invalid coin pair\n\nPlease try again in a few moments.`);
|
|
} else {
|
|
alert('Error: ' + response.error);
|
|
}
|
|
} else if (response.coingecko_error) {
|
|
console.error('CoinGecko error:', response.coingecko_error);
|
|
rateInput.value = originalValue || '';
|
|
|
|
let userMessage = 'Unable to get current market rate from CoinGecko.';
|
|
let details = '';
|
|
|
|
if (typeof response.coingecko_error === 'number') {
|
|
switch(response.coingecko_error) {
|
|
case 8:
|
|
details = 'This usually means:\n• One or both coins are not supported by CoinGecko\n• The trading pair is not available\n• Temporary API limitations\n\nYou can manually enter a rate or try again later.';
|
|
break;
|
|
case 429:
|
|
details = 'Rate limit exceeded. Please wait a moment and try again.';
|
|
break;
|
|
case 404:
|
|
details = 'The requested coin pair was not found on CoinGecko.';
|
|
break;
|
|
case 500:
|
|
details = 'CoinGecko service is temporarily unavailable. Please try again later.';
|
|
break;
|
|
default:
|
|
details = `Error code: ${response.coingecko_error}\n\nThis may be a temporary issue. Please try again or enter the rate manually.`;
|
|
}
|
|
} else {
|
|
details = `${response.coingecko_error}\n\nPlease try again or enter the rate manually.`;
|
|
}
|
|
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Market Rate Unavailable', `${userMessage}\n\n${details}`);
|
|
} else {
|
|
alert('Unable to get rate from CoinGecko: ' + response.coingecko_error);
|
|
}
|
|
} else {
|
|
rateInput.value = originalValue || '';
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Rate Not Available', `No current market rate is available for this ${coinFromSymbol}/${coinToSymbol} trading pair.\n\nThis could mean:\n• The coins are not traded together on major exchanges\n• CoinGecko doesn't have data for this pair\n• The coins may not be supported\n\nPlease enter a rate manually based on your research.`);
|
|
} else {
|
|
alert('No rate available from CoinGecko for this pair.');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('Error parsing rate data:', e);
|
|
rateInput.value = originalValue || '';
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Data Processing Error', 'Unable to process the rate information received from the server.\n\nThis could be due to:\n• Temporary server issues\n• Data format problems\n• Network interference\n\nPlease try again in a moment.');
|
|
} else {
|
|
alert('Error retrieving rate information. Please try again later.');
|
|
}
|
|
}
|
|
} else {
|
|
console.error('Error fetching rate data:', xhr.status, xhr.statusText);
|
|
rateInput.value = originalValue || '';
|
|
let errorMessage = 'Unable to retrieve rate information from the server.';
|
|
let details = '';
|
|
|
|
switch(xhr.status) {
|
|
case 404:
|
|
details = 'The rate service endpoint was not found. This may be a configuration issue.';
|
|
break;
|
|
case 500:
|
|
details = 'The server encountered an internal error. Please try again later.';
|
|
break;
|
|
case 503:
|
|
details = 'The rate service is temporarily unavailable. Please try again in a few minutes.';
|
|
break;
|
|
case 429:
|
|
details = 'Too many requests. Please wait a moment before trying again.';
|
|
break;
|
|
default:
|
|
details = `Server returned error ${xhr.status}. The rate service may be temporarily unavailable.`;
|
|
}
|
|
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Rate Service Unavailable', `${errorMessage}\n\n${details}\n\nYou can enter the rate manually if needed.`);
|
|
} else {
|
|
alert(`Unable to retrieve rate information (HTTP ${xhr.status}). The rate service may be unavailable.`);
|
|
}
|
|
}
|
|
rateInput.disabled = false;
|
|
|
|
if (getRateButton && originalButtonText) {
|
|
getRateButton.disabled = false;
|
|
getRateButton.textContent = originalButtonText;
|
|
}
|
|
};
|
|
xhr.onerror = function(e) {
|
|
console.error('Network error when fetching rate data:', e);
|
|
rateInput.value = originalValue || '';
|
|
rateInput.disabled = false;
|
|
|
|
if (getRateButton && originalButtonText) {
|
|
getRateButton.disabled = false;
|
|
getRateButton.textContent = originalButtonText;
|
|
}
|
|
|
|
if (window.showErrorModal) {
|
|
window.showErrorModal('Network Connection Error', 'Unable to connect to the rate service.\n\nPlease check:\n• Your internet connection\n• BasicSwap server status\n• Firewall settings\n\nTry again once your connection is stable, or enter the rate manually.');
|
|
} else {
|
|
alert('Unable to connect to the rate service. Please check your network connection and try again.');
|
|
}
|
|
};
|
|
|
|
const params = `coin_from=${encodeURIComponent(coinFromSymbol)}&coin_to=${encodeURIComponent(coinToSymbol)}`;
|
|
debugLog('Sending rate request with params:', params);
|
|
xhr.send(params);
|
|
}
|
|
|
|
function setupRateButtons() {
|
|
const addGetRateButton = document.getElementById('add-get-rate-button');
|
|
if (addGetRateButton) {
|
|
addGetRateButton.addEventListener('click', function() {
|
|
const coinFromSelect = document.getElementById('add-amm-coin-from');
|
|
const coinToSelect = document.getElementById('add-amm-coin-to');
|
|
const rateInput = document.getElementById('add-amm-rate');
|
|
|
|
if (coinFromSelect && coinToSelect && rateInput) {
|
|
getRateFromCoinGecko(coinFromSelect, coinToSelect, rateInput);
|
|
} else {
|
|
console.error('Missing required elements for rate lookup');
|
|
}
|
|
});
|
|
}
|
|
|
|
const editGetRateButton = document.getElementById('edit-get-rate-button');
|
|
if (editGetRateButton) {
|
|
editGetRateButton.addEventListener('click', function() {
|
|
const coinFromSelect = document.getElementById('edit-amm-coin-from');
|
|
const coinToSelect = document.getElementById('edit-amm-coin-to');
|
|
const rateInput = document.getElementById('edit-amm-rate');
|
|
|
|
if (coinFromSelect && coinToSelect && rateInput) {
|
|
getRateFromCoinGecko(coinFromSelect, coinToSelect, rateInput);
|
|
} else {
|
|
console.error('Missing required elements for rate lookup');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
async function initialize(options = {}) {
|
|
Object.assign(config, options);
|
|
|
|
initializeTabs();
|
|
setupButtonHandlers();
|
|
initializeCustomSelects();
|
|
setupRateButtons();
|
|
|
|
await initializePrices();
|
|
|
|
if (refreshButton) {
|
|
refreshButton.addEventListener('click', async function() {
|
|
|
|
if (refreshButton.disabled) return;
|
|
|
|
const icon = refreshButton.querySelector('svg');
|
|
refreshButton.disabled = true;
|
|
|
|
if (icon) {
|
|
icon.classList.add('animate-spin');
|
|
}
|
|
|
|
try {
|
|
await initializePrices();
|
|
updateTables();
|
|
} finally {
|
|
setTimeout(() => {
|
|
if (icon) {
|
|
icon.classList.remove('animate-spin');
|
|
}
|
|
refreshButton.disabled = false;
|
|
}, 500);
|
|
}
|
|
});
|
|
}
|
|
|
|
setupConfigFormListener();
|
|
updateTables();
|
|
startRefreshTimer();
|
|
|
|
debugLog('AMM Tables Manager initialized');
|
|
|
|
return {
|
|
updateTables,
|
|
startRefreshTimer,
|
|
stopRefreshTimer,
|
|
refreshDropdownBalanceDisplay,
|
|
refreshOfferDropdownBalanceDisplay,
|
|
refreshBidDropdownBalanceDisplay,
|
|
refreshDropdownOptions
|
|
};
|
|
}
|
|
|
|
return {
|
|
initialize
|
|
};
|
|
})();
|
|
|
|
document.addEventListener('DOMContentLoaded', async function() {
|
|
if (typeof AmmTablesManager !== 'undefined') {
|
|
window.ammTablesManager = await AmmTablesManager.initialize();
|
|
}
|
|
});
|