diff --git a/api1.js b/api1.js
index 13ba75e..03937b4 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,201 @@ 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 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 =
+ 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 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 =
+ 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;
+ 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,
+ report: nestedReport,
+ swapId: normalizedSwapId,
+ nativeSwapId,
+ appSwapId,
+ status: normalizedStatus,
+ completedAt,
+ filePath,
+ fileName,
+ isCoreReport,
+ protocol,
+ isTaproot,
+ protocolVersion,
+ };
+}
+
+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 +622,7 @@ function registerTakerHandlers() {
clearInterval(api1State.walletSyncInterval);
api1State.walletSyncInterval = null;
}
- api1State.takerInstance.shutdown();
+ safelyShutdownTaker(api1State.takerInstance);
} catch (err) {
console.error('โ ๏ธ Shutdown error:', err);
}
@@ -413,16 +653,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 +691,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 +751,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 +788,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 +803,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 +908,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 +1033,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 +1132,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 +1256,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 +1290,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 +1369,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 +1406,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 +1534,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,23 +1560,43 @@ 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) || {};
+ 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: 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.isTaproot || protocol === 'v2',
- protocolVersion: protocol === 'v2' ? 2 : 1,
+ 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);
- saveSwapReport(swapId, swapData);
} else if (msg.type === 'error') {
const existingSwap = api1State.activeSwaps.get(swapId);
const swapData = {
@@ -1346,7 +1609,6 @@ function registerCoinswapHandlers() {
failedAt: Date.now(),
};
api1State.activeSwaps.set(swapId, swapData);
- saveSwapReport(swapId, swapData);
}
});
@@ -1380,36 +1642,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 +1653,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..6c59af9 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 () => {
@@ -12,18 +12,16 @@ 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`);
- // 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
@@ -52,26 +50,44 @@ const { parentPort, workerData } = require('worker_threads');
parentPort.postMessage({
type: 'status',
status: 'in_progress',
- protocol: config.protocol,
- isTaproot: protocol === 'v2',
+ protocol: normalizedProtocol || config.protocol,
});
- // Run the coinswap
const swapParams = {
+ protocol: normalizedProtocol,
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: normalizedProtocol || config.protocol,
+ nativeSwapId: swapId,
+ });
+
+ console.log(`๐ Starting ${protocolName} coinswap...`);
+ const report = taker.startCoinswap(swapId);
// Send success message
parentPort.postMessage({
type: 'complete',
- report,
- protocol: config.protocol || 'v1',
- isTaproot: (config.protocol || 'v1') === 'v2',
+ report: {
+ ...report,
+ nativeSwapId: swapId,
+ appSwapId: config.appSwapId,
+ protocol: swapParams.protocol || normalizedProtocol || config.protocol,
+ },
+ protocol: normalizedProtocol || config.protocol,
+ nativeSwapId: swapId,
+ appSwapId: config.appSwapId,
});
} catch (error) {
// Send error message
diff --git a/offerbook-worker.js b/offerbook-worker.js
index 3ff2bb0..c62a539 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,62 @@ 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));
+
+ let stat;
+
+ try {
+ stat = fs.statSync(offerbookPath);
+ } catch (error) {
+ if (error.code === 'ENOENT') {
+ continue;
+ }
+
+ throw error;
+ }
+
+ if (!stat) {
+ continue;
+ }
+
+ 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.
-
-
-
-
-
-
-
-
Bitcoin Endpoints
Connect to a running bitcoind RPC+REST & ZMQ Ports. This is needed to sync the wallet and market data.
@@ -218,7 +152,7 @@ export function FirstTimeSetupModal(container, onComplete) {
-
+
Choose A Wallet. Or Create a New One.
@@ -481,8 +415,8 @@ export function FirstTimeSetupModal(container, onComplete) {
-
-
+
+
Tor Configuration
Connect with the Tor Proxy. This is needed for all network communications.
@@ -579,7 +513,7 @@ export function FirstTimeSetupModal(container, onComplete) {
- Get Started
+ Next
@@ -727,8 +661,6 @@ export function FirstTimeSetupModal(container, onComplete) {
// Determine which screen to show for each wizard step.
let stepToShow = `step-${step}`;
if (step === 3) {
- stepToShow = 'step-4';
- } else if (step === 4) {
if (!walletAction) {
stepToShow = 'step-3a'; // Show choice screen
} else if (walletAction === 'create') {
@@ -1194,36 +1126,6 @@ export function FirstTimeSetupModal(container, onComplete) {
console.log('๐ง Attaching event listeners...');
- // Protocol selection
- const protocolV2 = modal.querySelector('#protocol-v2');
- const protocolV1 = modal.querySelector('#protocol-v1');
-
- if (protocolV2) {
- protocolV2.addEventListener('click', () => {
- protocolVersion = 'v2';
- modal.querySelectorAll('.protocol-choice').forEach((el) => {
- el.classList.remove('border-[#FF6B35]');
- el.classList.add('border-gray-700');
- });
- protocolV2.classList.remove('border-gray-700');
- protocolV2.classList.add('border-[#FF6B35]');
- console.log('Protocol selected: V2 (Taproot)');
- });
- }
-
- if (protocolV1) {
- protocolV1.addEventListener('click', () => {
- protocolVersion = 'v1';
- modal.querySelectorAll('.protocol-choice').forEach((el) => {
- el.classList.remove('border-[#FF6B35]');
- el.classList.add('border-gray-700');
- });
- protocolV1.classList.remove('border-gray-700');
- protocolV1.classList.add('border-[#FF6B35]');
- console.log('Protocol selected: V1 (P2WSH)');
- });
- }
-
// Wallet action choice
const choiceCreate = modal.querySelector('#choice-create');
if (choiceCreate) {
@@ -1361,7 +1263,7 @@ export function FirstTimeSetupModal(container, onComplete) {
walletAction
);
- if (currentStep === 4) {
+ if (currentStep === 3) {
const valid = await validateWalletStep();
if (!valid) {
console.log('Validation failed');
@@ -1393,7 +1295,7 @@ export function FirstTimeSetupModal(container, onComplete) {
// Back button
modal.querySelector('#setup-back-btn').addEventListener('click', () => {
- if (currentStep === 4 && walletAction) {
+ if (currentStep === 3 && walletAction) {
// If in step 3 substep, go back to step 3a (choice)
walletAction = null;
showStep(currentStep);
diff --git a/src/components/swap/Coinswap.js b/src/components/swap/Coinswap.js
index 7568aa7..ad2aa29 100644
--- a/src/components/swap/Coinswap.js
+++ b/src/components/swap/Coinswap.js
@@ -1,6 +1,21 @@
import { SwapStateManager, formatElapsedTime } from './SwapStateManager.js';
export async function CoinswapComponent(container, swapConfig) {
+ function normalizeProtocol(value, fallbackIsTaproot = false) {
+ switch (value) {
+ case 'v2':
+ case 'Taproot':
+ return 'Taproot';
+ case 'Unified':
+ return 'Unified';
+ case 'v1':
+ case 'Legacy':
+ case 'Legacy P2WSH':
+ default:
+ return fallbackIsTaproot ? 'Taproot' : 'Legacy';
+ }
+ }
+
const content = document.createElement('div');
content.id = 'coinswap-content';
@@ -28,7 +43,11 @@ export async function CoinswapComponent(container, swapConfig) {
let logMessages = savedProgress ? savedProgress.logMessages || [] : [];
let currentHop = 0;
- const isV2 = actualSwapConfig.isTaproot || false;
+ const swapProtocol = normalizeProtocol(
+ actualSwapConfig?.protocol,
+ actualSwapConfig?.isTaproot || false
+ );
+ const isV2 = swapProtocol === 'Taproot';
const swapData = {
amount: actualSwapConfig.amount,
@@ -159,6 +178,20 @@ export async function CoinswapComponent(container, swapConfig) {
}
}
+ function markAllMakersComplete() {
+ for (let i = 0; i < swapData.makers; i++) {
+ updateMakerVisibility(i, true);
+ updateHopStatus(i, 'โ Complete', 'green');
+ }
+ }
+
+ function markAllMakersFailed() {
+ for (let i = 0; i < swapData.makers; i++) {
+ updateMakerVisibility(i, true);
+ updateHopStatus(i, 'Failed', 'orange');
+ }
+ }
+
function updateYouSend(active) {
const youSend = content.querySelector('#you-send');
if (youSend) youSend.style.opacity = active ? '1' : '0.5';
@@ -232,6 +265,12 @@ export async function CoinswapComponent(container, swapConfig) {
// Add raw message to log
addLog(message, type);
+ const makerIndexMatch = message.match(/maker\s+(\d+)/i);
+ const makerIndex =
+ makerIndexMatch && Number.isFinite(Number(makerIndexMatch[1]))
+ ? Number(makerIndexMatch[1])
+ : null;
+
// Update UI based on message content (supports both V1 and V2 protocols)
// V1: "Initiating coinswap with id" | V2: "Initiating coinswap with id"
@@ -425,6 +464,71 @@ export async function CoinswapComponent(container, swapConfig) {
) {
updateHopStatus(0, 'Offers received', 'blue');
}
+ // V2: "Sending contract data to maker N"
+ else if (
+ makerIndex !== null &&
+ message.includes('Sending contract data to maker')
+ ) {
+ updateMakerVisibility(makerIndex, true);
+ updateHopStatus(makerIndex, 'Contracting...', 'yellow');
+ }
+ // V2: "Received Taproot contract data from maker N"
+ else if (
+ makerIndex !== null &&
+ message.includes('Received Taproot contract data from maker')
+ ) {
+ updateMakerVisibility(makerIndex, true);
+ updateHopStatus(makerIndex, 'Contract received', 'blue');
+ }
+ // V2: "Verified Taproot contract data from maker N"
+ else if (
+ makerIndex !== null &&
+ message.includes('Verified Taproot contract data from maker')
+ ) {
+ updateMakerVisibility(makerIndex, true);
+ updateHopStatus(makerIndex, 'โ Contract ready', 'green');
+ }
+ // V2: "Received private key from maker N"
+ else if (
+ makerIndex !== null &&
+ message.includes('Received private key from maker')
+ ) {
+ updateMakerVisibility(makerIndex, true);
+ updateHopStatus(makerIndex, '๐ Key received', 'green');
+ }
+ // V2: "Sending privkey to maker N and awaiting response"
+ else if (
+ makerIndex !== null &&
+ message.includes('Sending privkey to maker') &&
+ message.includes('awaiting response')
+ ) {
+ updateMakerVisibility(makerIndex, true);
+ updateHopStatus(makerIndex, 'Finalizing...', 'yellow');
+ }
+ // V2: "Exchanging contract data with makers..."
+ else if (message.includes('Exchanging contract data with makers')) {
+ for (let i = 0; i < swapData.makers; i++) {
+ updateMakerVisibility(i, true);
+ updateHopStatus(i, 'Exchanging...', 'yellow');
+ }
+ }
+ // V2: "Finalizing swap..."
+ else if (message.includes('Finalizing swap')) {
+ for (let i = 0; i < swapData.makers; i++) {
+ updateMakerVisibility(i, true);
+ updateHopStatus(i, 'Finalizing...', 'yellow');
+ }
+ }
+ // V2: "Swap finalized successfully"
+ else if (message.includes('Swap finalized successfully')) {
+ markAllMakersComplete();
+ updateYouReceive(true);
+ }
+ // V2: "Sweeping N completed incoming swap coins"
+ else if (message.includes('Sweeping') && message.includes('completed incoming swap coins')) {
+ markAllMakersComplete();
+ updateYouReceive(true);
+ }
// V2: Recovery started
else if (message.includes('Starting taproot swap recovery')) {
addLog('Recovery initiated...', 'warn');
@@ -471,6 +575,9 @@ export async function CoinswapComponent(container, swapConfig) {
currentStep = 1;
addLog('Coinswap started...', 'info');
addLog(`Swap ID: ${swapId}`, 'info');
+ if (actualSwapConfig.nativeSwapId) {
+ addLog(`Backend Swap ID: ${actualSwapConfig.nativeSwapId}`, 'info');
+ }
addLog(`Amount: ${(swapData.amount / 100000000).toFixed(8)} BTC`, 'info');
addLog(`Makers: ${swapData.makers}`, 'info');
@@ -527,6 +634,16 @@ export async function CoinswapComponent(container, swapConfig) {
const swap = result.swap;
+ if (swap.nativeSwapId && actualSwapConfig.nativeSwapId !== swap.nativeSwapId) {
+ actualSwapConfig.nativeSwapId = swap.nativeSwapId;
+ addLog(`Backend Swap ID: ${swap.nativeSwapId}`, 'info');
+ }
+
+ if (swap.status === 'prepared') {
+ addLog('Swap prepared, starting execution...', 'info');
+ return;
+ }
+
if (swap.status === 'completed') {
clearInterval(pollInterval);
clearInterval(logPollInterval);
@@ -534,10 +651,10 @@ export async function CoinswapComponent(container, swapConfig) {
console.log('๐ฏ Swap completed! Report data:', swap.report);
if (swap.report) {
- completeSwapWithReport(swap.report);
+ await completeSwapWithReport(swap.report);
} else {
console.warn('โ ๏ธ No report from backend, using default');
- completeSwap();
+ await completeSwap();
}
} else if (swap.status === 'failed') {
if (pollInterval) {
@@ -553,13 +670,15 @@ export async function CoinswapComponent(container, swapConfig) {
content.dataset.failed = 'true';
addLog('Swap failed: ' + swap.error, 'error');
+ updateHeaderState('failed');
+ markAllMakersFailed();
content.querySelector('#swap-status-text').textContent =
'Swap Failed';
content.querySelector('#swap-status-text').className =
'text-2xl font-bold text-red-400';
- SwapStateManager.saveSwapProgress({
- ...SwapStateManager.getSwapProgress(),
+ await SwapStateManager.saveSwapProgress({
+ ...(await SwapStateManager.getSwapProgress()),
status: 'failed',
error: swap.error,
});
@@ -628,7 +747,7 @@ export async function CoinswapComponent(container, swapConfig) {
});
}
- function completeSwapWithReport(report) {
+ async function completeSwapWithReport(report) {
if (content.dataset.completed === 'true') return;
content.dataset.completed = 'true';
@@ -639,28 +758,33 @@ export async function CoinswapComponent(container, swapConfig) {
addLog('Generating swap report...', 'success');
+ updateHeaderState('completed');
+ markAllMakersComplete();
+ updateYouReceive(true);
content.querySelector('#swap-status-text').textContent = 'Swap Complete!';
content.querySelector('#swap-status-text').className =
'text-2xl font-bold text-green-400';
content.querySelector('#complete-button').classList.remove('hidden');
const transformedReport = transformSwapReport(report);
- transformedReport.protocol = actualSwapConfig.protocol || 'v1';
- transformedReport.isTaproot = actualSwapConfig.isTaproot || false;
- transformedReport.protocolVersion = actualSwapConfig.isTaproot ? 2 : 1;
+ transformedReport.protocol = swapProtocol;
+ transformedReport.isTaproot = swapProtocol === 'Taproot';
+ transformedReport.protocolVersion = swapProtocol === 'Taproot' ? 2 : 1;
+ transformedReport.nativeSwapId =
+ transformedReport.nativeSwapId || actualSwapConfig.nativeSwapId || null;
actualSwapConfig.swapReport = transformedReport;
- SwapStateManager.saveSwapProgress({
- ...SwapStateManager.getSwapProgress(),
+ await SwapStateManager.saveSwapProgress({
+ ...(await SwapStateManager.getSwapProgress()),
status: 'completed',
report: transformedReport,
});
- SwapStateManager.completeSwap(transformedReport);
+ await SwapStateManager.completeSwap(transformedReport);
setTimeout(() => {
- SwapStateManager.clearSwapData();
+ void SwapStateManager.clearSwapData();
}, 3000);
if (window.appManager) window.appManager.stopBackgroundSwapManager();
@@ -687,6 +811,11 @@ export async function CoinswapComponent(container, swapConfig) {
'swapId',
actualSwapConfig.swapId || 'unknown'
);
+ const nativeSwapId = getValue(
+ 'native_swap_id',
+ 'nativeSwapId',
+ actualSwapConfig.nativeSwapId || null
+ );
const swapDurationSeconds = getValue(
'swap_duration_seconds',
'swapDurationSeconds',
@@ -774,20 +903,28 @@ export async function CoinswapComponent(container, swapConfig) {
};
});
- const derivedTotalFee = Number.isFinite(feePaidOrEarned)
+ const componentTotalFee = totalMakerFees + Math.max(0, miningFee);
+ const netFeePaidOrEarned = Number.isFinite(feePaidOrEarned)
? Math.abs(feePaidOrEarned)
- : totalMakerFees + miningFee;
+ : NaN;
const normalizedTotalFee =
- Number.isFinite(totalFee) && (totalFee > 0 || derivedTotalFee <= 0)
+ Number.isFinite(totalFee) && totalFee >= 0
? totalFee
- : derivedTotalFee;
+ : componentTotalFee > 0
+ ? componentTotalFee
+ : Number.isFinite(netFeePaidOrEarned)
+ ? netFeePaidOrEarned
+ : 0;
const calculatedMiningFee =
- miningFee || normalizedTotalFee - totalMakerFees;
+ Number.isFinite(miningFee) && miningFee >= 0
+ ? miningFee
+ : Math.max(0, normalizedTotalFee - totalMakerFees);
const feePercentage =
targetAmount > 0 ? (normalizedTotalFee / targetAmount) * 100 : 0;
return {
swapId,
+ nativeSwapId,
swapDurationSeconds,
targetAmount,
totalInputAmount,
@@ -812,6 +949,7 @@ export async function CoinswapComponent(container, swapConfig) {
function getDefaultReport() {
return {
swapId: actualSwapConfig.swapId || 'unknown',
+ nativeSwapId: actualSwapConfig.nativeSwapId || null,
swapDurationSeconds: (Date.now() - startTime) / 1000,
targetAmount: swapData.amount || 0,
totalInputAmount: swapData.amount || 0,
@@ -831,19 +969,22 @@ export async function CoinswapComponent(container, swapConfig) {
};
}
- function completeSwap() {
+ async function completeSwap() {
if (content.dataset.completed === 'true') return;
content.dataset.completed = 'true';
addLog('All operations completed!', 'success');
+ updateHeaderState('completed');
+ markAllMakersComplete();
+ updateYouReceive(true);
content.querySelector('#swap-status-text').textContent = 'Swap Complete!';
content.querySelector('#swap-status-text').className =
'text-2xl font-bold text-green-400';
content.querySelector('#complete-button').classList.remove('hidden');
const defaultReport = getDefaultReport();
- SwapStateManager.completeSwap(defaultReport);
+ await SwapStateManager.completeSwap(defaultReport);
actualSwapConfig.swapReport = defaultReport;
if (window.appManager) window.appManager.stopBackgroundSwapManager();
@@ -1188,7 +1329,7 @@ export async function CoinswapComponent(container, swapConfig) {
}).join('')}
${isV2 ? `
-
Taproot V2
+
${swapProtocol}
MuSig2
` : ''}
@@ -1224,15 +1365,43 @@ export async function CoinswapComponent(container, swapConfig) {
}
}
+ function updateHeaderState(state) {
+ const titleEl = content.querySelector('#swap-page-title');
+ const badgeEl = content.querySelector('#swap-page-badge');
+ const badgeDotEl = content.querySelector('#swap-page-badge-dot');
+ const badgeTextEl = content.querySelector('#swap-page-badge-text');
+
+ if (!titleEl || !badgeEl || !badgeDotEl || !badgeTextEl) return;
+
+ if (state === 'completed') {
+ titleEl.textContent = 'Coinswap Complete';
+ badgeEl.className =
+ 'flex items-center gap-1.5 px-2.5 py-1 bg-green-500/10 border border-green-500/20 rounded-full';
+ badgeDotEl.className = 'w-2 h-2 rounded-full bg-green-400';
+ badgeTextEl.className = 'text-xs text-green-400 font-medium';
+ badgeTextEl.textContent = 'Complete';
+ return;
+ }
+
+ if (state === 'failed') {
+ titleEl.textContent = 'Coinswap Failed';
+ badgeEl.className =
+ 'flex items-center gap-1.5 px-2.5 py-1 bg-red-500/10 border border-red-500/20 rounded-full';
+ badgeDotEl.className = 'w-2 h-2 rounded-full bg-red-400';
+ badgeTextEl.className = 'text-xs text-red-400 font-medium';
+ badgeTextEl.textContent = 'Failed';
+ }
+ }
+
content.innerHTML = `
โ Back
-
Coinswap in Progress
-
-
- Active
+ Coinswap in Progress
+
+
+ Active
Executing swap through ${swapData.makers} makers...
@@ -1250,7 +1419,7 @@ export async function CoinswapComponent(container, swapConfig) {
${(swapData.amount / 100000000).toFixed(8)} BTC
${!isV2
? `
Hops: ${swapData.hops}
`
- : `
Taproot V2 (MuSig2)
`
+ : `
${swapProtocol} swap
`
}
diff --git a/src/components/swap/Swap.js b/src/components/swap/Swap.js
index 4f725e0..071ed97 100644
--- a/src/components/swap/Swap.js
+++ b/src/components/swap/Swap.js
@@ -70,13 +70,22 @@ export async function SwapComponent(container) {
console.log('๐ Active swap check:', { activeSwap, hasActiveSwap });
// If there's an active swap in progress, redirect to coinswap progress
- if (activeSwap && activeSwap.status === 'configured') {
- const age = Date.now() - activeSwap.createdAt;
- if (age > 15 * 60 * 1000) {
- console.log('๐งน Clearing stale configured swap');
- await SwapStateManager.clearSwapData();
- } else {
- console.log('๐ Active swap detected, redirecting to progress view');
+ if (activeSwap && hasActiveSwap) {
+ if (activeSwap.status === 'configured') {
+ const age = Date.now() - activeSwap.createdAt;
+ if (age > 15 * 60 * 1000) {
+ console.log('๐งน Clearing stale configured swap');
+ await SwapStateManager.clearSwapData();
+ } else {
+ console.log('๐ Configured swap detected, redirecting to progress view');
+ import('./Coinswap.js').then((module) => {
+ container.innerHTML = '';
+ module.CoinswapComponent(container, activeSwap);
+ });
+ return; // Exit early, don't render the config page
+ }
+ } else if (activeSwap.status === 'in_progress') {
+ console.log('๐ In-progress swap detected, redirecting to progress view');
import('./Coinswap.js').then((module) => {
container.innerHTML = '';
module.CoinswapComponent(container, activeSwap);
@@ -142,12 +151,19 @@ 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) =>
- currentProtocol === 'v2'
- ? maker.protocol === 'Taproot'
- : maker.protocol !== 'Taproot'
- );
+ return makers.filter((maker) => {
+ const makerProtocol = getMakerProtocol(maker);
+ // Unified treated as compatible but not yet a user-selectable mode.
+ if (makerProtocol === 'Unified') return true;
+ return currentProtocol === 'v2'
+ ? makerProtocol === 'Taproot'
+ : makerProtocol === 'Legacy';
+ });
}
function updateAvailableMakersCount() {
@@ -243,7 +259,7 @@ export async function SwapComponent(container) {
baseFee: offer.baseFee || 0,
volumeFeePct: offer.amountRelativeFeePct || 0,
timeFeePct: offer.timeRelativeFeePct || 0,
- protocol: item.protocol || 'Legacy',
+ protocol: getMakerProtocol(item, offer),
index: index,
};
})
@@ -1261,12 +1277,12 @@ export async function SwapComponent(container) {
// Filter makers by protocol
const compatibleMakers = goodMakers.filter((maker) => {
- const makerProtocol = maker.protocol;
- if (protocol === 'v2') {
- return makerProtocol === 'Taproot';
- } else {
- return makerProtocol === 'Legacy';
- }
+ const makerProtocol = getMakerProtocol(maker);
+ // Unified treated as compatible but not yet a user-selectable mode.
+ if (makerProtocol === 'Unified') return true;
+ return protocol === 'v2'
+ ? makerProtocol === 'Taproot'
+ : makerProtocol === 'Legacy';
});
const makersNeeded = getNumberOfMakers();
@@ -1340,7 +1356,7 @@ export async function SwapComponent(container) {
console.log('โ
Swap started with ID:', result.swapId);
swapConfig.swapId = result.swapId;
- SwapStateManager.saveSwapConfig(swapConfig);
+ await SwapStateManager.saveSwapConfig(swapConfig);
if (window.appManager) {
console.log('๐ Starting background swap manager');
diff --git a/src/components/swap/SwapHistory.js b/src/components/swap/SwapHistory.js
index b2cc9de..fa3f8e6 100644
--- a/src/components/swap/SwapHistory.js
+++ b/src/components/swap/SwapHistory.js
@@ -25,14 +25,28 @@ function formatDate(timestamp) {
}
function getProtocolLabel(report) {
- const protocol = report.protocol || report.report?.protocol || 'v1';
- return protocol === 'v2' ? 'Taproot' : 'Legacy P2WSH';
+ const protocol = report.protocol || report.report?.protocol;
+ switch (protocol) {
+ case 'v2':
+ case 'Taproot':
+ return 'Taproot';
+ case 'Unified':
+ return 'Unified';
+ case 'v1':
+ case 'Legacy':
+ default:
+ return 'Legacy';
+ }
}
function getProtocolBadgeClasses(protocolLabel) {
- return protocolLabel === 'Taproot'
- ? 'bg-purple-500/20 text-purple-400'
- : 'bg-blue-500/20 text-blue-400';
+ if (protocolLabel === 'Taproot') {
+ return 'bg-purple-500/20 text-purple-400';
+ }
+ if (protocolLabel === 'Unified') {
+ return 'bg-emerald-500/20 text-emerald-400';
+ }
+ return 'bg-blue-500/20 text-blue-400';
}
function normalizeSwapReport(report) {
@@ -115,7 +129,10 @@ function normalizeSwapReport(report) {
Math.floor((completedAt - startedAt) / 1000) || 0
),
status: report.status || 'completed',
- protocol: report.protocol || nested.protocol || 'v1',
+ protocol:
+ report.protocol ||
+ nested.protocol ||
+ (report.isTaproot ? 'Taproot' : nested.isTaproot ? 'Taproot' : 'v1'),
report: nested,
};
}
@@ -193,6 +210,8 @@ export function buildSwapHistoryMarkup(history) {
const totalOutputAmount = Number(swap.totalOutputAmount) || 0;
const feePercentage = Number(swap.feePercentage) || 0;
const totalFee = Number(swap.totalFee) || 0;
+ const protocolLabel = getProtocolLabel(swap);
+ const protocolClasses = getProtocolBadgeClasses(protocolLabel);
const btcAmount = satsToBtc(amount);
const outputBtc = satsToBtc(totalOutputAmount);
const timeAgo = formatRelativeTime(swap.completedAt);
@@ -210,6 +229,7 @@ export function buildSwapHistoryMarkup(history) {
Coinswap
Completed
${swap.hops} hops
+
${protocolLabel}
${timeAgo}
diff --git a/src/components/swap/SwapReport.js b/src/components/swap/SwapReport.js
index d6dc1ac..5e169de 100644
--- a/src/components/swap/SwapReport.js
+++ b/src/components/swap/SwapReport.js
@@ -1,4 +1,20 @@
export function SwapReportComponent(container, swapReport) {
+ function normalizeProtocol(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';
+ }
+ }
+
console.log('๐ SwapReportComponent loading with report:', swapReport);
console.log('๐ Report keys:', Object.keys(swapReport || {}));
@@ -25,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(
@@ -56,27 +87,42 @@ export function SwapReportComponent(container, swapReport) {
nestedReport.total_fee,
NaN
);
- const derivedTotalFee = Number.isFinite(rawFeePaidOrEarned)
+ const componentTotalFee = rawTotalMakerFees + Math.max(0, rawMiningFee);
+ const netFeePaidOrEarned = Number.isFinite(rawFeePaidOrEarned)
? Math.abs(rawFeePaidOrEarned)
- : rawTotalMakerFees + rawMiningFee;
+ : NaN;
const rawTotalFee =
- Number.isFinite(providedTotalFee) &&
- (providedTotalFee > 0 || derivedTotalFee <= 0)
+ Number.isFinite(providedTotalFee) && providedTotalFee >= 0
? providedTotalFee
- : derivedTotalFee;
+ : componentTotalFee > 0
+ ? componentTotalFee
+ : Number.isFinite(netFeePaidOrEarned)
+ ? netFeePaidOrEarned
+ : 0;
+ const normalizedMiningFee =
+ rawMiningFee >= 0
+ ? rawMiningFee
+ : Math.max(0, rawTotalFee - rawTotalMakerFees);
// Extract values with safe defaults
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 ??
+ swapReport.incomingAmount ??
+ swapReport.incoming_amount ??
+ swapReport.outgoingAmount ??
+ swapReport.outgoing_amount ??
nestedReport.targetAmount ??
nestedReport.target_amount ??
swapReport.amount ??
@@ -91,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 ??
@@ -101,8 +147,48 @@ export function SwapReportComponent(container, swapReport) {
normalizedTargetAmount > 0 ? (rawTotalFee / normalizedTargetAmount) * 100 : 0
);
+ const protocol = normalizeProtocol(
+ 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',
+ nativeSwapId:
+ swapReport.nativeSwapId ||
+ swapReport.native_swap_id ||
+ nestedReport.nativeSwapId ||
+ nestedReport.native_swap_id ||
+ null,
swapDurationSeconds:
toNumber(
swapReport.swapDurationSeconds ??
@@ -116,6 +202,8 @@ export function SwapReportComponent(container, swapReport) {
toNumber(
swapReport.totalInputAmount ??
swapReport.total_input_amount ??
+ swapReport.incomingAmount ??
+ swapReport.incoming_amount ??
nestedReport.totalInputAmount ??
nestedReport.total_input_amount,
normalizedTargetAmount
@@ -124,6 +212,10 @@ export function SwapReportComponent(container, swapReport) {
toNumber(
swapReport.totalOutputAmount ??
swapReport.total_output_amount ??
+ swapReport.outgoingAmount ??
+ swapReport.outgoing_amount ??
+ swapReport.incomingAmount ??
+ swapReport.incoming_amount ??
nestedReport.totalOutputAmount ??
nestedReport.total_output_amount ??
nestedReport.outgoingAmount ??
@@ -145,9 +237,10 @@ export function SwapReportComponent(container, swapReport) {
[],
totalFundingTxs: normalizedTotalFundingTxs,
fundingTxidsByHop: normalizedFundingTxids,
+ fundingTxids: flattenedFundingTxids,
totalFee: rawTotalFee,
totalMakerFees: rawTotalMakerFees,
- miningFee: rawMiningFee,
+ miningFee: normalizedMiningFee,
feePercentage: normalizedFeePercentage,
makerFeeInfo:
swapReport.makerFeeInfo ||
@@ -175,21 +268,23 @@ 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: swapReport.protocol || 'v1',
- isTaproot: swapReport.isTaproot || false,
- protocolVersion: swapReport.protocolVersion || 1,
+ sweepTxid,
+ protocol: hasExplicitProtocolMetadata ? protocol : null,
+ isTaproot:
+ protocol === 'Taproot' ||
+ swapReport.isTaproot ||
+ nestedReport.isTaproot ||
+ false,
+ protocolVersion:
+ swapReport.protocolVersion ||
+ (protocol === 'Taproot' ? 2 : 1),
+ outgoingContractTxid,
+ incomingContractTxid,
+ recoveryTxids,
};
console.log('๐ Normalized report:', report);
- const isV2Swap = report.isTaproot || false;
-
// Helper functions
function satsToBtc(sats) {
if (typeof sats !== 'number' || isNaN(sats)) return '0.00000000';
@@ -356,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('');
}
@@ -550,21 +555,26 @@ export function SwapReportComponent(container, swapReport) {
.join('');
}
- function getProtocolInfoLines() {
+ 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.'}
`;
}
// Build swap circuit visualization (circular SVG)
function buildCircularFlowHtml() {
+ const isFinitePoint = (point) =>
+ point &&
+ Number.isFinite(point.x) &&
+ Number.isFinite(point.y);
+
const makersCount = Number(report.makersCount);
const actualMakers = Math.max(
0,
@@ -740,6 +750,40 @@ export function SwapReportComponent(container, swapReport) {
const { centerX, centerY, svgWidth, svgHeight, positions, guideMarkup } =
buildAdaptiveLayout();
+ const hasValidLayout =
+ Number.isFinite(centerX) &&
+ Number.isFinite(centerY) &&
+ Number.isFinite(svgWidth) &&
+ Number.isFinite(svgHeight) &&
+ Array.isArray(positions) &&
+ positions.length === totalNodes &&
+ positions.every(isFinitePoint);
+
+ if (!hasValidLayout) {
+ console.warn('โ ๏ธ Invalid swap circuit layout, falling back to simple list', {
+ actualMakers,
+ totalNodes,
+ centerX,
+ centerY,
+ svgWidth,
+ svgHeight,
+ positions,
+ });
+
+ return `
+
+
Swap path visualization unavailable for this report.
+
+
You
+ ${Array.from({ length: actualMakers }, (_, i) => {
+ const addr = report.makerAddresses[i] || `Maker ${i + 1}`;
+ const color = makerColors[i % makerColors.length];
+ return `
Maker ${i + 1}: ${truncateAddress(addr)}
`;
+ }).join('')}
+
+
+ `;
+ }
return `
@@ -766,13 +810,16 @@ export function SwapReportComponent(container, swapReport) {
const color = i < actualMakers ? makerColors[i % makerColors.length] : '#10B981';
const fromHalf = i === 0 ? youHalf : makerHalf;
const toHalf = (i + 1) % positions.length === 0 ? youHalf : makerHalf;
+ if (!isFinitePoint(pos) || !isFinitePoint(nextPos)) return '';
const dx = nextPos.x - pos.x;
const dy = nextPos.y - pos.y;
const len = Math.sqrt(dx * dx + dy * dy);
+ if (!Number.isFinite(len) || len <= 0) return '';
const sx = pos.x + (dx / len) * (fromHalf + 4);
const sy = pos.y + (dy / len) * (fromHalf + 4);
const ex = nextPos.x - (dx / len) * (toHalf + 10);
const ey = nextPos.y - (dy / len) * (toHalf + 10);
+ if (![sx, sy, ex, ey].every(Number.isFinite)) return '';
return ` `;
}).join('')}
@@ -807,10 +854,8 @@ export function SwapReportComponent(container, swapReport) {
`;
}).join('')}
- ${isV2Swap ? `
- Taproot V2
- MuSig2
- ` : ''}
+ Private Route
+ ${actualMakers} makers
`;
@@ -921,6 +966,16 @@ export function SwapReportComponent(container, swapReport) {
ID:
${report.swapId}
+ ${
+ report.nativeSwapId
+ ? `
+
+ Backend Swap ID:
+ ${report.nativeSwapId}
+
+ `
+ : ''
+ }
@@ -946,20 +1001,19 @@ export function SwapReportComponent(container, swapReport) {
-
-
-
- โน๏ธ Protocol Details
-
-
- ${getProtocolInfoLines()}
-
-
+
+
+ โน๏ธ Report Summary
+
+
+ ${getReportInfoLines()}
+
+
-