Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
509 changes: 360 additions & 149 deletions api1.js

Large diffs are not rendered by default.

48 changes: 32 additions & 16 deletions coinswap-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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
Expand Down Expand Up @@ -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();
Comment on lines +63 to +64
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Put a deadline around the extra offerbook sync step.

api1.js already waits for a synced offerbook before spawning this worker, but this second syncOfferbookAndWait() has no timeout or watchdog. If Tor/Nostr stalls here, the worker never emits prepared/error, so the swap stays stuck in startup indefinitely. Based on learnings: Timeout handling in swap protocols; Handle network failures during swaps.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@coinswap-worker.js` around lines 63 - 64, The extra
taker.syncOfferbookAndWait() call has no timeout so the worker can hang
indefinitely; wrap this call in a deadline (e.g., Promise.race or a timeout
helper) and treat a timeout as a failure path that logs the error and causes the
worker to emit the existing error/cleanup/failed startup flow (so it will emit
prepared or error instead of hanging). Locate the sync call
(taker.syncOfferbookAndWait) and enforce a configurable timeout (e.g., 5–30s)
and ensure the timeout branch triggers the same error handling used elsewhere in
the startup/prepare logic so the swap doesn't stay stuck.


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,
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} catch (error) {
// Send error message
Expand Down
65 changes: 58 additions & 7 deletions offerbook-worker.js
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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(
Expand All @@ -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;
Comment on lines +31 to +34
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate config.dataDir before using it as a filesystem root.

Line 31 builds offerbook.json directly from workerData.config.dataDir. That makes this worker trust a raw config path for the later statSync() / readFileSync() calls, so a malformed or tampered value can redirect reads outside the expected app data directory.

Based on learnings: Sanitize file paths for wallet operations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@offerbook-worker.js` around lines 31 - 34, The code constructs offerbookPath
from config.dataDir without validating the config input, allowing path
traversal; update the logic that builds offerbookPath (the use of config.dataDir
when creating offerbookPath and before calling
fs.existsSync/fs.statSync/readFileSync) to sanitize and constrain the directory:
require config.dataDir to be an absolute, normalized path (use
path.resolve/path.normalize) and enforce it resides under the approved
application data root (reject or override if it does not), and only then join
the sanitized directory with 'offerbook.json' to form offerbookPath; ensure all
subsequent filesystem calls (fs.existsSync, fs.statSync, readFileSync) use this
validated offerbookPath.


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.
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

parentPort.postMessage({
type: 'completed',
offerbookUpdated: sawUpdatedOfferbook,
});
} catch (err) {
parentPort.postMessage({ type: 'error', error: err.message });
}
Expand Down
2 changes: 1 addition & 1 deletion setup-coinswap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
71 changes: 60 additions & 11 deletions src/components/market/Market.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand All @@ -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 || [];
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand All @@ -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');
}
Expand All @@ -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')); }
Expand All @@ -294,6 +342,7 @@ export function Market(container) {
});

syncProgress = null;
console.log('🔁 [market] Sync finished, reloading makers');
await fetchMakers();

refreshBtn.innerHTML = 'Refreshed!';
Expand All @@ -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';
Expand Down Expand Up @@ -694,16 +744,14 @@ export function Market(container) {
} else {
tableBody.innerHTML = displayedMakers
.map(
(maker) => `
(maker) => {
const protocolBadge = getProtocolPresentation(maker.protocol);
return `
<div class="grid grid-cols-8 gap-4 p-4 hover:bg-[#242d3d] transition-colors">

<div class="text-sm">
<span class="px-2 py-1 ${
maker.protocol === 'Taproot'
? 'bg-purple-500/20 text-purple-400'
: 'bg-blue-500/20 text-blue-400'
} rounded text-xs font-semibold text-lg">
${maker.protocol === 'Taproot' ? '⚡ Taproot' : '🔒 Legacy'}
<span class="px-2 py-1 ${protocolBadge.classes} rounded text-xs font-semibold text-lg">
${protocolBadge.icon} ${protocolBadge.label}
</span>
</div>
<div class="text-gray-300 font-mono text-sm truncate" title="${maker.address}">${maker.address.substring(0, 18)}...</div>
Expand All @@ -716,7 +764,8 @@ export function Market(container) {
${maker.bond > 0 ? maker.bond.toLocaleString() : 'N/A'}
</div>
</div>
`
`;
}
)
.join('');
}
Expand Down
Loading