mirror of
https://github.com/basicswap/basicswap.git
synced 2026-04-15 21:27:23 +02:00
615 lines
25 KiB
JavaScript
615 lines
25 KiB
JavaScript
const BidPage = {
|
|
bidId: null,
|
|
bidStateInd: null,
|
|
createdAtTimestamp: null,
|
|
autoRefreshInterval: null,
|
|
elapsedTimeInterval: null,
|
|
AUTO_REFRESH_SECONDS: 60,
|
|
refreshPaused: false,
|
|
swapType: null,
|
|
coinFrom: null,
|
|
coinTo: null,
|
|
previousStateInd: null,
|
|
|
|
INACTIVE_STATES: [8, 17, 18, 19, 21, 22, 23, 25, 31], // Completed, Failed variants, Timed-out, Abandoned, Error, Rejected, Expired
|
|
|
|
DELAYING_STATE: 20,
|
|
|
|
STATE_TOOLTIPS: {
|
|
'Bid Sent': 'Your bid has been broadcast to the network',
|
|
'Bid Receiving': 'Receiving partial bid message from the network',
|
|
'Bid Received': 'Bid received and waiting for decision to accept or reject',
|
|
'Bid Receiving accept': 'Receiving acceptance message from the other party',
|
|
'Bid Accepted': 'Bid accepted. The atomic swap process is starting',
|
|
'Bid Initiated': 'Swap initiated. First lock transaction is being created',
|
|
'Bid Participating': 'Participating in the swap. Second lock transaction is being created',
|
|
'Bid Completed': 'Swap completed successfully! Both parties received their coins',
|
|
'Bid Script coin locked': null,
|
|
'Bid Script coin spend tx valid': null,
|
|
'Bid Scriptless coin locked': null,
|
|
'Bid Script coin lock released': 'Adaptor signature revealed. The script coin can now be claimed',
|
|
'Bid Script tx redeemed': null,
|
|
'Bid Script pre-refund tx in chain': 'Pre-refund transaction detected. Swap may be failing',
|
|
'Bid Scriptless tx redeemed': null,
|
|
'Bid Scriptless tx recovered': null,
|
|
'Bid Failed, refunded': 'Swap failed but your coins have been refunded',
|
|
'Bid Failed, swiped': 'Swap failed due to an unexpected issue. Please check the event log for details',
|
|
'Bid Failed': 'Swap failed. Check events for details',
|
|
'Bid Delaying': 'Brief delay between swap steps to ensure network propagation',
|
|
'Bid Timed-out': 'Swap timed out waiting for the other party',
|
|
'Bid Abandoned': 'Swap was manually abandoned. Locked coins will be refunded after timelock',
|
|
'Bid Error': 'An error occurred. Check events for details',
|
|
'Bid Rejected': 'Bid was rejected by the offer owner',
|
|
'Bid Stalled (debug)': 'Debug mode: swap intentionally stalled for testing',
|
|
'Bid Exchanged script lock tx sigs msg': 'Exchanging adaptor signatures needed for lock transactions',
|
|
'Bid Exchanged script lock spend tx msg': 'Exchanging signed spend transaction for locked coins',
|
|
'Bid Request sent': 'Connection request sent to the other party',
|
|
'Bid Request accepted': 'Connection request accepted',
|
|
'Bid Expired': 'Bid expired before being accepted',
|
|
'Bid Auto accept delay': 'Waiting for automation delay before auto-accepting',
|
|
'Bid Auto accept failed': 'Automation failed to accept this bid',
|
|
'Bid Connect request sent': 'Sent connection request to peer',
|
|
'Bid Unknown bid state': 'Unknown state - please check the swap details',
|
|
|
|
'ITX Sent': 'Initiate transaction has been broadcast to the network',
|
|
'ITX Confirmed': 'Initiate transaction has been confirmed by miners',
|
|
'ITX Redeemed': 'Initiate transaction has been successfully claimed',
|
|
'ITX Refunded': 'Initiate transaction has been refunded',
|
|
'ITX In Mempool': 'Initiate transaction is in the mempool (unconfirmed)',
|
|
'ITX In Chain': 'Initiate transaction is included in a block',
|
|
'PTX Sent': 'Participate transaction has been broadcast to the network',
|
|
'PTX Confirmed': 'Participate transaction has been confirmed by miners',
|
|
'PTX Redeemed': 'Participate transaction has been successfully claimed',
|
|
'PTX Refunded': 'Participate transaction has been refunded',
|
|
'PTX In Mempool': 'Participate transaction is in the mempool (unconfirmed)',
|
|
'PTX In Chain': 'Participate transaction is included in a block'
|
|
},
|
|
|
|
getStateTooltip: function(stateText) {
|
|
const staticTooltip = this.STATE_TOOLTIPS[stateText];
|
|
if (staticTooltip !== null && staticTooltip !== undefined) {
|
|
return staticTooltip;
|
|
}
|
|
|
|
const scriptlessCoins = ['XMR', 'WOW'];
|
|
let scriptCoin, scriptlessCoin;
|
|
|
|
if (scriptlessCoins.includes(this.coinFrom)) {
|
|
scriptlessCoin = this.coinFrom;
|
|
scriptCoin = this.coinTo;
|
|
} else if (scriptlessCoins.includes(this.coinTo)) {
|
|
scriptlessCoin = this.coinTo;
|
|
scriptCoin = this.coinFrom;
|
|
} else {
|
|
scriptCoin = this.coinFrom;
|
|
scriptlessCoin = this.coinTo;
|
|
}
|
|
|
|
const dynamicTooltips = {
|
|
'Bid Script coin locked': `${scriptCoin} is locked in the atomic swap contract`,
|
|
'Bid Script coin spend tx valid': `The ${scriptCoin} spend transaction has been validated and is ready`,
|
|
'Bid Scriptless coin locked': `${scriptlessCoin} is locked using adaptor signatures`,
|
|
'Bid Script tx redeemed': `${scriptCoin} has been successfully claimed`,
|
|
'Bid Scriptless tx redeemed': `${scriptlessCoin} has been successfully claimed`,
|
|
'Bid Scriptless tx recovered': `${scriptlessCoin} recovered after swap failure`,
|
|
};
|
|
|
|
return dynamicTooltips[stateText] || null;
|
|
},
|
|
|
|
EVENT_TOOLTIPS: {
|
|
'Lock tx A published': 'First lock transaction broadcast to the blockchain network',
|
|
'Lock tx A seen in mempool': 'First lock transaction detected in mempool (unconfirmed)',
|
|
'Lock tx A seen in chain': 'First lock transaction included in a block',
|
|
'Lock tx A confirmed in chain': 'First lock transaction has enough confirmations',
|
|
'Lock tx B published': 'Second lock transaction broadcast to the blockchain network',
|
|
'Lock tx B seen in mempool': 'Second lock transaction detected in mempool (unconfirmed)',
|
|
'Lock tx B seen in chain': 'Second lock transaction included in a block',
|
|
'Lock tx B confirmed in chain': 'Second lock transaction has enough confirmations',
|
|
'Lock tx A spend tx published': 'Transaction to claim coins from first lock has been broadcast',
|
|
'Lock tx A spend tx seen in chain': 'First lock spend transaction included in a block',
|
|
'Lock tx B spend tx published': 'Transaction to claim coins from second lock has been broadcast',
|
|
'Lock tx B spend tx seen in chain': 'Second lock spend transaction included in a block',
|
|
'Failed to publish lock tx B': 'ERROR: Could not broadcast second lock transaction',
|
|
'Failed to publish lock tx B spend': 'ERROR: Could not broadcast spend transaction for second lock',
|
|
'Failed to publish lock tx B refund': 'ERROR: Could not broadcast refund transaction',
|
|
'Detected invalid lock Tx B': 'ERROR: Second lock transaction is invalid or malformed',
|
|
'Lock tx A pre-refund tx published': 'Pre-refund transaction broadcast. Swap is being cancelled',
|
|
'Lock tx A refund spend tx published': 'Refund transaction for first lock has been broadcast',
|
|
'Lock tx A refund swipe tx published': 'Other party claimed your refund (swiped)',
|
|
'Lock tx B refund tx published': 'Refund transaction for second lock has been broadcast',
|
|
'Lock tx A conflicting txn/s': 'WARNING: Conflicting transaction detected for first lock',
|
|
'Lock tx A pre-refund tx seen in chain': 'Pre-refund transaction detected in blockchain',
|
|
'Lock tx A refund spend tx seen in chain': 'Refund spend transaction detected in blockchain',
|
|
'Initiate tx published': 'Secret-hash swap: Initiate transaction broadcast',
|
|
'Initiate tx redeem tx published': 'Secret-hash swap: Initiate transaction claimed',
|
|
'Initiate tx refund tx published': 'Secret-hash swap: Initiate transaction refunded',
|
|
'Participate tx published': 'Secret-hash swap: Participate transaction broadcast',
|
|
'Participate tx redeem tx published': 'Secret-hash swap: Participate transaction claimed',
|
|
'Participate tx refund tx published': 'Secret-hash swap: Participate transaction refunded',
|
|
'BCH mercy tx found': 'BCH specific: Mercy transaction detected',
|
|
'Lock tx B mercy tx published': 'BCH specific: Mercy transaction broadcast',
|
|
'Auto accepting': 'Automation is accepting this bid',
|
|
'Failed auto accepting': 'Automation constraints prevented accepting this bid',
|
|
'Debug tweak applied': 'Debug mode: A test tweak was applied'
|
|
},
|
|
|
|
STATE_PHASES: {
|
|
1: { phase: 'negotiation', order: 1, label: 'Negotiation' }, // BID_SENT
|
|
2: { phase: 'negotiation', order: 2, label: 'Negotiation' }, // BID_RECEIVING
|
|
3: { phase: 'negotiation', order: 3, label: 'Negotiation' }, // BID_RECEIVED
|
|
4: { phase: 'negotiation', order: 4, label: 'Negotiation' }, // BID_RECEIVING_ACC
|
|
5: { phase: 'accepted', order: 5, label: 'Accepted' }, // BID_ACCEPTED
|
|
6: { phase: 'locking', order: 6, label: 'Locking' }, // SWAP_INITIATED
|
|
7: { phase: 'locking', order: 7, label: 'Locking' }, // SWAP_PARTICIPATING
|
|
8: { phase: 'complete', order: 100, label: 'Complete' }, // SWAP_COMPLETED
|
|
9: { phase: 'locking', order: 8, label: 'Locking' }, // XMR_SWAP_SCRIPT_COIN_LOCKED
|
|
10: { phase: 'locking', order: 9, label: 'Locking' }, // XMR_SWAP_HAVE_SCRIPT_COIN_SPEND_TX
|
|
11: { phase: 'locking', order: 10, label: 'Locking' }, // XMR_SWAP_NOSCRIPT_COIN_LOCKED
|
|
12: { phase: 'redemption', order: 11, label: 'Redemption' }, // XMR_SWAP_LOCK_RELEASED
|
|
13: { phase: 'redemption', order: 12, label: 'Redemption' }, // XMR_SWAP_SCRIPT_TX_REDEEMED
|
|
14: { phase: 'failed', order: 90, label: 'Failed' }, // XMR_SWAP_SCRIPT_TX_PREREFUND
|
|
15: { phase: 'redemption', order: 13, label: 'Redemption' }, // XMR_SWAP_NOSCRIPT_TX_REDEEMED
|
|
16: { phase: 'failed', order: 91, label: 'Recovered' }, // XMR_SWAP_NOSCRIPT_TX_RECOVERED
|
|
17: { phase: 'failed', order: 92, label: 'Failed' }, // XMR_SWAP_FAILED_REFUNDED
|
|
18: { phase: 'failed', order: 93, label: 'Failed' }, // XMR_SWAP_FAILED_SWIPED
|
|
19: { phase: 'failed', order: 94, label: 'Failed' }, // XMR_SWAP_FAILED
|
|
20: { phase: 'locking', order: 7.5, label: 'Locking' }, // SWAP_DELAYING
|
|
21: { phase: 'failed', order: 95, label: 'Failed' }, // SWAP_TIMEDOUT
|
|
22: { phase: 'failed', order: 96, label: 'Abandoned' }, // BID_ABANDONED
|
|
23: { phase: 'failed', order: 97, label: 'Error' }, // BID_ERROR
|
|
25: { phase: 'failed', order: 98, label: 'Rejected' }, // BID_REJECTED
|
|
27: { phase: 'accepted', order: 5.5, label: 'Accepted' }, // XMR_SWAP_MSG_SCRIPT_LOCK_TX_SIGS
|
|
28: { phase: 'accepted', order: 5.6, label: 'Accepted' }, // XMR_SWAP_MSG_SCRIPT_LOCK_SPEND_TX
|
|
29: { phase: 'negotiation', order: 0.5, label: 'Negotiation' }, // BID_REQUEST_SENT
|
|
30: { phase: 'negotiation', order: 0.6, label: 'Negotiation' }, // BID_REQUEST_ACCEPTED
|
|
31: { phase: 'failed', order: 99, label: 'Expired' }, // BID_EXPIRED
|
|
32: { phase: 'negotiation', order: 3.5, label: 'Negotiation' }, // BID_AACCEPT_DELAY
|
|
33: { phase: 'failed', order: 89, label: 'Failed' }, // BID_AACCEPT_FAIL
|
|
34: { phase: 'negotiation', order: 0.4, label: 'Negotiation' } // CONNECT_REQ_SENT
|
|
},
|
|
|
|
init: function(bidId, bidStateInd, createdAtTimestamp, stateTimeTimestamp, options) {
|
|
this.bidId = bidId;
|
|
this.bidStateInd = bidStateInd;
|
|
this.createdAtTimestamp = createdAtTimestamp;
|
|
this.stateTimeTimestamp = stateTimeTimestamp;
|
|
this.tooltipCounter = 0;
|
|
|
|
options = options || {};
|
|
this.swapType = options.swapType || 'secret-hash';
|
|
this.coinFrom = options.coinFrom || '';
|
|
this.coinTo = options.coinTo || '';
|
|
|
|
if (this.bidStateInd === this.DELAYING_STATE) {
|
|
this.previousStateInd = this.findPreviousState();
|
|
}
|
|
|
|
this.applyStateTooltips();
|
|
this.applyEventTooltips();
|
|
this.createProgressBar();
|
|
this.startElapsedTimeUpdater();
|
|
this.setupAutoRefresh();
|
|
},
|
|
|
|
findPreviousState: function() {
|
|
const sections = document.querySelectorAll('section');
|
|
let oldStatesSection = null;
|
|
|
|
sections.forEach(section => {
|
|
const h4 = section.querySelector('h4');
|
|
if (h4 && h4.textContent.includes('Old states')) {
|
|
oldStatesSection = section.nextElementSibling;
|
|
}
|
|
});
|
|
|
|
if (oldStatesSection) {
|
|
const table = oldStatesSection.querySelector('table');
|
|
if (table) {
|
|
const rows = table.querySelectorAll('tr');
|
|
|
|
for (let i = rows.length - 1; i >= 0; i--) {
|
|
const cells = rows[i].querySelectorAll('td');
|
|
if (cells.length >= 2) {
|
|
const stateText = cells[cells.length - 1].textContent.trim();
|
|
if (!stateText.includes('Delaying')) {
|
|
return this.stateTextToIndex(stateText);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
stateTextToIndex: function(stateText) {
|
|
const stateMap = {
|
|
'Sent': 1, 'Receiving': 2, 'Received': 3, 'Receiving accept': 4,
|
|
'Accepted': 5, 'Initiated': 6, 'Participating': 7, 'Completed': 8,
|
|
'Script coin locked': 9, 'Script coin spend tx valid': 10,
|
|
'Scriptless coin locked': 11, 'Script coin lock released': 12,
|
|
'Script tx redeemed': 13, 'Script pre-refund tx in chain': 14,
|
|
'Scriptless tx redeemed': 15, 'Scriptless tx recovered': 16,
|
|
'Failed, refunded': 17, 'Failed, swiped': 18, 'Failed': 19,
|
|
'Delaying': 20, 'Timed-out': 21, 'Abandoned': 22, 'Error': 23,
|
|
'Rejected': 25, 'Exchanged script lock tx sigs msg': 27,
|
|
'Exchanged script lock spend tx msg': 28, 'Request sent': 29,
|
|
'Request accepted': 30, 'Expired': 31
|
|
};
|
|
|
|
for (const [key, value] of Object.entries(stateMap)) {
|
|
if (stateText.includes(key)) {
|
|
return value;
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
isActiveState: function() {
|
|
return !this.INACTIVE_STATES.includes(this.bidStateInd);
|
|
},
|
|
|
|
setupAutoRefresh: function() {
|
|
const refreshBtn = document.getElementById('refresh');
|
|
if (!refreshBtn) return;
|
|
|
|
if (!this.isActiveState()) {
|
|
refreshBtn.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
const originalSpan = refreshBtn.querySelector('span');
|
|
if (!originalSpan) return;
|
|
|
|
let countdown = this.AUTO_REFRESH_SECONDS;
|
|
let isRefreshing = false;
|
|
let isPersistentlyPaused = false;
|
|
|
|
const updateCountdown = () => {
|
|
if (this.refreshPaused || isPersistentlyPaused || isRefreshing) return;
|
|
|
|
originalSpan.textContent = `Auto-refresh in ${countdown}s`;
|
|
countdown--;
|
|
|
|
if (countdown < 0 && !isRefreshing) {
|
|
isRefreshing = true;
|
|
if (this.autoRefreshInterval) {
|
|
clearInterval(this.autoRefreshInterval);
|
|
this.autoRefreshInterval = null;
|
|
}
|
|
window.location.href = window.location.pathname + window.location.search;
|
|
}
|
|
};
|
|
|
|
updateCountdown();
|
|
this.autoRefreshInterval = setInterval(updateCountdown, 1000);
|
|
|
|
refreshBtn.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
|
|
if (isPersistentlyPaused) {
|
|
window.location.href = window.location.pathname + window.location.search;
|
|
} else {
|
|
isPersistentlyPaused = true;
|
|
if (this.autoRefreshInterval) {
|
|
clearInterval(this.autoRefreshInterval);
|
|
this.autoRefreshInterval = null;
|
|
}
|
|
originalSpan.textContent = 'Paused (click to refresh)';
|
|
}
|
|
});
|
|
|
|
refreshBtn.addEventListener('mouseenter', () => {
|
|
if (!isPersistentlyPaused) {
|
|
this.refreshPaused = true;
|
|
if (this.autoRefreshInterval) {
|
|
clearInterval(this.autoRefreshInterval);
|
|
this.autoRefreshInterval = null;
|
|
}
|
|
originalSpan.textContent = 'Click to pause';
|
|
}
|
|
});
|
|
|
|
refreshBtn.addEventListener('mouseleave', () => {
|
|
if (!isPersistentlyPaused) {
|
|
this.refreshPaused = false;
|
|
countdown = this.AUTO_REFRESH_SECONDS;
|
|
if (!this.autoRefreshInterval) {
|
|
updateCountdown();
|
|
this.autoRefreshInterval = setInterval(updateCountdown, 1000);
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
createTooltip: function(element, tooltipText) {
|
|
if (window.TooltipManager && typeof window.TooltipManager.create === 'function') {
|
|
try {
|
|
const tooltipContent = `
|
|
<div class="py-1 px-2 text-sm text-white">
|
|
${tooltipText}
|
|
</div>
|
|
`;
|
|
window.TooltipManager.create(element, tooltipContent, {
|
|
placement: 'top'
|
|
});
|
|
element.classList.add('cursor-help');
|
|
} catch (e) {
|
|
element.setAttribute('title', tooltipText);
|
|
element.classList.add('cursor-help');
|
|
}
|
|
} else {
|
|
element.setAttribute('title', tooltipText);
|
|
element.classList.add('cursor-help');
|
|
}
|
|
},
|
|
|
|
applyStateTooltips: function() {
|
|
const sections = document.querySelectorAll('section');
|
|
let oldStatesSection = null;
|
|
|
|
sections.forEach(section => {
|
|
const h4 = section.querySelector('h4');
|
|
if (h4 && h4.textContent.includes('Old states')) {
|
|
oldStatesSection = section.nextElementSibling;
|
|
}
|
|
});
|
|
|
|
if (oldStatesSection) {
|
|
const table = oldStatesSection.querySelector('table');
|
|
if (table) {
|
|
const rows = table.querySelectorAll('tr');
|
|
rows.forEach(row => {
|
|
const cells = row.querySelectorAll('td');
|
|
if (cells.length >= 2) {
|
|
const stateCell = cells[cells.length - 1];
|
|
const stateText = stateCell.textContent.trim();
|
|
const tooltip = this.getStateTooltip(stateText) || this.getStateTooltip('Bid ' + stateText);
|
|
if (tooltip) {
|
|
this.addHelpIcon(stateCell, tooltip);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
const allRows = document.querySelectorAll('table tr');
|
|
allRows.forEach(row => {
|
|
const firstCell = row.querySelector('td');
|
|
if (firstCell) {
|
|
const labelText = firstCell.textContent.trim();
|
|
if (labelText === 'Bid State') {
|
|
const valueCell = row.querySelectorAll('td')[1];
|
|
if (valueCell) {
|
|
const stateText = valueCell.textContent.trim();
|
|
const tooltip = this.getStateTooltip(stateText) || this.getStateTooltip('Bid ' + stateText);
|
|
if (tooltip) {
|
|
this.addHelpIcon(valueCell, tooltip);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
addHelpIcon: function(cell, tooltipText) {
|
|
if (cell.querySelector('.help-icon')) return;
|
|
|
|
const helpIcon = document.createElement('span');
|
|
helpIcon.className = 'help-icon cursor-help inline-flex items-center justify-center w-4 h-4 ml-2 text-xs font-medium text-white bg-blue-500 dark:bg-blue-600 rounded-full hover:bg-blue-600 dark:hover:bg-blue-500';
|
|
helpIcon.textContent = '?';
|
|
helpIcon.style.fontSize = '10px';
|
|
helpIcon.style.verticalAlign = 'middle';
|
|
helpIcon.style.flexShrink = '0';
|
|
|
|
cell.appendChild(helpIcon);
|
|
|
|
setTimeout(() => {
|
|
this.createTooltip(helpIcon, tooltipText);
|
|
}, 50);
|
|
},
|
|
|
|
applyEventTooltips: function() {
|
|
const sections = document.querySelectorAll('section');
|
|
let eventsSection = null;
|
|
|
|
sections.forEach(section => {
|
|
const h4 = section.querySelector('h4');
|
|
if (h4 && h4.textContent.includes('Events')) {
|
|
eventsSection = section.nextElementSibling;
|
|
}
|
|
});
|
|
|
|
if (eventsSection) {
|
|
const table = eventsSection.querySelector('table');
|
|
if (table) {
|
|
const rows = table.querySelectorAll('tr');
|
|
rows.forEach(row => {
|
|
const cells = row.querySelectorAll('td');
|
|
if (cells.length >= 2) {
|
|
const eventCell = cells[cells.length - 1];
|
|
const eventText = eventCell.textContent.trim();
|
|
|
|
let tooltip = this.EVENT_TOOLTIPS[eventText];
|
|
|
|
if (!tooltip) {
|
|
for (const [key, value] of Object.entries(this.EVENT_TOOLTIPS)) {
|
|
if (eventText.startsWith(key.replace(':', ''))) {
|
|
tooltip = value;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!tooltip && eventText.startsWith('Warning:')) {
|
|
tooltip = 'System warning - check message for details';
|
|
}
|
|
if (!tooltip && eventText.startsWith('Error:')) {
|
|
tooltip = 'Error occurred - check message for details';
|
|
}
|
|
if (!tooltip && eventText.startsWith('Temporary RPC error')) {
|
|
tooltip = 'Temporary error checking transaction. Will retry automatically';
|
|
}
|
|
|
|
if (tooltip) {
|
|
this.addHelpIcon(eventCell, tooltip);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
},
|
|
|
|
createProgressBar: function() {
|
|
let stateForProgress = this.bidStateInd;
|
|
let isDelaying = false;
|
|
|
|
if (this.bidStateInd === this.DELAYING_STATE && this.previousStateInd) {
|
|
stateForProgress = this.previousStateInd;
|
|
isDelaying = true;
|
|
}
|
|
|
|
const phaseInfo = this.STATE_PHASES[stateForProgress];
|
|
if (!phaseInfo) return;
|
|
|
|
let progressPercent = 0;
|
|
const phase = phaseInfo.phase;
|
|
|
|
if (phase === 'negotiation') progressPercent = 15;
|
|
else if (phase === 'accepted') progressPercent = 30;
|
|
else if (phase === 'locking') progressPercent = 55;
|
|
else if (phase === 'redemption') progressPercent = 80;
|
|
else if (phase === 'complete') progressPercent = 100;
|
|
else if (phase === 'failed' || phase === 'error') progressPercent = 100;
|
|
|
|
const bidStateRow = document.querySelector('td.bold');
|
|
if (!bidStateRow) return;
|
|
|
|
let targetRow = null;
|
|
const rows = document.querySelectorAll('table tr');
|
|
rows.forEach(row => {
|
|
const firstTd = row.querySelector('td.bold');
|
|
if (firstTd && firstTd.textContent.trim() === 'Bid State') {
|
|
targetRow = row;
|
|
}
|
|
});
|
|
|
|
if (!targetRow) return;
|
|
|
|
const progressRow = document.createElement('tr');
|
|
progressRow.className = 'opacity-100 text-gray-500 dark:text-gray-100';
|
|
|
|
const isError = ['failed', 'error'].includes(phase);
|
|
const isComplete = phase === 'complete';
|
|
const barColor = isError ? 'bg-red-500' : (isComplete ? 'bg-green-500' : 'bg-blue-500');
|
|
|
|
let phaseLabel;
|
|
if (isError) {
|
|
phaseLabel = phaseInfo.label;
|
|
} else if (isComplete) {
|
|
phaseLabel = 'Complete';
|
|
} else if (isDelaying) {
|
|
phaseLabel = `${phaseInfo.label} (${progressPercent}%) - Delaying`;
|
|
} else {
|
|
phaseLabel = `${phaseInfo.label} (${progressPercent}%)`;
|
|
}
|
|
|
|
progressRow.innerHTML = `
|
|
<td class="py-3 px-6 bold">Swap Progress</td>
|
|
<td class="py-3 px-6">
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex-1 bg-gray-200 dark:bg-gray-600 rounded-full h-2.5 max-w-xs">
|
|
<div class="${barColor} h-2.5 rounded-full transition-all duration-500" style="width: ${progressPercent}%"></div>
|
|
</div>
|
|
<span class="text-sm font-medium text-gray-900 dark:text-white">${phaseLabel}</span>
|
|
</div>
|
|
</td>
|
|
`;
|
|
|
|
targetRow.parentNode.insertBefore(progressRow, targetRow.nextSibling);
|
|
},
|
|
|
|
startElapsedTimeUpdater: function() {
|
|
if (!this.createdAtTimestamp) return;
|
|
|
|
let createdAtRow = null;
|
|
const rows = document.querySelectorAll('table tr');
|
|
rows.forEach(row => {
|
|
const firstTd = row.querySelector('td');
|
|
if (firstTd && firstTd.textContent.includes('Created At')) {
|
|
createdAtRow = row;
|
|
}
|
|
});
|
|
|
|
if (!createdAtRow) return;
|
|
|
|
const isCompleted = !this.isActiveState() && this.stateTimeTimestamp;
|
|
|
|
const elapsedRow = document.createElement('tr');
|
|
elapsedRow.className = 'opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600';
|
|
|
|
const labelText = isCompleted ? 'Swap Duration' : 'Time Elapsed';
|
|
const iconColor = isCompleted ? '#10B981' : '#3B82F6';
|
|
|
|
elapsedRow.innerHTML = `
|
|
<td class="flex items-center px-46 whitespace-nowrap">
|
|
<svg alt="" class="w-5 h-5 rounded-full ml-5" xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
|
|
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="${iconColor}" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="11"></circle>
|
|
<polyline points="12,6 12,12 18,12" stroke="${iconColor}"></polyline>
|
|
</g>
|
|
</svg>
|
|
<div class="py-3 pl-2 bold">
|
|
<div>${labelText}</div>
|
|
</div>
|
|
</td>
|
|
<td class="py-3 px-6" id="elapsed-time-display">Calculating...</td>
|
|
`;
|
|
createdAtRow.parentNode.insertBefore(elapsedRow, createdAtRow.nextSibling);
|
|
|
|
const elapsedDisplay = document.getElementById('elapsed-time-display');
|
|
|
|
if (isCompleted) {
|
|
const duration = this.stateTimeTimestamp - this.createdAtTimestamp;
|
|
elapsedDisplay.textContent = this.formatDuration(duration);
|
|
} else {
|
|
const updateElapsed = () => {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const elapsed = now - this.createdAtTimestamp;
|
|
elapsedDisplay.textContent = this.formatDuration(elapsed);
|
|
};
|
|
|
|
updateElapsed();
|
|
this.elapsedTimeInterval = setInterval(updateElapsed, 1000);
|
|
}
|
|
},
|
|
|
|
formatDuration: function(seconds) {
|
|
if (seconds < 60) {
|
|
return `${seconds} second${seconds !== 1 ? 's' : ''}`;
|
|
}
|
|
const minutes = Math.floor(seconds / 60);
|
|
if (minutes < 60) {
|
|
const remainingSeconds = seconds % 60;
|
|
if (remainingSeconds > 0) {
|
|
return `${minutes} min ${remainingSeconds} sec`;
|
|
}
|
|
return `${minutes} minute${minutes !== 1 ? 's' : ''}`;
|
|
}
|
|
const hours = Math.floor(minutes / 60);
|
|
const remainingMinutes = minutes % 60;
|
|
if (hours < 24) {
|
|
if (remainingMinutes > 0) {
|
|
return `${hours} hr ${remainingMinutes} min`;
|
|
}
|
|
return `${hours} hour${hours !== 1 ? 's' : ''}`;
|
|
}
|
|
const days = Math.floor(hours / 24);
|
|
const remainingHours = hours % 24;
|
|
if (remainingHours > 0) {
|
|
return `${days} day${days !== 1 ? 's' : ''} ${remainingHours} hr`;
|
|
}
|
|
return `${days} day${days !== 1 ? 's' : ''}`;
|
|
}
|
|
};
|