const DOM = { get: (id) => document.getElementById(id), getValue: (id) => { const el = document.getElementById(id); return el ? el.value : ''; }, setValue: (id, value) => { const el = document.getElementById(id); if (el) el.value = value; }, addEvent: (id, event, handler) => { const el = document.getElementById(id); if (el) el.addEventListener(event, handler); }, query: (selector) => document.querySelector(selector), queryAll: (selector) => document.querySelectorAll(selector) }; const ErrorModal = { show: function(title, message) { const errorTitle = document.getElementById('errorTitle'); const errorMessage = document.getElementById('errorMessage'); const modal = document.getElementById('errorModal'); if (errorTitle) errorTitle.textContent = title || 'Error'; if (errorMessage) errorMessage.textContent = message || 'An error occurred'; if (modal) modal.classList.remove('hidden'); }, hide: function() { const modal = document.getElementById('errorModal'); if (modal) modal.classList.add('hidden'); }, init: function() { const errorOkBtn = document.getElementById('errorOk'); if (errorOkBtn) { errorOkBtn.addEventListener('click', this.hide.bind(this)); } } }; const Storage = { get: (key) => { try { return JSON.parse(localStorage.getItem(key)); } catch(e) { console.warn(`Failed to retrieve item from storage: ${key}`, e); return null; } }, set: (key, value) => { try { localStorage.setItem(key, JSON.stringify(value)); return true; } catch(e) { console.error(`Failed to save item to storage: ${key}`, e); return false; } }, setRaw: (key, value) => { try { localStorage.setItem(key, value); return true; } catch(e) { console.error(`Failed to save raw item to storage: ${key}`, e); return false; } }, getRaw: (key) => { try { return localStorage.getItem(key); } catch(e) { console.warn(`Failed to retrieve raw item from storage: ${key}`, e); return null; } } }; const Ajax = { post: (url, data, onSuccess, onError) => { const xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState !== XMLHttpRequest.DONE) return; if (xhr.status === 200) { if (onSuccess) { try { const response = xhr.responseText.startsWith('{') ? JSON.parse(xhr.responseText) : xhr.responseText; onSuccess(response); } catch (e) { console.error('Failed to parse response:', e); if (onError) onError('Invalid response format'); } } } else { console.error('Request failed:', xhr.statusText); if (onError) onError(xhr.statusText); } }; xhr.open('POST', url); xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); xhr.send(data); return xhr; } }; function handleNewOfferAddress() { const STORAGE_KEY = 'lastUsedAddressNewOffer'; const selectElement = DOM.query('select[name="addr_from"]'); const form = selectElement?.closest('form'); if (!selectElement || !form) return; function loadInitialAddress() { const savedAddress = Storage.get(STORAGE_KEY); if (savedAddress) { try { selectElement.value = savedAddress.value; } catch (e) { selectFirstAddress(); } } else { selectFirstAddress(); } } function selectFirstAddress() { if (selectElement.options.length > 1) { const firstOption = selectElement.options[1]; if (firstOption) { selectElement.value = firstOption.value; saveAddress(firstOption.value, firstOption.text); } } } function saveAddress(value, text) { Storage.set(STORAGE_KEY, { value, text }); } form.addEventListener('submit', () => { saveAddress(selectElement.value, selectElement.selectedOptions[0].text); }); selectElement.addEventListener('change', (event) => { saveAddress(event.target.value, event.target.selectedOptions[0].text); }); loadInitialAddress(); } const RateManager = { lookupRates: () => { const coinFrom = DOM.getValue('coin_from'); const coinTo = DOM.getValue('coin_to'); const ratesDisplay = DOM.get('rates_display'); if (!coinFrom || !coinTo || !ratesDisplay) { console.log('Required elements for lookup_rates not found'); return; } if (coinFrom === '-1' || coinTo === '-1') { alert('Coins from and to must be set first.'); return; } const selectedCoin = (coinFrom === '15') ? '3' : coinFrom; ratesDisplay.innerHTML = '
Updating...
'; const priceJsonElement = DOM.query(".pricejsonhidden"); if (priceJsonElement) { priceJsonElement.classList.remove("hidden"); } const params = 'coin_from=' + selectedCoin + '&coin_to=' + coinTo; Ajax.post('/json/rates', params, (response) => { if (ratesDisplay) { ratesDisplay.innerHTML = typeof response === 'string' ? response : '' + JSON.stringify(response, null, ' ') + '';
}
},
(error) => {
if (ratesDisplay) {
ratesDisplay.innerHTML = 'Error loading rates: ' + error + '
'; } } ); }, getRateInferred: (event) => { if (event) event.preventDefault(); const coinFrom = DOM.getValue('coin_from'); const coinTo = DOM.getValue('coin_to'); const rateElement = DOM.get('rate'); if (!coinFrom || !coinTo || !rateElement) { console.log('Required elements for getRateInferred not found'); return; } const params = 'coin_from=' + encodeURIComponent(coinFrom) + '&coin_to=' + encodeURIComponent(coinTo); DOM.setValue('rate', 'Loading...'); Ajax.post('/json/rates', params, (response) => { if (response.coingecko && response.coingecko.rate_inferred) { DOM.setValue('rate', response.coingecko.rate_inferred); RateManager.setRate('rate'); } else { DOM.setValue('rate', 'Error: No rate available'); console.error('Rate not available in response'); } }, (error) => { DOM.setValue('rate', 'Error: Rate lookup failed'); console.error('Error fetching rate data:', error); } ); }, setRate: (valueChanged) => { const elements = { coinFrom: DOM.get('coin_from'), coinTo: DOM.get('coin_to'), amtFrom: DOM.get('amt_from'), amtTo: DOM.get('amt_to'), rate: DOM.get('rate'), rateLock: DOM.get('rate_lock'), swapType: DOM.get('swap_type') }; if (!elements.coinFrom || !elements.coinTo || !elements.amtFrom || !elements.amtTo || !elements.rate) { console.log('Required elements for setRate not found'); return; } const values = { coinFrom: elements.coinFrom.value, coinTo: elements.coinTo.value, amtFrom: elements.amtFrom.value, amtTo: elements.amtTo.value, rate: elements.rate.value, lockRate: elements.rate.value == '' ? false : (elements.rateLock ? elements.rateLock.checked : false) }; if (valueChanged === 'coin_from' || valueChanged === 'coin_to') { DOM.setValue('rate', ''); return; } if (elements.swapType) { SwapTypeManager.setSwapTypeEnabled( values.coinFrom, values.coinTo, elements.swapType ); } if (values.coinFrom == '-1' || values.coinTo == '-1') { return; } let params = 'coin_from=' + values.coinFrom + '&coin_to=' + values.coinTo; if (valueChanged == 'rate' || (values.lockRate && valueChanged == 'amt_from') || (values.amtTo == '' && valueChanged == 'amt_from')) { if (values.rate == '' || (values.amtFrom == '' && values.amtTo == '')) { return; } else if (values.amtFrom == '' && values.amtTo != '') { if (valueChanged == 'amt_from') { return; } params += '&rate=' + values.rate + '&amt_to=' + values.amtTo; } else { params += '&rate=' + values.rate + '&amt_from=' + values.amtFrom; } } else if (values.lockRate && valueChanged == 'amt_to') { if (values.amtTo == '' || values.rate == '') { return; } params += '&amt_to=' + values.amtTo + '&rate=' + values.rate; } else { if (values.amtFrom == '' || values.amtTo == '') { return; } params += '&amt_from=' + values.amtFrom + '&amt_to=' + values.amtTo; } Ajax.post('/json/rate', params, (response) => { if (response.hasOwnProperty('rate')) { DOM.setValue('rate', response.rate); } else if (response.hasOwnProperty('amount_to')) { DOM.setValue('amt_to', response.amount_to); } else if (response.hasOwnProperty('amount_from')) { DOM.setValue('amt_from', response.amount_from); } }, (error) => { console.error('Rate calculation failed:', error); } ); } }; function set_rate(valueChanged) { RateManager.setRate(valueChanged); } function lookup_rates() { RateManager.lookupRates(); } function getRateInferred(event) { RateManager.getRateInferred(event); } const SwapTypeManager = { adaptor_sig_only_coins: ['6', '9', '8', '7', '13', '18', '17'], secret_hash_only_coins: ['11', '12'], setSwapTypeEnabled: (coinFrom, coinTo, swapTypeElement) => { if (!swapTypeElement) return; let makeHidden = false; coinFrom = String(coinFrom); coinTo = String(coinTo); if (SwapTypeManager.adaptor_sig_only_coins.includes(coinFrom) || SwapTypeManager.adaptor_sig_only_coins.includes(coinTo)) { swapTypeElement.disabled = true; swapTypeElement.value = 'xmr_swap'; makeHidden = true; swapTypeElement.classList.add('select-disabled'); } else if (SwapTypeManager.secret_hash_only_coins.includes(coinFrom) || SwapTypeManager.secret_hash_only_coins.includes(coinTo)) { swapTypeElement.disabled = true; swapTypeElement.value = 'seller_first'; makeHidden = true; swapTypeElement.classList.add('select-disabled'); } else { swapTypeElement.disabled = false; swapTypeElement.classList.remove('select-disabled'); if (['xmr_swap', 'seller_first'].includes(swapTypeElement.value) == false) { swapTypeElement.value = 'xmr_swap'; } } let swapTypeHidden = DOM.get('swap_type_hidden'); if (makeHidden) { if (!swapTypeHidden) { const form = DOM.get('form'); if (form) { swapTypeHidden = document.createElement('input'); swapTypeHidden.setAttribute('id', 'swap_type_hidden'); swapTypeHidden.setAttribute('type', 'hidden'); swapTypeHidden.setAttribute('name', 'swap_type'); form.appendChild(swapTypeHidden); } } if (swapTypeHidden) { swapTypeHidden.setAttribute('value', swapTypeElement.value); } } else if (swapTypeHidden) { swapTypeHidden.parentNode.removeChild(swapTypeHidden); } } }; const UIEnhancer = { handleErrorHighlighting: () => { const errMsgs = document.querySelectorAll('p.error_msg'); const errorFieldMap = { 'coin_to': ['coin_to', 'Coin To'], 'coin_from': ['Coin From'], 'amt_from': ['Amount From'], 'amt_to': ['Amount To'], 'amt_bid_min': ['Minimum Bid Amount'], 'Select coin you send': ['coin_from', 'parentNode'] }; errMsgs.forEach(errMsg => { const text = errMsg.innerText; Object.entries(errorFieldMap).forEach(([field, keywords]) => { if (keywords.some(keyword => text.includes(keyword))) { let element = DOM.get(field); if (field === 'Select coin you send' && element) { element = element.parentNode; } if (element) { element.classList.add('error'); } } }); }); document.querySelectorAll('input.error, select.error').forEach(element => { element.addEventListener('focus', event => { event.target.classList.remove('error'); }); }); }, updateDisabledStyles: () => { document.querySelectorAll('select.disabled-select').forEach(select => { if (select.disabled) { select.classList.add('disabled-select-enabled'); } else { select.classList.remove('disabled-select-enabled'); } }); document.querySelectorAll('input.disabled-input, input[type="checkbox"].disabled-input').forEach(input => { if (input.readOnly) { input.classList.add('disabled-input-enabled'); } else { input.classList.remove('disabled-input-enabled'); } }); }, setupCustomSelects: () => { const selectCache = {}; function updateSelectCache(select) { if (!select || !select.options || select.selectedIndex === undefined) return; const selectedOption = select.options[select.selectedIndex]; if (!selectedOption) return; const image = selectedOption.getAttribute('data-image'); const name = selectedOption.textContent.trim(); selectCache[select.id] = { image, name }; } function setSelectData(select) { if (!select || !select.options || select.selectedIndex === undefined) return; const selectedOption = select.options[select.selectedIndex]; if (!selectedOption) return; const image = selectedOption.getAttribute('data-image') || ''; const name = selectedOption.textContent.trim(); select.style.backgroundImage = image ? `url(${image}?${new Date().getTime()})` : ''; const selectImage = select.nextElementSibling?.querySelector('.select-image'); if (selectImage) { selectImage.src = image; } const selectNameElement = select.nextElementSibling?.querySelector('.select-name'); if (selectNameElement) { if (select.id === 'coin_from' && name.includes(' - Balance: ')) { const parts = name.split(' - Balance: '); const coinName = parts[0]; const balanceInfo = parts[1] || ''; selectNameElement.innerHTML = ''; selectNameElement.style.display = 'flex'; selectNameElement.style.flexDirection = 'column'; selectNameElement.style.alignItems = 'flex-start'; selectNameElement.style.lineHeight = '1.2'; const coinNameDiv = document.createElement('div'); coinNameDiv.textContent = coinName; coinNameDiv.style.fontWeight = 'normal'; coinNameDiv.style.color = 'inherit'; const balanceDiv = document.createElement('div'); balanceDiv.textContent = `Balance: ${balanceInfo}`; balanceDiv.style.fontSize = '0.75rem'; balanceDiv.style.color = '#6b7280'; balanceDiv.style.marginTop = '1px'; selectNameElement.appendChild(coinNameDiv); selectNameElement.appendChild(balanceDiv); } else { selectNameElement.textContent = name; selectNameElement.style.display = 'block'; selectNameElement.style.flexDirection = ''; selectNameElement.style.alignItems = ''; } } updateSelectCache(select); } function setupCustomSelect(select) { if (!select) return; const options = select.querySelectorAll('option'); const selectIcon = select.parentElement?.querySelector('.select-icon'); const selectImage = select.parentElement?.querySelector('.select-image'); if (!options || !selectIcon || !selectImage) return; options.forEach(option => { const image = option.getAttribute('data-image'); if (image) { option.style.backgroundImage = `url(${image})`; } }); const storedValue = Storage.getRaw(select.name); if (storedValue && select.value == '-1') { select.value = storedValue; } select.addEventListener('change', () => { setSelectData(select); Storage.setRaw(select.name, select.value); }); setSelectData(select); selectIcon.style.display = 'none'; selectImage.style.display = 'none'; } const selectIcons = document.querySelectorAll('.custom-select .select-icon'); const selectImages = document.querySelectorAll('.custom-select .select-image'); const selectNames = document.querySelectorAll('.custom-select .select-name'); selectIcons.forEach(icon => icon.style.display = 'none'); selectImages.forEach(image => image.style.display = 'none'); selectNames.forEach(name => name.style.display = 'none'); const customSelects = document.querySelectorAll('.custom-select select'); customSelects.forEach(setupCustomSelect); } }; function initializeApp() { handleNewOfferAddress(); DOM.addEvent('get_rate_inferred_button', 'click', RateManager.getRateInferred); const coinFrom = DOM.get('coin_from'); const coinTo = DOM.get('coin_to'); const swapType = DOM.get('swap_type'); if (coinFrom && coinTo && swapType) { SwapTypeManager.setSwapTypeEnabled(coinFrom.value, coinTo.value, swapType); coinFrom.addEventListener('change', function() { SwapTypeManager.setSwapTypeEnabled(this.value, coinTo.value, swapType); RateManager.setRate('coin_from'); }); coinTo.addEventListener('change', function() { SwapTypeManager.setSwapTypeEnabled(coinFrom.value, this.value, swapType); RateManager.setRate('coin_to'); }); } ['amt_from', 'amt_to', 'rate'].forEach(id => { DOM.addEvent(id, 'change', function() { RateManager.setRate(id); }); DOM.addEvent(id, 'input', function() { RateManager.setRate(id); }); }); DOM.addEvent('rate_lock', 'change', function() { if (DOM.getValue('rate')) { RateManager.setRate('rate'); } }); DOM.addEvent('lookup_rates_button', 'click', RateManager.lookupRates); UIEnhancer.handleErrorHighlighting(); UIEnhancer.updateDisabledStyles(); UIEnhancer.setupCustomSelects(); ErrorModal.init(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeApp); } else { initializeApp(); } window.showErrorModal = ErrorModal.show.bind(ErrorModal); window.hideErrorModal = ErrorModal.hide.bind(ErrorModal);