mirror of
https://github.com/basicswap/basicswap.git
synced 2026-01-01 10:01:38 +01:00
609 lines
21 KiB
JavaScript
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);
|