Files
basicswap/basicswap/static/js/pages/offer-new-page.js
2025-10-10 11:08:23 +02:00

609 lines
21 KiB
JavaScript

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 = '<p>Updating...</p>';
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 : '<pre><code>' + JSON.stringify(response, null, ' ') + '</code></pre>';
}
},
(error) => {
if (ratesDisplay) {
ratesDisplay.innerHTML = '<p>Error loading rates: ' + error + '</p>';
}
}
);
},
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);