diff --git a/api1.js b/api1.js index e9b69a3..a5fd28a 100644 --- a/api1.js +++ b/api1.js @@ -1453,7 +1453,7 @@ function registerCoinswapHandlers() { // Start coinswap ipcMain.handle( 'coinswap:start', - async (event, { amount, makerCount, outpoints, password }) => { + async (event, { amount, makerCount, outpoints, password, selectedMakerAddresses }) => { try { if (!api1State.takerInstance) { return { success: false, error: 'Taker not initialized' }; @@ -1464,6 +1464,14 @@ function registerCoinswapHandlers() { return { success: false, error: 'Invalid amount' }; } + if ( + selectedMakerAddresses != null && + (!Array.isArray(selectedMakerAddresses) || + selectedMakerAddresses.some((a) => typeof a !== 'string' || !a.trim())) + ) { + return { success: false, error: 'Invalid selectedMakerAddresses: must be an array of non-empty strings' }; + } + const protocol = api1State.protocolVersion || 'v1'; const protocolName = protocol === 'v2' ? 'Taproot' : 'P2WSH'; const swapId = `swap_${Date.now()}_${Math.random().toString(36).substring(7)}`; @@ -1571,7 +1579,7 @@ function registerCoinswapHandlers() { }); const worker = new Worker(path.join(__dirname, 'coinswap-worker.js'), { - workerData: { amount, makerCount, outpoints, config }, + workerData: { amount, makerCount, outpoints, selectedMakerAddresses, config }, }); api1State.activeSwaps.set(swapId, { diff --git a/coinswap-worker.js b/coinswap-worker.js index 6c59af9..64aefa2 100644 --- a/coinswap-worker.js +++ b/coinswap-worker.js @@ -10,7 +10,7 @@ const { parentPort, workerData } = require('worker_threads'); try { const coinswapNapi = require('coinswap-napi'); - const { amount, makerCount, outpoints, config } = workerData; + const { amount, makerCount, outpoints, selectedMakerAddresses, config } = workerData; const protocol = config.protocol || 'v1'; const normalizedProtocol = protocol === 'v2' ? 'Taproot' : 'Legacy'; const protocolName = @@ -58,6 +58,7 @@ const { parentPort, workerData } = require('worker_threads'); sendAmount: amount, makerCount: makerCount, manuallySelectedOutpoints: outpoints || undefined, + preferredMakers: selectedMakerAddresses && selectedMakerAddresses.length > 0 ? selectedMakerAddresses : undefined, }; console.log(`🔄 Syncing offerbook in swap worker before prepare...`); diff --git a/src/components/market/Market.js b/src/components/market/Market.js index 30e93ec..316bb9c 100644 --- a/src/components/market/Market.js +++ b/src/components/market/Market.js @@ -38,20 +38,14 @@ export function Market(container) { } } - function formatTorEndpoint(address, start = 6, end = 0) { + function formatTorEndpoint(address, start = 8, end = 6) { if (!address || typeof address !== 'string') return 'unknown'; const separatorIndex = address.lastIndexOf(':'); - if (separatorIndex === -1) return address; + const host = (separatorIndex !== -1 ? address.slice(0, separatorIndex) : address).replace(/\.onion$/i, ''); - const host = address.slice(0, separatorIndex).replace(/\.onion$/i, ''); - const port = address.slice(separatorIndex + 1); - - if (host.length <= start + end + 3) { - return `${host}:${port}`; - } - - return end > 0 ? `${host.slice(0, start)}..${host.slice(-end)}:${port}` : `${host.slice(0, start)}..:${port}`; + if (host.length <= start + end + 3) return host; + return `${host.slice(0, start)}...${host.slice(-end)}`; } // Check sync state every second @@ -92,12 +86,11 @@ export function Market(container) { const addr = item.address; let fullAddress; if (typeof addr === 'string') { - fullAddress = addr.includes(':') ? addr : `${addr}:6102`; + fullAddress = addr; } else { - const addressObj = addr || {}; - const onionAddr = addressObj.onion_addr || ''; - const port = addressObj.port || '6102'; - fullAddress = `${onionAddr}:${port}`; + const host = addr?.onion_addr || ''; + const portSuffix = addr?.port ? `:${addr.port}` : ''; + fullAddress = host || portSuffix ? `${host}${portSuffix}` : ''; } // Handle null offers (unresponsive makers) @@ -770,7 +763,7 @@ export function Market(container) { .map( (maker) => { return ` -
@@ -485,7 +485,7 @@ export function FirstTimeSetupModal(container, onComplete) {
-@@ -986,6 +986,9 @@ export function FirstTimeSetupModal(container, onComplete) { ]; renderConnectionResults(resultDiv, results); + const nodeFailed = results.some((r) => !r.ok); + const infoDiv = modal.querySelector('#node-setup-info'); + if (infoDiv) infoDiv.classList.toggle('hidden', !nodeFailed); } catch (error) { console.error('RPC test failed:', error); @@ -1000,6 +1003,8 @@ export function FirstTimeSetupModal(container, onComplete) { : error.message, }, ]); + const infoDiv = modal.querySelector('#node-setup-info'); + if (infoDiv) infoDiv.classList.remove('hidden'); } btn.textContent = originalText; @@ -1090,6 +1095,7 @@ export function FirstTimeSetupModal(container, onComplete) { window.api.testTcpPort({ host: '127.0.0.1', port: controlPort }), ]); + const torFailed = !socksResult?.success || !controlResult?.success; renderConnectionResults(resultDiv, [ { label: 'SOCKS Port', @@ -1106,6 +1112,8 @@ export function FirstTimeSetupModal(container, onComplete) { : controlResult?.error, }, ]); + const infoDiv = modal.querySelector('#tor-setup-info'); + if (infoDiv) infoDiv.classList.toggle('hidden', !torFailed); } catch (error) { console.error('Tor test failed:', error); @@ -1116,6 +1124,8 @@ export function FirstTimeSetupModal(container, onComplete) { message: error.message || String(error), }, ]); + const infoDiv = modal.querySelector('#tor-setup-info'); + if (infoDiv) infoDiv.classList.remove('hidden'); } btn.textContent = originalText; diff --git a/src/components/swap/Swap.js b/src/components/swap/Swap.js index 8c7cc1c..f106193 100644 --- a/src/components/swap/Swap.js +++ b/src/components/swap/Swap.js @@ -51,20 +51,23 @@ function saveSwapDataToCache(utxos, makers, balance) { } } +function escapeHtml(str) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + function formatTorEndpoint(address, start = 14, end = 16) { if (!address || typeof address !== 'string') return 'unknown'; const separatorIndex = address.lastIndexOf(':'); - if (separatorIndex === -1) return address; - - const host = address.slice(0, separatorIndex); - const port = address.slice(separatorIndex + 1); + const host = separatorIndex !== -1 ? address.slice(0, separatorIndex) : address; - if (host.length <= start + end + 3) { - return `${host}:${port}`; - } - - return `${host.slice(0, start)}...${host.slice(-end)}:${port}`; + if (host.length <= start + end + 3) return host; + return `${host.slice(0, start)}...${host.slice(-end)}`; } export async function SwapComponent(container) { @@ -122,6 +125,8 @@ export async function SwapComponent(container) { let numberOfHops = 3; let selectionMode = 'auto'; let selectedUtxos = []; + let makerSelectionMode = 'auto'; // 'auto' | 'manual' + let selectedMakerAddresses = []; // array of maker address strings let useCustomHops = false; let customHopCount = 6; let networkFeeRate = 2; @@ -156,6 +161,8 @@ export async function SwapComponent(container) { numberOfHops = savedSelections.numberOfHops || 3; selectionMode = savedSelections.selectionMode || 'auto'; selectedUtxos = savedSelections.selectedUtxos || []; + makerSelectionMode = savedSelections.makerSelectionMode || 'auto'; + selectedMakerAddresses = savedSelections.selectedMakerAddresses || []; useCustomHops = savedSelections.useCustomHops || false; customHopCount = savedSelections.customHopCount || 6; networkFeeRate = savedSelections.networkFeeRate || 5; @@ -265,10 +272,10 @@ export async function SwapComponent(container) { .filter((item) => item.offer !== null) .map((item, index) => { const offer = item.offer; - const addressObj = item.address || {}; - const onionAddr = addressObj.onion_addr || ''; - const port = addressObj.port || '6102'; - const makerAddress = `${onionAddr}:${port}`; + const makerAddress = typeof item.address === 'string' + ? item.address + : `${item.address?.onion_addr || ''}:${item.address?.port || ''}`; + return { address: makerAddress, minSize: offer.minSize || 0, @@ -435,6 +442,62 @@ export async function SwapComponent(container) { }); } + function renderMakerList() { + const makerListContainer = content.querySelector('#maker-list'); + if (!makerListContainer) return; + + const availableAddrs = new Set(availableMakers.map((m) => m.address)); + selectedMakerAddresses = selectedMakerAddresses.filter((a) => availableAddrs.has(a)); + + if (availableMakers.length === 0) { + makerListContainer.innerHTML = + '
No makers available
'; + return; + } + + makerListContainer.innerHTML = availableMakers + .map((maker, index) => { + return ` + + `; + }) + .join(''); + + // Restore checked state from selectedMakerAddresses + availableMakers.forEach((maker, index) => { + const checkbox = content.querySelector('#maker-addr-' + index); + if (checkbox) { + checkbox.checked = selectedMakerAddresses.includes(maker.address); + } + }); + + // Update count display + const countEl = content.querySelector('#selected-makers-count'); + if (countEl) countEl.textContent = selectedMakerAddresses.length; + + // Attach change listeners + availableMakers.forEach((maker, index) => { + const checkbox = content.querySelector('#maker-addr-' + index); + if (checkbox) { + checkbox.addEventListener('change', () => { + toggleMakerSelection(maker.address); + saveCurrentSelections(); + }); + } + }); + } + async function renderSwapHistorySection() { const swapHistoryContainer = content.querySelector('#swap-history-container'); const swapHistoryStats = content.querySelector('#swap-history-stats'); @@ -679,10 +742,25 @@ export async function SwapComponent(container) { details.hops + ' hop' + (details.hops !== 1 ? 's' : ''); content.querySelector('#estimated-time').textContent = formatEstimatedTime(details.timeSeconds); - const selectedMakersText = - getTopCandidateMakers() - .map((maker) => formatTorEndpoint(maker.address)) - .join(', ') || 'None selected'; + + // Update required-makers-count in the maker selection section + const requiredMakersCountEl = content.querySelector('#required-makers-count'); + if (requiredMakersCountEl) requiredMakersCountEl.textContent = getNumberOfMakers(); + + // Update makers label and display based on selection mode + const makersLabelEl = content.querySelector('#makers-label'); + let selectedMakersText; + if (makerSelectionMode === 'manual' && selectedMakerAddresses.length > 0) { + selectedMakersText = + selectedMakerAddresses.map((addr) => formatTorEndpoint(addr)).join(', '); + if (makersLabelEl) makersLabelEl.textContent = 'Selected Makers'; + } else { + selectedMakersText = + getTopCandidateMakers() + .map((maker) => formatTorEndpoint(maker.address)) + .join(', ') || 'None selected'; + if (makersLabelEl) makersLabelEl.textContent = 'Top Maker Candidates'; + } const selectedMakersEl = content.querySelector('#selected-makers-display'); selectedMakersEl.textContent = selectedMakersText; selectedMakersEl.title = selectedMakersText; @@ -764,6 +842,13 @@ export async function SwapComponent(container) { ); } + // Manual maker mode: check enough makers selected + if (makerSelectionMode === 'manual' && selectedMakerAddresses.length < makersNeeded) { + warnings.push( + `Need ${makersNeeded} maker${makersNeeded !== 1 ? 's' : ''} for this swap, but only ${selectedMakerAddresses.length} selected` + ); + } + // Check custom hops validity if (useCustomHops && customHopCount < 2) { warnings.push('Minimum 2 hops required (1 maker)'); @@ -873,6 +958,52 @@ export async function SwapComponent(container) { updateSummary(); } + function toggleMakerSelectionMode(mode) { + makerSelectionMode = mode; + + content.querySelectorAll('.maker-mode-btn').forEach((btn) => { + btn.className = + 'maker-mode-btn flex-1 bg-[#0f1419] hover:bg-[#242d3d] border border-gray-700 rounded-lg py-3 text-white font-semibold text-lg transition-colors'; + }); + content.querySelector('#maker-mode-' + mode).className = + 'maker-mode-btn flex-1 bg-[#FF6B35] border-2 border-[#FF6B35] rounded-lg py-3 text-white font-semibold text-lg'; + + const makerSelectionSection = content.querySelector('#maker-selection-section'); + if (makerSelectionSection) { + if (mode === 'manual') { + makerSelectionSection.classList.remove('hidden'); + } else { + makerSelectionSection.classList.add('hidden'); + } + } + + updateSummary(); + } + + function toggleMakerSelection(address) { + const addrIndex = selectedMakerAddresses.indexOf(address); + if (addrIndex > -1) { + selectedMakerAddresses.splice(addrIndex, 1); + } else { + selectedMakerAddresses.push(address); + } + + // Update checkbox state + const makerIndex = availableMakers.findIndex((m) => m.address === address); + if (makerIndex > -1) { + const checkbox = content.querySelector('#maker-addr-' + makerIndex); + if (checkbox) { + checkbox.checked = selectedMakerAddresses.includes(address); + } + } + + // Update count display + const countEl = content.querySelector('#selected-makers-count'); + if (countEl) countEl.textContent = selectedMakerAddresses.length; + + updateSummary(); + } + function checkUtxoTypeWarning() { const warningEl = content.querySelector('#utxo-warning'); if (!warningEl) return; @@ -948,6 +1079,8 @@ export async function SwapComponent(container) { numberOfHops, selectionMode, selectedUtxos, + makerSelectionMode, + selectedMakerAddresses, useCustomHops, customHopCount, networkFeeRate, @@ -1082,6 +1215,30 @@ export async function SwapComponent(container) {More hops = better privacy, higher fees