From 984c4389b04e10969c84ba92b85b6e74e0cdff01 Mon Sep 17 00:00:00 2001 From: iAmKeralis1131f Date: Fri, 27 Mar 2026 12:51:02 +0530 Subject: [PATCH 1/2] unification of protocols --- api1.js | 441 +++++++++++++------- coinswap-worker.js | 40 +- offerbook-worker.js | 54 ++- setup-coinswap.js | 2 +- src/components/market/Market.js | 71 +++- src/components/settings/FirstTimeSetup.js | 120 +----- src/components/swap/Coinswap.js | 221 ++++++++-- src/components/swap/Swap.js | 54 ++- src/components/swap/SwapHistory.js | 32 +- src/components/swap/SwapReport.js | 162 ++++++- src/components/taker/TakerInitialization.js | 2 +- src/components/wallet/UtxoList.js | 9 + src/components/wallet/Wallet.js | 12 +- src/styles/output.css | 31 +- 14 files changed, 886 insertions(+), 365 deletions(-) diff --git a/api1.js b/api1.js index 13ba75e..5174086 100644 --- a/api1.js +++ b/api1.js @@ -18,7 +18,7 @@ const api1State = { DEFAULT_WALLET_NAME: 'taker-wallet', currentWalletName: 'taker-wallet', currentWalletPassword: '', - protocolVersion: 'v1', // 'v1' (P2WSH/Taker) or 'v2' (Taproot/TaprootTaker) + protocolVersion: 'v1', // App-local protocol string: 'v1'/'v2' walletSyncInterval: null, syncState: { @@ -73,45 +73,90 @@ function getCurrentWalletName() { return api1State.DEFAULT_WALLET_NAME; } -function saveSwapReport(swapId, swapData) { - try { - const walletName = api1State.currentWalletName || getCurrentWalletName(); - const reportsDir = path.join( - api1State.DATA_DIR, - 'swap_reports', - walletName - ); +function buildTakerConfig({ + dataDir = api1State.DATA_DIR, + walletName = api1State.currentWalletName || api1State.DEFAULT_WALLET_NAME, + rpcConfig, + controlPort = 9051, + torAuthPassword, + zmqAddr = 'tcp://127.0.0.1:28332', + password = '', + protocol = api1State.protocolVersion || 'v1', + logLevel = store.get('logLevel') || process.env.LOG_LEVEL || 'debug', + appSwapId, +} = {}) { + return { + dataDir, + walletName, + rpcConfig, + controlPort, + torAuthPassword, + zmqAddr, + password, + protocol, + logLevel, + appSwapId, + }; +} - if (!fs.existsSync(reportsDir)) { - fs.mkdirSync(reportsDir, { recursive: true }); - console.log('๐Ÿ“ Created swap_reports directory'); - } +function safelyShutdownTaker(takerInstance) { + if (!takerInstance) return; + if (typeof takerInstance.shutdown === 'function') { + takerInstance.shutdown(); + } +} - const reportData = { - ...swapData, - swapId: swapId, - status: swapData.status || 'completed', - amount: swapData.amount, - startedAt: swapData.startedAt || Date.now(), - completedAt: swapData.completedAt || Date.now(), - }; +function toNumber(value, fallback = 0) { + const normalized = Number(value); + return Number.isFinite(normalized) ? normalized : fallback; +} - const filename = `${swapId}.json`; - const filepath = path.join(reportsDir, filename); +function getOfferbookSnapshot() { + const offerbookPath = path.join(api1State.DATA_DIR, 'offerbook.json'); + const snapshot = { + exists: false, + path: offerbookPath, + makerCount: 0, + stateCounts: {}, + updatedAt: null, + sample: [], + }; - fs.writeFileSync(filepath, JSON.stringify(reportData, null, 2), 'utf8'); - console.log(`๐Ÿ’พ Swap report saved: ${filepath}`); + try { + if (!fs.existsSync(offerbookPath)) { + return snapshot; + } - return true; + snapshot.exists = true; + snapshot.updatedAt = fs.statSync(offerbookPath).mtimeMs; + + const offerbook = JSON.parse(fs.readFileSync(offerbookPath, 'utf8')); + const makers = Array.isArray(offerbook.makers) ? offerbook.makers : []; + + snapshot.makerCount = makers.length; + snapshot.stateCounts = makers.reduce((acc, maker) => { + const key = + typeof maker.state === 'string' + ? maker.state + : maker.state && typeof maker.state === 'object' + ? Object.keys(maker.state)[0] || 'Unknown' + : 'Unknown'; + acc[key] = (acc[key] || 0) + 1; + return acc; + }, {}); + snapshot.sample = makers.slice(0, 3).map((maker) => ({ + address: maker.address + ? `${maker.address.onion_addr}:${maker.address.port}` + : 'unknown', + state: maker.state, + maxSize: maker.offer?.max_size ?? null, + bondExpiry: maker.offer?.fidelity?.bond?.cert_expiry ?? null, + })); } catch (error) { - console.error('โŒ Failed to save swap report:', error); - return false; + snapshot.error = error.message; } -} -function toNumber(value, fallback = 0) { - const normalized = Number(value); - return Number.isFinite(normalized) ? normalized : fallback; + return snapshot; } function getAllSwapReportPaths() { @@ -135,6 +180,129 @@ function getAllSwapReportPaths() { return reportPaths; } +function getCoreSwapReportPaths() { + const reportsRoot = path.join(api1State.DATA_DIR, 'swap_reports'); + if (!fs.existsSync(reportsRoot)) return []; + + return fs + .readdirSync(reportsRoot, { withFileTypes: true }) + .filter((entry) => entry.isFile() && entry.name.endsWith('.json')) + .map((entry) => path.join(reportsRoot, entry.name)); +} + +function readJsonFile(filePath) { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +function buildSwapReportRecord(filePath, rawReport) { + const fileName = path.basename(filePath, '.json'); + const nativeSwapId = + rawReport.nativeSwapId || + rawReport.native_swap_id || + rawReport.swap_id || + rawReport.swapId || + rawReport.report?.nativeSwapId || + rawReport.report?.native_swap_id || + rawReport.report?.swap_id || + rawReport.report?.swapId || + null; + const appSwapId = + rawReport.appSwapId || + rawReport.app_swap_id || + rawReport.swapId || + rawReport.swap_id || + rawReport.report?.appSwapId || + rawReport.report?.app_swap_id || + null; + const normalizedSwapId = appSwapId || nativeSwapId || fileName; + const nestedReport = rawReport.report ? rawReport.report : rawReport; + const isCoreReport = !filePath.includes( + `${path.sep}swap_reports${path.sep}${getCurrentWalletName()}${path.sep}` + ); + const rawStatus = + rawReport.status || rawReport.report?.status || (isCoreReport ? 'Success' : null); + const normalizedStatus = + String(rawStatus || '').toLowerCase() === 'success' + ? 'completed' + : String(rawStatus || '').toLowerCase().startsWith('recovery') + ? 'completed' + : rawStatus; + const rawCompletedAt = + rawReport.completedAt || + rawReport.completed_at || + rawReport.report?.completedAt || + rawReport.report?.completed_at || + rawReport.report?.endTimestamp || + rawReport.report?.end_timestamp || + null; + const completedAt = + Number.isFinite(Number(rawCompletedAt)) && Number(rawCompletedAt) < 1e12 + ? Number(rawCompletedAt) * 1000 + : rawCompletedAt; + + return { + ...rawReport, + report: nestedReport, + swapId: normalizedSwapId, + nativeSwapId, + appSwapId, + status: normalizedStatus, + completedAt, + filePath, + fileName, + isCoreReport, + }; +} + +function getPreferredSwapReports() { + const records = []; + const seen = new Set(); + + const corePaths = getCoreSwapReportPaths(); + for (const filePath of corePaths) { + try { + const record = buildSwapReportRecord(filePath, readJsonFile(filePath)); + const key = record.nativeSwapId || record.swapId || record.fileName; + seen.add(key); + records.push(record); + } catch (error) { + console.error(`Failed to read core swap report ${filePath}:`, error); + } + } + + for (const filePath of getAllSwapReportPaths()) { + try { + const record = buildSwapReportRecord(filePath, readJsonFile(filePath)); + const key = record.nativeSwapId || record.swapId || record.fileName; + if (seen.has(key)) continue; + seen.add(key); + records.push(record); + } catch (error) { + console.error(`Failed to read swap report ${filePath}:`, error); + } + } + + return records.sort((a, b) => { + const aTime = Number(a.completedAt || 0); + const bTime = Number(b.completedAt || 0); + return bTime - aTime; + }); +} + +function findSwapReportRecord(swapId) { + const normalizedTarget = String(swapId || ''); + return getPreferredSwapReports().find((record) => { + return [ + record.swapId, + record.nativeSwapId, + record.appSwapId, + record.fileName, + ] + .filter(Boolean) + .some((candidate) => String(candidate) === normalizedTarget); + }); +} + function getHistoricalSwapOutputMap() { const swapOutputs = new Map(); @@ -382,7 +550,7 @@ function registerTakerHandlers() { clearInterval(api1State.walletSyncInterval); api1State.walletSyncInterval = null; } - api1State.takerInstance.shutdown(); + safelyShutdownTaker(api1State.takerInstance); } catch (err) { console.error('โš ๏ธ Shutdown error:', err); } @@ -413,16 +581,13 @@ function registerTakerHandlers() { const torAuthPassword = config.taker?.tor_auth_password; const controlPort = config.taker?.control_port || 9051; - // โœ… SELECT TAKER CLASS - const TakerClass = - protocol === 'v2' - ? api1State.coinswapNapi.TaprootTaker - : api1State.coinswapNapi.Taker; + // Unified FFI now always exposes a single Taker class. + const TakerClass = api1State.coinswapNapi.Taker; if (!TakerClass) { return { success: false, - error: `${protocol === 'v2' ? 'TaprootTaker' : 'Taker'} class not found. Rebuild coinswap-napi.`, + error: 'Taker class not found. Rebuild coinswap-napi.', }; } @@ -454,15 +619,16 @@ function registerTakerHandlers() { api1State.protocolVersion = protocol; api1State.currentWalletName = walletName; api1State.currentWalletPassword = finalPassword; - api1State.storedTakerConfig = { + api1State.storedTakerConfig = buildTakerConfig({ dataDir: api1State.DATA_DIR, + walletName, rpcConfig, - zmqAddr, controlPort, torAuthPassword, + zmqAddr, password: finalPassword, protocol, - }; + }); console.log(`โœ… ${protocolName} Taker initialized`); @@ -513,7 +679,7 @@ function registerTakerHandlers() { // Shutdown taker instance if (api1State.takerInstance) { - api1State.takerInstance.shutdown(); + safelyShutdownTaker(api1State.takerInstance); api1State.takerInstance = null; api1State.protocolVersion = null; api1State.currentWalletName = null; @@ -550,6 +716,13 @@ function registerTakerHandlers() { } const syncId = `${source}_${Date.now()}_${Math.random().toString(36).substring(7)}`; + const preSyncSnapshot = getOfferbookSnapshot(); + console.log(`๐Ÿ”„ [${syncId}] Starting offerbook sync`, { + source, + walletName: api1State.currentWalletName || api1State.DEFAULT_WALLET_NAME, + protocol: api1State.protocolVersion || 'v1', + offerbook: preSyncSnapshot, + }); api1State.syncState.isRunning = true; api1State.syncState.currentSyncId = syncId; api1State.activeSyncs.set(syncId, { @@ -558,28 +731,29 @@ function registerTakerHandlers() { source, }); - const workerConfig = { - dataDir: api1State.DATA_DIR, + const workerConfig = buildTakerConfig({ + ...api1State.storedTakerConfig, walletName: api1State.currentWalletName || api1State.DEFAULT_WALLET_NAME, - rpcConfig: api1State.storedTakerConfig.rpcConfig, - zmqAddr: api1State.storedTakerConfig.zmqAddr, - controlPort: api1State.storedTakerConfig.controlPort || 9051, - torAuthPassword: api1State.storedTakerConfig.torAuthPassword, - password: api1State.storedTakerConfig.password || '', protocol: api1State.protocolVersion || 'v1', - }; + }); const worker = new Worker(path.join(__dirname, 'offerbook-worker.js'), { workerData: { config: workerConfig }, }); const finish = (status, extra = {}) => { + const postSyncSnapshot = getOfferbookSnapshot(); api1State.activeSyncs.set(syncId, { ...api1State.activeSyncs.get(syncId), ...extra, + offerbook: postSyncSnapshot, status, completedAt: Date.now(), }); + console.log(`๐Ÿ“˜ [${syncId}] Offerbook snapshot after sync`, { + status, + offerbook: postSyncSnapshot, + }); if (status === 'completed') api1State.syncState.lastSyncTime = Date.now(); api1State.syncState.isRunning = false; api1State.syncState.currentSyncId = null; @@ -662,32 +836,6 @@ function registerTakerHandlers() { let maxSwappable = Math.max(regular, swap) - 3000; - if ( - typeof api1State.takerInstance.checkSwapLiquidity === 'function' - ) { - try { - const nativeResult = api1State.takerInstance.checkSwapLiquidity(); - if (typeof nativeResult === 'number') { - maxSwappable = nativeResult; - } else if ( - nativeResult && - typeof nativeResult.maxSwappable === 'number' - ) { - maxSwappable = nativeResult.maxSwappable; - } else if ( - nativeResult && - typeof nativeResult.max_swappable === 'number' - ) { - maxSwappable = nativeResult.max_swappable; - } - } catch (nativeError) { - console.warn( - 'โš ๏ธ checkSwapLiquidity native call failed, using balance fallback:', - nativeError.message - ); - } - } - return { success: true, liquidity: { @@ -813,6 +961,8 @@ function registerTakerHandlers() { } } + // Returns the app-local protocol string. Native calls map this to + // 'Legacy'/'Taproot' when building SwapParams. const protocol = api1State.protocolVersion || 'v1'; const protocolName = protocol === 'v2' ? 'Taproot' : 'Legacy'; @@ -910,7 +1060,7 @@ function registerTakerHandlers() { } console.log('๐Ÿ”„ Recovering from failed swap...'); - api1State.takerInstance.recoverFromSwap(); + api1State.takerInstance.recoverActiveSwap(); console.log('โœ… Recovery completed'); return { success: true, message: 'Recovery completed' }; } catch (error) { @@ -1034,6 +1184,13 @@ function registerTakerHandlers() { if (!sync) { return { success: false, error: 'Sync not found' }; } + console.log(`๐Ÿ“ก [${syncId}] Sync status requested`, { + status: sync.status, + source: sync.source, + startedAt: sync.startedAt, + completedAt: sync.completedAt, + offerbook: sync.offerbook, + }); return { success: true, sync, @@ -1061,6 +1218,7 @@ function registerTakerHandlers() { } const offerbookPath = path.join(api1State.DATA_DIR, 'offerbook.json'); + console.log('๐Ÿ“– [getOffers] Reading offerbook', getOfferbookSnapshot()); if (fs.existsSync(offerbookPath)) { const offerbookData = fs.readFileSync(offerbookPath, 'utf8'); @@ -1139,6 +1297,12 @@ function registerTakerHandlers() { allMakers: makers.map(transformMaker), }; + console.log('๐Ÿ“Š [getOffers] Categorized offerbook', { + good: goodMakers.length, + bad: badMakers.length, + unresponsive: unresponsiveMakers.length, + }); + return { success: true, offerbook: transformedOfferbook, @@ -1170,7 +1334,16 @@ function registerTakerHandlers() { return { success: false, error: 'Taker not initialized' }; } - const goodMakers = api1State.takerInstance.getAllGoodMakers(); + const offerbookPath = path.join(api1State.DATA_DIR, 'offerbook.json'); + if (!fs.existsSync(offerbookPath)) { + return { success: true, makers: [] }; + } + + const offerbookData = fs.readFileSync(offerbookPath, 'utf8'); + const offerbook = JSON.parse(offerbookData); + const makers = Array.isArray(offerbook.makers) ? offerbook.makers : []; + const goodMakers = makers.filter(isUsableMaker); + return { success: true, makers: goodMakers }; } catch (error) { console.error('โŒ Fetch good makers failed:', error); @@ -1289,22 +1462,20 @@ function registerCoinswapHandlers() { const walletName = api1State.currentWalletName || api1State.DEFAULT_WALLET_NAME; - const config = { + const config = buildTakerConfig({ + ...api1State.storedTakerConfig, dataDir: api1State.DATA_DIR, - walletName: walletName, - controlPort: api1State.storedTakerConfig?.controlPort || 9051, + walletName, rpcConfig: api1State.storedTakerConfig?.rpcConfig || { url: '127.0.0.1:38332', username: 'user', password: 'password', - walletName: walletName, + walletName, }, - zmqAddr: - api1State.storedTakerConfig?.zmqAddr || 'tcp://127.0.0.1:28332', - password: password || '', - protocol: protocol, - logLevel: store.get('logLevel') || process.env.LOG_LEVEL || 'debug', - }; + password: password || api1State.storedTakerConfig?.password || '', + protocol, + appSwapId: swapId, + }); const worker = new Worker(path.join(__dirname, 'coinswap-worker.js'), { workerData: { amount, makerCount, outpoints, config }, @@ -1317,26 +1488,42 @@ function registerCoinswapHandlers() { protocol: protocol, isTaproot: protocol === 'v2', protocolVersion: protocol === 'v2' ? 2 : 1, + nativeSwapId: null, startedAt: Date.now(), }); worker.on('message', (msg) => { - if (msg.type === 'complete') { + if (msg.type === 'status') { + const existingSwap = api1State.activeSwaps.get(swapId) || {}; + api1State.activeSwaps.set(swapId, { + ...existingSwap, + status: msg.status || existingSwap.status, + nativeSwapId: msg.nativeSwapId || existingSwap.nativeSwapId, + protocol: msg.protocol || existingSwap.protocol || protocol, + isTaproot: + (msg.protocol || existingSwap.protocol || protocol) === 'v2', + protocolVersion: + (msg.protocol || existingSwap.protocol || protocol) === 'v2' + ? 2 + : 1, + }); + } else if (msg.type === 'complete') { const existingSwap = api1State.activeSwaps.get(swapId); const swapData = { ...existingSwap, status: 'completed', report: msg.report, protocol: msg.protocol || protocol, - isTaproot: msg.isTaproot || protocol === 'v2', + isTaproot: (msg.protocol || protocol) === 'v2', protocolVersion: protocol === 'v2' ? 2 : 1, - completedAt: Date.now(), - }; - api1State.activeSwaps.set(swapId, swapData); - saveSwapReport(swapId, swapData); - } else if (msg.type === 'error') { - const existingSwap = api1State.activeSwaps.get(swapId); - const swapData = { + nativeSwapId: msg.nativeSwapId || existingSwap?.nativeSwapId, + appSwapId: msg.appSwapId || swapId, + completedAt: Date.now(), + }; + api1State.activeSwaps.set(swapId, swapData); + } else if (msg.type === 'error') { + const existingSwap = api1State.activeSwaps.get(swapId); + const swapData = { ...existingSwap, status: 'failed', error: msg.error, @@ -1346,7 +1533,6 @@ function registerCoinswapHandlers() { failedAt: Date.now(), }; api1State.activeSwaps.set(swapId, swapData); - saveSwapReport(swapId, swapData); } }); @@ -1380,36 +1566,7 @@ function registerSwapReportsHandlers() { // Get all swap reports ipcMain.handle('swapReports:getAll', async () => { try { - const walletName = api1State.currentWalletName || getCurrentWalletName(); - const reportsDir = path.join( - api1State.DATA_DIR, - 'swap_reports', - walletName - ); - - if (!fs.existsSync(reportsDir)) { - fs.mkdirSync(reportsDir, { recursive: true }); - return { success: true, reports: [] }; - } - - const files = fs.readdirSync(reportsDir); - const jsonFiles = files.filter((f) => f.endsWith('.json')); - - const reports = jsonFiles - .map((file) => { - try { - const filePath = path.join(reportsDir, file); - const content = fs.readFileSync(filePath, 'utf-8'); - const report = JSON.parse(content); - const swapId = file.replace('.json', ''); - return { ...report, swapId }; - } catch (error) { - console.error(`Failed to read swap report ${file}:`, error); - return null; - } - }) - .filter((r) => r !== null); - + const reports = getPreferredSwapReports(); return { success: true, reports }; } catch (error) { console.error('Failed to get swap reports:', error); @@ -1420,33 +1577,11 @@ function registerSwapReportsHandlers() { // Get specific swap report ipcMain.handle('swapReports:get', async (event, swapId) => { try { - const walletName = api1State.currentWalletName || getCurrentWalletName(); - const reportsDir = path.join( - api1State.DATA_DIR, - 'swap_reports', - walletName - ); - - if (!fs.existsSync(reportsDir)) { - return { - success: false, - error: 'Swap reports directory does not exist', - }; - } - - const files = fs.readdirSync(reportsDir); - const matchingFile = files.find( - (f) => f.startsWith(swapId) && f.endsWith('.json') - ); - - if (!matchingFile) { + const report = findSwapReportRecord(swapId); + if (!report) { return { success: false, error: 'Swap report not found' }; } - const filePath = path.join(reportsDir, matchingFile); - const content = fs.readFileSync(filePath, 'utf-8'); - const report = JSON.parse(content); - return { success: true, report }; } catch (error) { console.error('Failed to get swap report:', error); diff --git a/coinswap-worker.js b/coinswap-worker.js index 24af8b2..d4c54f6 100644 --- a/coinswap-worker.js +++ b/coinswap-worker.js @@ -3,7 +3,7 @@ const { parentPort, workerData } = require('worker_threads'); /** * Worker thread for running long-running coinswap operations * This prevents blocking the main Electron process - * Supports both V1 (P2WSH/Taker) and V2 (Taproot/TaprootTaker) protocols + * Swap protocol is now a swap parameter on the unified Taker class. */ (async () => { @@ -16,14 +16,10 @@ const { parentPort, workerData } = require('worker_threads'); console.log(`๐Ÿ”ง Coinswap worker starting with ${protocolName} protocol`); - // Select the appropriate Taker class based on protocol - const TakerClass = - protocol === 'v2' ? coinswapNapi.TaprootTaker : coinswapNapi.Taker; + const TakerClass = coinswapNapi.Taker; if (!TakerClass) { - throw new Error( - `${protocol === 'v2' ? 'TaprootTaker' : 'Taker'} class not found. Please rebuild coinswap-napi.` - ); + throw new Error('Taker class not found. Please rebuild coinswap-napi.'); } // Setup logging if available @@ -53,25 +49,43 @@ const { parentPort, workerData } = require('worker_threads'); type: 'status', status: 'in_progress', protocol: config.protocol, - isTaproot: protocol === 'v2', }); - // Run the coinswap const swapParams = { + protocol: protocol === 'v2' ? 'Taproot' : 'Legacy', sendAmount: amount, makerCount: makerCount, manuallySelectedOutpoints: outpoints || undefined, }; - console.log(`๐Ÿš€ Executing ${protocolName} coinswap...`); - const report = taker.doCoinswap(swapParams); + console.log(`๐Ÿ”„ Syncing offerbook in swap worker before prepare...`); + taker.syncOfferbookAndWait(); + + console.log(`๐Ÿš€ Preparing ${protocolName} coinswap...`); + const swapId = taker.prepareCoinswap(swapParams); + + parentPort.postMessage({ + type: 'status', + status: 'prepared', + protocol: config.protocol, + nativeSwapId: swapId, + }); + + console.log(`๐Ÿš€ Starting ${protocolName} coinswap...`); + const report = taker.startCoinswap(swapId); // Send success message parentPort.postMessage({ type: 'complete', - report, + report: { + ...report, + nativeSwapId: swapId, + appSwapId: config.appSwapId, + protocol: config.protocol, + }, protocol: config.protocol || 'v1', - isTaproot: (config.protocol || 'v1') === 'v2', + nativeSwapId: swapId, + appSwapId: config.appSwapId, }); } catch (error) { // Send error message diff --git a/offerbook-worker.js b/offerbook-worker.js index 3ff2bb0..dc6c455 100644 --- a/offerbook-worker.js +++ b/offerbook-worker.js @@ -1,4 +1,6 @@ const { parentPort, workerData } = require('worker_threads'); +const fs = require('fs'); +const path = require('path'); /** * Worker thread for running offerbook sync operations. @@ -10,14 +12,10 @@ const { parentPort, workerData } = require('worker_threads'); const coinswapNapi = require('coinswap-napi'); const { config } = workerData; - const protocol = config.protocol || 'v1'; - const TakerClass = - protocol === 'v2' ? coinswapNapi.TaprootTaker : coinswapNapi.Taker; + const TakerClass = coinswapNapi.Taker; if (!TakerClass) { - throw new Error( - `${protocol === 'v2' ? 'TaprootTaker' : 'Taker'} class not found. Please rebuild coinswap-napi.` - ); + throw new Error('Taker class not found. Please rebuild coinswap-napi.'); } const taker = new TakerClass( @@ -30,9 +28,51 @@ const { parentPort, workerData } = require('worker_threads'); config.password || '' ); + const offerbookPath = path.join(config.dataDir, 'offerbook.json'); + const initialMtime = fs.existsSync(offerbookPath) + ? fs.statSync(offerbookPath).mtimeMs + : 0; + taker.syncOfferbookAndWait(); - parentPort.postMessage({ type: 'completed' }); + // Keep the worker alive until the offerbook file has had a chance to be + // refreshed on disk. The unified backend can continue processing Nostr + // announcements briefly after syncOfferbookAndWait() returns. + const timeoutAt = Date.now() + 12000; + let sawUpdatedOfferbook = false; + + while (Date.now() < timeoutAt) { + await new Promise((resolve) => setTimeout(resolve, 500)); + + if (!fs.existsSync(offerbookPath)) { + continue; + } + + const stat = fs.statSync(offerbookPath); + if (stat.mtimeMs <= initialMtime) { + continue; + } + + sawUpdatedOfferbook = true; + + try { + const offerbook = JSON.parse(fs.readFileSync(offerbookPath, 'utf8')); + const makers = Array.isArray(offerbook.makers) ? offerbook.makers : []; + + // Once the file is rewritten and we have maker entries, let the main + // process consume the refreshed offerbook immediately. + if (makers.length > 0) { + break; + } + } catch (error) { + // File may be mid-write; keep polling briefly. + } + } + + parentPort.postMessage({ + type: 'completed', + offerbookUpdated: sawUpdatedOfferbook, + }); } catch (err) { parentPort.postMessage({ type: 'error', error: err.message }); } diff --git a/setup-coinswap.js b/setup-coinswap.js index 5b03e1b..cfb3f98 100755 --- a/setup-coinswap.js +++ b/setup-coinswap.js @@ -18,7 +18,7 @@ function runCommand(cmd, options = {}) { } // STEP 1 โ€” Clone coinswap-ffi if missing -const BRANCH = 'offerbook-fix'; +const BRANCH = 'main'; const REPO_URL = 'https://github.com/citadel-tech/coinswap-ffi.git'; if (!fs.existsSync(FFI_DIR)) { diff --git a/src/components/market/Market.js b/src/components/market/Market.js index 0d5cfdb..f8aa8b8 100644 --- a/src/components/market/Market.js +++ b/src/components/market/Market.js @@ -11,6 +11,31 @@ export function Market(container) { let periodicRefreshInterval = null; let relayCount = null; + function getProtocolPresentation(protocol) { + switch (protocol) { + case 'Taproot': + return { + label: 'Taproot', + icon: 'โšก', + classes: 'bg-purple-500/20 text-purple-400', + }; + case 'Unified': + return { + label: 'Unified', + icon: 'โ—ˆ', + classes: 'bg-emerald-500/20 text-emerald-400', + }; + case 'Legacy': + case 'Legacy P2WSH': + default: + return { + label: 'Legacy', + icon: '๐Ÿ”’', + classes: 'bg-blue-500/20 text-blue-400', + }; + } + } + // Check sync state every second function startSyncStateMonitor() { if (syncCheckInterval) return; @@ -83,7 +108,7 @@ export function Market(container) { return { address: fullAddress, protocol: - item.protocol || (offer.tweakablePoint ? 'Taproot' : 'Legacy P2WSH'), + item.protocol || (offer.tweakablePoint ? 'Taproot' : 'Legacy'), baseFee: offer.baseFee || 0, volumeFee: (offer.amountRelativeFeePct || 0).toFixed(2), timeFee: (offer.timeRelativeFeePct || 0).toFixed(2), @@ -107,11 +132,17 @@ export function Market(container) { // API FUNCTIONS async function fetchMakers() { try { - console.log('๐Ÿ“ก Fetching makers from API...'); + console.log('๐Ÿ“ก [market] Fetching makers from API...'); isLoading = true; updateUI(); const data = await window.api.taker.getOffers(); + console.log('๐Ÿ“ก [market] Raw getOffers response', { + success: data.success, + cached: data.cached, + message: data.message, + error: data.error, + }); if (data.success && data.offerbook) { const goodMakers = data.offerbook.goodMakers || []; @@ -141,10 +172,17 @@ export function Market(container) { })), ]; - console.log('โœ… Loaded', makers.length, 'makers:', { + console.log('โœ… [market] Loaded makers into UI', { + total: makers.length, good: goodMakers.length, bad: badMakers.length, unresponsive: unresponsiveMakers.length, + sample: makers.slice(0, 3).map((maker) => ({ + address: maker.address, + status: maker.status, + maxSize: maker.maxSize, + bond: maker.bond, + })), }); isLoading = false; updateUI(); @@ -260,9 +298,11 @@ export function Market(container) { async function handleRefresh() { const refreshBtn = content.querySelector('#refresh-market-btn'); + console.log('๐Ÿ” [market] Refresh button clicked'); // Guard against double-click if sync already running const stateCheck = await window.api.taker.getCurrentSyncState(); + console.log('๐Ÿ” [market] Current sync state before refresh', stateCheck); if (stateCheck.success && stateCheck.isRunning) { showError('Sync already in progress'); return; @@ -277,6 +317,7 @@ export function Market(container) { try { const result = await window.api.taker.syncOfferbookAndWait(); + console.log('๐Ÿ” [market] syncOfferbookAndWait start result', result); if (!result.success) { throw new Error(result.error || 'Failed to start sync'); } @@ -286,6 +327,13 @@ export function Market(container) { const poll = setInterval(async () => { try { const status = await window.api.taker.getSyncStatus(syncId); + console.log('๐Ÿ” [market] Polled sync status', { + syncId, + success: status.success, + status: status.sync?.status, + error: status.sync?.error || status.error, + offerbook: status.sync?.offerbook, + }); if (!status.success) { clearInterval(poll); reject(new Error('Failed to get sync status')); return; } if (status.sync.status === 'completed') { clearInterval(poll); resolve(); } else if (status.sync.status === 'failed') { clearInterval(poll); reject(new Error(status.sync.error || 'Sync failed')); } @@ -294,6 +342,7 @@ export function Market(container) { }); syncProgress = null; + console.log('๐Ÿ” [market] Sync finished, reloading makers'); await fetchMakers(); refreshBtn.innerHTML = 'Refreshed!'; @@ -302,6 +351,7 @@ export function Market(container) { refreshBtn.innerHTML = originalText; }, 2000); } catch (error) { + console.error('โŒ [market] Refresh failed', error); syncProgress = null; updateUI(); refreshBtn.innerHTML = 'Refresh Failed'; @@ -694,16 +744,14 @@ export function Market(container) { } else { tableBody.innerHTML = displayedMakers .map( - (maker) => ` + (maker) => { + const protocolBadge = getProtocolPresentation(maker.protocol); + return `
- - ${maker.protocol === 'Taproot' ? 'โšก Taproot' : '๐Ÿ”’ Legacy'} + + ${protocolBadge.icon} ${protocolBadge.label}
${maker.address.substring(0, 18)}...
@@ -716,7 +764,8 @@ export function Market(container) { ${maker.bond > 0 ? maker.bond.toLocaleString() : 'N/A'}
- ` + `; + } ) .join(''); } diff --git a/src/components/settings/FirstTimeSetup.js b/src/components/settings/FirstTimeSetup.js index 99903ed..2d1670d 100644 --- a/src/components/settings/FirstTimeSetup.js +++ b/src/components/settings/FirstTimeSetup.js @@ -22,10 +22,10 @@ export function FirstTimeSetupModal(container, onComplete) { 'fixed inset-0 bg-black/70 flex items-center justify-center z-50'; let currentStep = 1; - const totalSteps = 4; + const totalSteps = 3; let walletAction = null; // 'create', 'load', or 'restore' let walletData = {}; - let protocolVersion = 'v2'; // 'v1' (P2WSH) or 'v2' (Taproot) + let protocolVersion = 'v2'; // Fixed app-local default until the rest of the flow stops expecting v1/v2. modal.innerHTML = `
@@ -35,82 +35,16 @@ export function FirstTimeSetupModal(container, onComplete) {

Wallet and Other Setups.

-
+
- Step 1 of 4 + Step 1 of 3
- +
-
-

Getting Started

-

Choose your swap protocol and we'll configure your wallet for private Bitcoin swaps.

-
- - -
-

Select Swap Type

-

- This determines which type of swap you can perform. Either Taproot or Legacy(P2WSH) swaps. -

- -
- -
-
- โšก -
Taproot
-
-

- Contract Tx with MuSig2 + Taproot HTLC -

-
    -
  • โœ“ Cheaper swap fees
  • -
  • โœ“ Enhanced privacy
  • -
  • โœ“ Modern protocol
  • -
-
- Recommended -
-
- - -
-
- ๐Ÿ” -
Legacy P2WSH
-
-

- Contract Tx with 2-of-2 Multisig + P2WSH HTLC -

-
    -
  • โ€ข Higher swap fees
  • -
  • โ€ข Less private
  • -
  • โ€ข Original atomic swap protocol
  • -
-
- Battle-tested -
-
-
- - -
-
- ${iconWarning} -

- Important: You can only perform one type of swap with a taker (Taproot OR Legacy swaps). You cannot do both. However, your wallet can handle both Taproot and Legacy transactions for regular operations of send and receive. -

-
-
-
-
- - - - + - - -
+

Swap Amount

@@ -991,6 +1120,15 @@ export function SwapReportComponent(container, swapReport) {

+
+
+

Protocol

+ ${report.protocol === 'Taproot' ? 'โšก' : report.protocol === 'Unified' ? 'โ—ˆ' : '๐Ÿ”’'} +
+

${report.protocol}

+

Rendered from normalized report data

+
+

Total Fee

diff --git a/src/components/taker/TakerInitialization.js b/src/components/taker/TakerInitialization.js index 07c1067..6188e37 100644 --- a/src/components/taker/TakerInitialization.js +++ b/src/components/taker/TakerInitialization.js @@ -371,7 +371,7 @@ export function getSwapStatus(swapId) { return window.api.coinswap.getStatus(swapId); } -export function recoverFromSwap() { +export function recoverActiveSwap() { return window.api.taker.recover(); } diff --git a/src/components/wallet/UtxoList.js b/src/components/wallet/UtxoList.js index cf0b8bc..3e1208f 100644 --- a/src/components/wallet/UtxoList.js +++ b/src/components/wallet/UtxoList.js @@ -23,6 +23,14 @@ export function UtxoListComponent(container) { } } + async function syncWalletState() { + const result = await window.api.taker.sync(); + if (!result?.success) { + throw new Error(result?.error || 'Wallet sync failed'); + } + console.log('โœ… Wallet sync completed before UTXO refresh'); + } + // Helper Functions function satsToBtc(sats) { return (sats / 100000000).toFixed(8); @@ -320,6 +328,7 @@ export function UtxoListComponent(container) { refreshBtn.disabled = true; try { + await syncWalletState(); await loadUtxos(); refreshBtn.textContent = 'Refreshed!'; diff --git a/src/components/wallet/Wallet.js b/src/components/wallet/Wallet.js index 08dd5bc..fc2cbb9 100644 --- a/src/components/wallet/Wallet.js +++ b/src/components/wallet/Wallet.js @@ -96,6 +96,14 @@ export async function WalletComponent(container) { } } + async function syncWalletState() { + const result = await window.api.taker.sync(); + if (!result?.success) { + throw new Error(result?.error || 'Wallet sync failed'); + } + console.log('โœ… Wallet sync completed before refresh'); + } + // Helper Functions function satsToBtc(sats) { return (sats / 100000000).toFixed(8); @@ -345,7 +353,9 @@ export async function WalletComponent(container) { refreshBtn.disabled = true; try { - // โœ… FORCE FRESH FETCH + await syncWalletState(); + + // โœ… FORCE FRESH FETCH AFTER WALLET SYNC const [balance, transactions, utxos] = await Promise.all([ fetchBalance(), fetchTransactions(), diff --git a/src/styles/output.css b/src/styles/output.css index 850dc38..ca89c79 100644 --- a/src/styles/output.css +++ b/src/styles/output.css @@ -21,6 +21,8 @@ --color-green-500: oklch(72.3% 0.219 149.579); --color-green-600: oklch(62.7% 0.194 149.214); --color-green-700: oklch(52.7% 0.154 150.069); + --color-emerald-400: oklch(76.5% 0.177 163.223); + --color-emerald-500: oklch(69.6% 0.17 162.48); --color-cyan-300: oklch(86.5% 0.127 207.078); --color-cyan-400: oklch(78.9% 0.154 211.53); --color-cyan-500: oklch(71.5% 0.143 215.221); @@ -67,7 +69,6 @@ --font-weight-semibold: 600; --font-weight-bold: 700; --tracking-wide: 0.025em; - --leading-relaxed: 1.625; --radius-lg: 0.5rem; --radius-xl: 0.75rem; --ease-out: cubic-bezier(0, 0, 0.2, 1); @@ -581,6 +582,9 @@ .grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } + .grid-cols-5 { + grid-template-columns: repeat(5, minmax(0, 1fr)); + } .grid-cols-8 { grid-template-columns: repeat(8, minmax(0, 1fr)); } @@ -823,6 +827,12 @@ .border-gray-800 { border-color: var(--color-gray-800); } + .border-green-500\/20 { + border-color: color-mix(in srgb, oklch(72.3% 0.219 149.579) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-green-500) 20%, transparent); + } + } .border-green-500\/30 { border-color: color-mix(in srgb, oklch(72.3% 0.219 149.579) 30%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -856,6 +866,12 @@ border-color: color-mix(in oklab, var(--color-purple-500) 30%, transparent); } } + .border-red-500\/20 { + border-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-red-500) 20%, transparent); + } + } .border-red-500\/30 { border-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 30%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -958,6 +974,12 @@ background-color: color-mix(in oklab, var(--color-cyan-500) 20%, transparent); } } + .bg-emerald-500\/20 { + background-color: color-mix(in srgb, oklch(69.6% 0.17 162.48) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-emerald-500) 20%, transparent); + } + } .bg-gray-500\/20 { background-color: color-mix(in srgb, oklch(55.1% 0.027 264.364) 20%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -1244,10 +1266,6 @@ --tw-leading: 1; line-height: 1; } - .leading-relaxed { - --tw-leading: var(--leading-relaxed); - line-height: var(--leading-relaxed); - } .font-bold { --tw-font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold); @@ -1302,6 +1320,9 @@ .text-cyan-400 { color: var(--color-cyan-400); } + .text-emerald-400 { + color: var(--color-emerald-400); + } .text-gray-300 { color: var(--color-gray-300); } From 8f6ed9d4756ee8d6f26dee1027c5f13dc17a7134 Mon Sep 17 00:00:00 2001 From: iAmKeralis1131f Date: Fri, 27 Mar 2026 13:52:26 +0530 Subject: [PATCH 2/2] update swap report --- api1.js | 116 ++++++++-- coinswap-worker.js | 14 +- offerbook-worker.js | 15 +- src/components/swap/Swap.js | 12 +- src/components/swap/SwapReport.js | 357 ++++++++++++------------------ src/styles/output.css | 21 -- 6 files changed, 265 insertions(+), 270 deletions(-) diff --git a/api1.js b/api1.js index 5174086..03937b4 100644 --- a/api1.js +++ b/api1.js @@ -194,6 +194,60 @@ function readJsonFile(filePath) { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } +function normalizeSwapProtocol(value, fallbackIsTaproot = false) { + switch (value) { + case 'v2': + case 'Taproot': + return 'Taproot'; + case 'Unified': + return 'Unified'; + case 'v1': + case 'Legacy': + case 'Legacy P2WSH': + return 'Legacy'; + default: + return fallbackIsTaproot ? 'Taproot' : 'Legacy'; + } +} + +function inferTaprootFromReport(rawReport = {}) { + const nestedReport = rawReport.report || rawReport; + const rawProtocol = rawReport.protocol || nestedReport.protocol || null; + const explicitIsTaproot = + rawReport.isTaproot ?? nestedReport.isTaproot ?? null; + + if (explicitIsTaproot === true) return true; + if (explicitIsTaproot === false) return false; + + const explicitProtocol = rawProtocol + ? normalizeSwapProtocol(rawProtocol, false) + : null; + + if (explicitProtocol === 'Taproot') return true; + if (explicitProtocol === 'Legacy') return false; + + const protocolVersion = + rawReport.protocolVersion || + rawReport.protocol_version || + nestedReport.protocolVersion || + nestedReport.protocol_version || + null; + if (Number(protocolVersion) === 2) return true; + if (Number(protocolVersion) === 1) return false; + + const outputSwapUtxos = + rawReport.outputSwapUtxos || + rawReport.output_swap_utxos || + nestedReport.outputSwapUtxos || + nestedReport.output_swap_utxos || + []; + + return outputSwapUtxos.some((entry) => { + const address = Array.isArray(entry) ? String(entry[1] || '') : ''; + return /^(bc1p|tb1p|bcrt1p)/i.test(address); + }); +} + function buildSwapReportRecord(filePath, rawReport) { const fileName = path.basename(filePath, '.json'); const nativeSwapId = @@ -216,9 +270,13 @@ function buildSwapReportRecord(filePath, rawReport) { null; const normalizedSwapId = appSwapId || nativeSwapId || fileName; const nestedReport = rawReport.report ? rawReport.report : rawReport; - const isCoreReport = !filePath.includes( - `${path.sep}swap_reports${path.sep}${getCurrentWalletName()}${path.sep}` + const normalizedFilePath = path.normalize(String(filePath || '')); + const escapedSep = path.sep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const walletScopedReportPattern = new RegExp( + `${escapedSep}swap_reports${escapedSep}[^${escapedSep}]+${escapedSep}` ); + const isWalletScopedReport = walletScopedReportPattern.test(normalizedFilePath); + const isCoreReport = !isWalletScopedReport; const rawStatus = rawReport.status || rawReport.report?.status || (isCoreReport ? 'Success' : null); const normalizedStatus = @@ -239,6 +297,17 @@ function buildSwapReportRecord(filePath, rawReport) { Number.isFinite(Number(rawCompletedAt)) && Number(rawCompletedAt) < 1e12 ? Number(rawCompletedAt) * 1000 : rawCompletedAt; + const isTaproot = inferTaprootFromReport(rawReport); + const protocol = normalizeSwapProtocol( + rawReport.protocol || nestedReport.protocol, + isTaproot + ); + const protocolVersion = + rawReport.protocolVersion || + rawReport.protocol_version || + nestedReport.protocolVersion || + nestedReport.protocol_version || + (protocol === 'Taproot' ? 2 : 1); return { ...rawReport, @@ -251,6 +320,9 @@ function buildSwapReportRecord(filePath, rawReport) { filePath, fileName, isCoreReport, + protocol, + isTaproot, + protocolVersion, }; } @@ -1495,35 +1567,39 @@ function registerCoinswapHandlers() { worker.on('message', (msg) => { if (msg.type === 'status') { const existingSwap = api1State.activeSwaps.get(swapId) || {}; + const normalizedProtocol = normalizeSwapProtocol( + msg.protocol || existingSwap.protocol || protocol, + existingSwap.isTaproot || protocol === 'v2' + ); api1State.activeSwaps.set(swapId, { ...existingSwap, status: msg.status || existingSwap.status, nativeSwapId: msg.nativeSwapId || existingSwap.nativeSwapId, - protocol: msg.protocol || existingSwap.protocol || protocol, - isTaproot: - (msg.protocol || existingSwap.protocol || protocol) === 'v2', - protocolVersion: - (msg.protocol || existingSwap.protocol || protocol) === 'v2' - ? 2 - : 1, + protocol: normalizedProtocol, + isTaproot: normalizedProtocol === 'Taproot', + protocolVersion: normalizedProtocol === 'Taproot' ? 2 : 1, }); } else if (msg.type === 'complete') { const existingSwap = api1State.activeSwaps.get(swapId); + const normalizedProtocol = normalizeSwapProtocol( + msg.protocol || msg.report?.protocol || existingSwap?.protocol || protocol, + existingSwap?.isTaproot || protocol === 'v2' + ); const swapData = { ...existingSwap, status: 'completed', report: msg.report, - protocol: msg.protocol || protocol, - isTaproot: (msg.protocol || protocol) === 'v2', - protocolVersion: protocol === 'v2' ? 2 : 1, - nativeSwapId: msg.nativeSwapId || existingSwap?.nativeSwapId, - appSwapId: msg.appSwapId || swapId, - completedAt: Date.now(), - }; - api1State.activeSwaps.set(swapId, swapData); - } else if (msg.type === 'error') { - const existingSwap = api1State.activeSwaps.get(swapId); - const swapData = { + protocol: normalizedProtocol, + isTaproot: normalizedProtocol === 'Taproot', + protocolVersion: normalizedProtocol === 'Taproot' ? 2 : 1, + nativeSwapId: msg.nativeSwapId || existingSwap?.nativeSwapId, + appSwapId: msg.appSwapId || swapId, + completedAt: Date.now(), + }; + api1State.activeSwaps.set(swapId, swapData); + } else if (msg.type === 'error') { + const existingSwap = api1State.activeSwaps.get(swapId); + const swapData = { ...existingSwap, status: 'failed', error: msg.error, diff --git a/coinswap-worker.js b/coinswap-worker.js index d4c54f6..6c59af9 100644 --- a/coinswap-worker.js +++ b/coinswap-worker.js @@ -12,7 +12,9 @@ const { parentPort, workerData } = require('worker_threads'); const { amount, makerCount, outpoints, config } = workerData; const protocol = config.protocol || 'v1'; - const protocolName = protocol === 'v2' ? 'Taproot (V2)' : 'P2WSH (V1)'; + const normalizedProtocol = protocol === 'v2' ? 'Taproot' : 'Legacy'; + const protocolName = + normalizedProtocol === 'Taproot' ? 'Taproot (V2)' : 'P2WSH (V1)'; console.log(`๐Ÿ”ง Coinswap worker starting with ${protocolName} protocol`); @@ -48,11 +50,11 @@ const { parentPort, workerData } = require('worker_threads'); parentPort.postMessage({ type: 'status', status: 'in_progress', - protocol: config.protocol, + protocol: normalizedProtocol || config.protocol, }); const swapParams = { - protocol: protocol === 'v2' ? 'Taproot' : 'Legacy', + protocol: normalizedProtocol, sendAmount: amount, makerCount: makerCount, manuallySelectedOutpoints: outpoints || undefined, @@ -67,7 +69,7 @@ const { parentPort, workerData } = require('worker_threads'); parentPort.postMessage({ type: 'status', status: 'prepared', - protocol: config.protocol, + protocol: normalizedProtocol || config.protocol, nativeSwapId: swapId, }); @@ -81,9 +83,9 @@ const { parentPort, workerData } = require('worker_threads'); ...report, nativeSwapId: swapId, appSwapId: config.appSwapId, - protocol: config.protocol, + protocol: swapParams.protocol || normalizedProtocol || config.protocol, }, - protocol: config.protocol || 'v1', + protocol: normalizedProtocol || config.protocol, nativeSwapId: swapId, appSwapId: config.appSwapId, }); diff --git a/offerbook-worker.js b/offerbook-worker.js index dc6c455..c62a539 100644 --- a/offerbook-worker.js +++ b/offerbook-worker.js @@ -44,11 +44,22 @@ const path = require('path'); while (Date.now() < timeoutAt) { await new Promise((resolve) => setTimeout(resolve, 500)); - if (!fs.existsSync(offerbookPath)) { + let stat; + + try { + stat = fs.statSync(offerbookPath); + } catch (error) { + if (error.code === 'ENOENT') { + continue; + } + + throw error; + } + + if (!stat) { continue; } - const stat = fs.statSync(offerbookPath); if (stat.mtimeMs <= initialMtime) { continue; } diff --git a/src/components/swap/Swap.js b/src/components/swap/Swap.js index 0569fc4..071ed97 100644 --- a/src/components/swap/Swap.js +++ b/src/components/swap/Swap.js @@ -151,9 +151,13 @@ export async function SwapComponent(container) { let totalBalance = 0; const btcPrice = 50000; + function getMakerProtocol(makerOrItem, offer = makerOrItem?.offer) { + return makerOrItem?.protocol || (offer?.tweakablePoint ? 'Taproot' : 'Legacy'); + } + function filterMakersByProtocol(makers) { return makers.filter((maker) => { - const makerProtocol = maker.protocol || 'Legacy'; + const makerProtocol = getMakerProtocol(maker); // Unified treated as compatible but not yet a user-selectable mode. if (makerProtocol === 'Unified') return true; return currentProtocol === 'v2' @@ -255,9 +259,7 @@ export async function SwapComponent(container) { baseFee: offer.baseFee || 0, volumeFeePct: offer.amountRelativeFeePct || 0, timeFeePct: offer.timeRelativeFeePct || 0, - protocol: - item.protocol || - (offer.tweakablePoint ? 'Taproot' : 'Legacy'), + protocol: getMakerProtocol(item, offer), index: index, }; }) @@ -1275,7 +1277,7 @@ export async function SwapComponent(container) { // Filter makers by protocol const compatibleMakers = goodMakers.filter((maker) => { - const makerProtocol = maker.protocol || 'Legacy'; + const makerProtocol = getMakerProtocol(maker); // Unified treated as compatible but not yet a user-selectable mode. if (makerProtocol === 'Unified') return true; return protocol === 'v2' diff --git a/src/components/swap/SwapReport.js b/src/components/swap/SwapReport.js index 2240348..5e169de 100644 --- a/src/components/swap/SwapReport.js +++ b/src/components/swap/SwapReport.js @@ -41,6 +41,21 @@ export function SwapReportComponent(container, swapReport) { const normalized = Number(value); return Number.isFinite(normalized) ? normalized : fallback; }; + + const flattenTxidEntries = (value) => { + if (Array.isArray(value)) { + return value.flatMap((entry) => flattenTxidEntries(entry)); + } + + if (typeof value === 'string' && value.trim()) { + return [value.trim()]; + } + + return []; + }; + + const dedupeTxids = (value) => [...new Set(flattenTxidEntries(value))]; + const nestedReport = swapReport.report || {}; const rawTotalMakerFees = toNumber( @@ -93,11 +108,14 @@ export function SwapReportComponent(container, swapReport) { const normalizedFundingTxids = swapReport.fundingTxidsByHop || swapReport.funding_txids_by_hop || + swapReport.fundingTxids || + swapReport.funding_txids || nestedReport.fundingTxidsByHop || nestedReport.funding_txids_by_hop || nestedReport.fundingTxids || nestedReport.funding_txids || []; + const flattenedFundingTxids = dedupeTxids(normalizedFundingTxids); const normalizedTargetAmount = toNumber( swapReport.targetAmount ?? swapReport.target_amount ?? @@ -119,7 +137,7 @@ export function SwapReportComponent(container, swapReport) { swapReport.total_funding_txs ?? nestedReport.totalFundingTxs ?? nestedReport.total_funding_txs, - Array.isArray(normalizedFundingTxids) ? normalizedFundingTxids.length : 0 + flattenedFundingTxids.length ); const normalizedFeePercentage = toNumber( swapReport.feePercentage ?? @@ -133,6 +151,35 @@ export function SwapReportComponent(container, swapReport) { swapReport.protocol || nestedReport.protocol, swapReport.isTaproot || nestedReport.isTaproot || false ); + const hasExplicitProtocolMetadata = + Boolean(swapReport.protocol || nestedReport.protocol) || + typeof swapReport.isTaproot === 'boolean' || + typeof nestedReport.isTaproot === 'boolean'; + const outgoingContractTxid = + swapReport.outgoingContractTxid || + swapReport.outgoing_contract_txid || + nestedReport.outgoingContractTxid || + nestedReport.outgoing_contract_txid || + null; + const incomingContractTxid = + swapReport.incomingContractTxid || + swapReport.incoming_contract_txid || + nestedReport.incomingContractTxid || + nestedReport.incoming_contract_txid || + null; + const recoveryTxids = dedupeTxids( + swapReport.recoveryTxids || + swapReport.recovery_txids || + nestedReport.recoveryTxids || + nestedReport.recovery_txids || + [] + ); + const sweepTxid = + swapReport.sweep_txid || + swapReport.sweepTxid || + swapReport.taker_sweep_txid || + swapReport.takerSweepTxid || + null; const report = { swapId: swapReport.swapId || swapReport.swap_id || 'unknown', @@ -190,6 +237,7 @@ export function SwapReportComponent(container, swapReport) { [], totalFundingTxs: normalizedTotalFundingTxs, fundingTxidsByHop: normalizedFundingTxids, + fundingTxids: flattenedFundingTxids, totalFee: rawTotalFee, totalMakerFees: rawTotalMakerFees, miningFee: normalizedMiningFee, @@ -220,13 +268,8 @@ export function SwapReportComponent(container, swapReport) { nestedReport.outputSwapUtxos || nestedReport.output_swap_utxos || [], - sweepTxid: - swapReport.sweep_txid || - swapReport.sweepTxid || - swapReport.taker_sweep_txid || - swapReport.takerSweepTxid || - null, - protocol, + sweepTxid, + protocol: hasExplicitProtocolMetadata ? protocol : null, isTaproot: protocol === 'Taproot' || swapReport.isTaproot || @@ -235,12 +278,13 @@ export function SwapReportComponent(container, swapReport) { protocolVersion: swapReport.protocolVersion || (protocol === 'Taproot' ? 2 : 1), + outgoingContractTxid, + incomingContractTxid, + recoveryTxids, }; console.log('๐Ÿ“Š Normalized report:', report); - const isV2Swap = report.protocol === 'Taproot'; - // Helper functions function satsToBtc(sats) { if (typeof sats !== 'number' || isNaN(sats)) return '0.00000000'; @@ -407,167 +451,77 @@ export function SwapReportComponent(container, swapReport) { } const makerColors = ['#FF6B35', '#3B82F6', '#A855F7', '#06B6D4', '#10B981']; - - // Build funding transactions HTML - function buildFundingTxsHtml() { - if (!report.fundingTxidsByHop || report.fundingTxidsByHop.length === 0) { - return '

No transaction data available

'; - } - - if (isV2Swap) { - // Extract txids from fundingTxidsByHop - const allTxids = report.fundingTxidsByHop.map((arr) => - Array.isArray(arr) ? arr[0] : arr - ); - - // Outgoing: Taker is [0], Makers are [1], [2], [3] - const takerOutgoing = allTxids[0]; - const makersOutgoing = allTxids.slice(1); // [1, 2, 3] - - // Incoming: Makers receive [0], [1], [2], Taker receives [3] - const makersIncoming = allTxids.slice(0, -1); // [0, 1, 2] - const takerIncoming = allTxids[allTxids.length - 1]; // [3] - + const transactionArtifacts = [ + { + label: 'Outgoing Contract', + txid: report.outgoingContractTxid, + accent: '#FF6B35', + description: 'Contract transaction recorded on the outgoing side.', + }, + { + label: 'Incoming Contract', + txid: report.incomingContractTxid, + accent: '#10B981', + description: 'Contract transaction recorded on the incoming side.', + }, + ...report.fundingTxids.map((txid, index) => ({ + label: `Funding Transaction ${index + 1}`, + txid, + accent: makerColors[index % makerColors.length], + description: 'Funding transaction captured directly from the saved report.', + })), + ...report.recoveryTxids.map((txid, index) => ({ + label: `Recovery Transaction ${index + 1}`, + txid, + accent: '#F59E0B', + description: 'Recovery-related transaction included by the backend.', + })), + ...(report.sweepTxid + ? [ + { + label: 'Final Sweep', + txid: report.sweepTxid, + accent: '#06B6D4', + description: 'Final sweep transaction when present in the report.', + }, + ] + : []), + ].filter((artifact) => artifact.txid); + report.transactionArtifacts = transactionArtifacts; + report.artifactsCount = transactionArtifacts.length; + + function buildTransactionArtifactsHtml() { + if (!report.transactionArtifacts || report.transactionArtifacts.length === 0) { return ` - -
-

- ๐Ÿ“ค Outgoing Contracts -

- - -
-

- Taker (You) -

-
-

- ${takerOutgoing ? truncateTxid(takerOutgoing) : 'N/A'} +

+

+ No transaction IDs were embedded in this report file. The report still includes makers, fees, and UTXO outputs below.

-
- - -
-
+ `; + } - - ${report.makerAddresses - .map((addr, idx) => { - const color = makerColors[idx % makerColors.length]; - const txid = makersOutgoing[idx] || 'N/A'; - - return ` -
-

- Maker ${idx + 1} -

-
-

- ${typeof txid === 'string' ? truncateTxid(txid) : 'N/A'} -

-
- - + return report.transactionArtifacts + .map((artifact) => { + return ` +
+
+
+

+ ${artifact.label} +

+

${artifact.description}

+

${artifact.txid}

-
-
- `; - }) - .join('')} -
- - -
-

- ๐Ÿ“ฅ Incoming Contracts -

- - - ${report.makerAddresses - .map((addr, idx) => { - const color = makerColors[idx % makerColors.length]; - const txid = makersIncoming[idx] || 'N/A'; - - return ` -
-

- Maker ${idx + 1} -

-
-

- ${typeof txid === 'string' ? truncateTxid(txid) : 'N/A'} -

-
- - +
+ +
`; - }) - .join('')} - - -
-

- Taker (You) - Final Sweep -

-
-

- ${takerIncoming ? truncateTxid(takerIncoming) : 'N/A'} -

-
- - -
-
-
-
- `; - } - - // โœ… V1 PROTOCOL: Show all funding transactions - - return report.fundingTxidsByHop - .map((txids, hopIdx) => { - const txidArray = Array.isArray(txids) ? txids : [txids]; - const color = makerColors[hopIdx % makerColors.length]; - - return ` -
-

- - ${hopIdx + 1} - - Hop ${hopIdx + 1} -

- ${txidArray - .map( - (txid) => ` -
-

${truncateTxid(txid)}

-
- - -
-
- ` - ) - .join('')} -
- `; }) .join(''); } @@ -601,41 +555,15 @@ export function SwapReportComponent(container, swapReport) { .join(''); } - function getProtocolInfoLines() { - if (report.protocol === 'Unified') { - return ` -
-

Compatibility: Treated as compatible with both app modes.

-

Current backend behavior: Native Unified handling is still being validated.

-
-
-

Anonymity Set: Determined by the contracts actually used in the swap.

-

Display note: Shown distinctly so mixed maker markets are easier to inspect.

-
- `; - } - - if (!isV2Swap) { - return ` -
-

Compatibility: Uses Legacy P2WSH swap contracts.

-

Tradeoff: Wider legacy maker compatibility with higher on-chain footprint.

-
-
-

Anonymity Set: Legacy P2WSH swap outputs.

-

Routing: Privacy comes from the maker hop circuit rather than direct links.

-
- `; - } - + function getReportInfoLines() { return `
-

Save Money: Lesser Fees than V1 swaps.

-

Efficient: Combined tapscript with Musig2 + HTLC leaves.

+

Rendering Mode: This report is built from whichever fields are actually present in the saved JSON.

+

Artifacts: Contract, funding, recovery, and sweep transaction IDs are shown whenever the backend included them.

-

Anonymity Set โ€” Legacy: All P2WSH UTXOs.

-

Anonymity Set โ€” Taproot: All Taproot Single Sig UTXOs.

+

Makers Recorded: ${report.makersCount || report.makerAddresses.length}

+

Protocol Metadata: ${report.protocol || 'Not explicitly included in this report file.'}

`; } @@ -926,10 +854,8 @@ export function SwapReportComponent(container, swapReport) { `; }).join('')} - ${isV2Swap ? ` - ${report.protocol} - MuSig2 - ` : ''} + Private Route + ${actualMakers} makers
`; @@ -1075,15 +1001,14 @@ export function SwapReportComponent(container, swapReport) { - -
-

- โ„น๏ธ Protocol Details -

-
- ${getProtocolInfoLines()} -
-
+
+

+ โ„น๏ธ Report Summary +

+
+ ${getReportInfoLines()} +
+
@@ -1109,24 +1034,24 @@ export function SwapReportComponent(container, swapReport) {
-

${isV2Swap ? 'On-Chain TXs' : 'Privacy Hops'}

+

On-Chain Artifacts

๐Ÿ”—

- ${report.totalFundingTxs} + ${report.artifactsCount}

- ${isV2Swap ? 'Funding transactions observed' : `${report.makersCount} makers used`} + Transaction IDs extracted from this report

-

Protocol

- ${report.protocol === 'Taproot' ? 'โšก' : report.protocol === 'Unified' ? 'โ—ˆ' : '๐Ÿ”’'} +

Swap Partners

+ ๐Ÿค
-

${report.protocol}

-

Rendered from normalized report data

+

${report.makersCount || report.makerAddresses.length}

+

Makers recorded in the report

@@ -1145,13 +1070,13 @@ export function SwapReportComponent(container, swapReport) {
- +

- ๐Ÿ“ Funding Transactions + ๐Ÿ“ Transaction Artifacts

- ${buildFundingTxsHtml()} + ${buildTransactionArtifactsHtml()}
diff --git a/src/styles/output.css b/src/styles/output.css index ca89c79..364bb9b 100644 --- a/src/styles/output.css +++ b/src/styles/output.css @@ -785,9 +785,6 @@ border-left-style: var(--tw-border-style); border-left-width: 4px; } - .border-\[\#10B981\] { - border-color: #10B981; - } .border-\[\#FF6B35\] { border-color: #FF6B35; } @@ -1258,10 +1255,6 @@ .text-\[10px\] { font-size: 10px; } - .leading-6 { - --tw-leading: calc(var(--spacing) * 6); - line-height: calc(var(--spacing) * 6); - } .leading-none { --tw-leading: 1; line-height: 1; @@ -1528,13 +1521,6 @@ } } } - .hover\:border-\[\#FF6B35\]\/50 { - &:hover { - @media (hover: hover) { - border-color: color-mix(in oklab, #FF6B35 50%, transparent); - } - } - } .hover\:border-gray-600 { &:hover { @media (hover: hover) { @@ -1646,13 +1632,6 @@ } } } - .hover\:text-\[\#10B981\] { - &:hover { - @media (hover: hover) { - color: #10B981; - } - } - } .hover\:text-\[\#FF6B35\] { &:hover { @media (hover: hover) {