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
45 changes: 0 additions & 45 deletions .github/workflows/claude-code-review.yml

This file was deleted.

217 changes: 68 additions & 149 deletions api1.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,8 @@ function registerTakerHandlers() {
// ✅ START BACKGROUND SERVICES
setTimeout(async () => {
console.log('🔄 Starting background services...');
await startOfferbookSync('auto');
startPeriodicSync();
// Offerbook sync is triggered explicitly on launch and manually by the user.
// The Rust backend keeps offerbook.json up to date internally.
startPeriodicWalletSync();
console.log('✅ Background services started');
}, 2000);
Expand Down Expand Up @@ -320,9 +320,6 @@ function registerTakerHandlers() {
console.log('🛑 Shutting down taker...');
console.trace('Shutdown called from:'); // ← ADD THIS to see who called it

// Stop periodic syncs
stopPeriodicSync();

// Stop wallet sync
if (api1State.walletSyncInterval) {
clearInterval(api1State.walletSyncInterval);
Expand All @@ -345,138 +342,80 @@ function registerTakerHandlers() {
}
});

async function startOfferbookSync(source = 'manual') {
try {
console.log(`🚀 startOfferbookSync called with source: ${source}`);

if (!api1State.takerInstance) {
console.log('❌ No taker instance!');
return { success: false, error: 'Taker not initialized' };
}

console.log('✅ Taker instance exists');
/**
* Spawn a worker thread to run syncOfferbookAndWait().
*
* The sync MUST run off the main thread — calling it on the main thread
* blocks Electron's entire event loop and triggers the OS "not responding"
* dialog. The worker creates its own Taker instance. At launch this matches
* the main Taker (both cold). Mid-session the worker's Tor circuits warm up
* quickly because the Tor daemon reuses circuits it already established for
* the main Taker.
*
* Returns { success, syncId } immediately. Caller polls getSyncStatus(syncId).
*/
function startSyncWorker(source = 'manual') {
if (!api1State.takerInstance || !api1State.storedTakerConfig) {
return { success: false, error: 'Taker not initialized' };
}

// Check if sync already running
if (api1State.syncState.isRunning) {
console.log(`⏭️ Sync already running (${source}), skipping`);
return {
success: false,
duplicate: true,
existingSyncId: api1State.syncState.currentSyncId,
};
}
if (api1State.syncState.isRunning) {
console.log(`⏭️ Sync already running (${source}), skipping`);
return { success: false, duplicate: true };
}

const syncId = `${source}_${Date.now()}_${Math.random().toString(36).substring(7)}`;
console.log(
`🔄 [${syncId}] Triggering manual offerbook sync (${source})...`
);
const syncId = `${source}_${Date.now()}_${Math.random().toString(36).substring(7)}`;
api1State.syncState.isRunning = true;
api1State.syncState.currentSyncId = syncId;
api1State.activeSyncs.set(syncId, {
status: 'syncing',
startedAt: Date.now(),
source,
});

// Set flags
api1State.syncState.isRunning = true;
api1State.syncState.currentSyncId = syncId;
const workerConfig = {
dataDir: api1State.DATA_DIR,
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',
};

// ✅ FIX: Delete old offerbook and create fresh empty one
const offerbookPath = path.join(
api1State.storedTakerConfig.dataDir,
'offerbook.json'
);
const worker = new Worker(path.join(__dirname, 'offerbook-worker.js'), {
workerData: { config: workerConfig },
});

if (fs.existsSync(offerbookPath)) {
try {
const content = fs.readFileSync(offerbookPath, 'utf8');
JSON.parse(content); // Test if valid
console.log('✅ Existing offerbook is valid');
} catch (parseError) {
console.log('⚠️ Corrupted offerbook detected, recreating...');
fs.writeFileSync(
offerbookPath,
JSON.stringify({ makers: [] }),
'utf8'
);
}
} else {
console.log('📝 Creating initial offerbook.json...');
fs.writeFileSync(offerbookPath, JSON.stringify({ makers: [] }), 'utf8');
}
const finish = (status, extra = {}) => {
api1State.activeSyncs.set(syncId, {
status: 'syncing',
startedAt: Date.now(),
source: source,
...api1State.activeSyncs.get(syncId),
...extra,
status,
completedAt: Date.now(),
});

// ✅ Trigger manual sync
api1State.takerInstance.runOfferSyncNow();

// Monitor until complete
const checkInterval = setInterval(() => {
try {
const isSyncing = api1State.takerInstance.isOfferbookSyncing();

if (!isSyncing) {
clearInterval(checkInterval);

console.log(`✅ Offerbook sync completed (${source})`);
api1State.activeSyncs.set(syncId, {
...api1State.activeSyncs.get(syncId),
status: 'completed',
completedAt: Date.now(),
});

// Clear flags
api1State.syncState.isRunning = false;
api1State.syncState.currentSyncId = null;
api1State.syncState.lastSyncTime = Date.now();
}
} catch (err) {
clearInterval(checkInterval);
console.error(`❌ Sync check failed:`, err);

api1State.syncState.isRunning = false;
api1State.syncState.currentSyncId = null;

api1State.activeSyncs.set(syncId, {
...api1State.activeSyncs.get(syncId),
status: 'failed',
error: err.message,
});
}
}, 1000);

return { success: true, syncId, source };
} catch (error) {
console.error('❌ Sync offerbook failed:', error);
if (status === 'completed') api1State.syncState.lastSyncTime = Date.now();
api1State.syncState.isRunning = false;
api1State.syncState.currentSyncId = null;
return { success: false, error: error.message };
}
}

function startPeriodicSync() {
// Clear any existing interval
if (api1State.syncState.periodicInterval) {
clearInterval(api1State.syncState.periodicInterval);
}
};

console.log('⏰ Starting periodic sync scheduler (every 15 minutes)');
worker.on('message', (msg) => {
if (msg.type === 'completed') {
console.log(`✅ [${syncId}] Offerbook sync completed`);
finish('completed');
} else if (msg.type === 'error') {
console.error(`❌ [${syncId}] Offerbook sync failed:`, msg.error);
finish('failed', { error: msg.error });
}
});

api1State.syncState.periodicInterval = setInterval(
async () => {
console.log('⏰ Periodic sync triggered');
await startOfferbookSync('periodic');
},
15 * 60 * 1000
); // 15 minutes
}
worker.on('error', (err) => {
console.error(`❌ [${syncId}] Offerbook worker error:`, err.message);
finish('failed', { error: err.message });
});

/**
* Stop periodic offerbook syncs
*/
function stopPeriodicSync() {
if (api1State.syncState.periodicInterval) {
clearInterval(api1State.syncState.periodicInterval);
api1State.syncState.periodicInterval = null;
console.log('⏰ Periodic sync scheduler stopped');
}
return { success: true, syncId };
}

// Get wallet info
Expand Down Expand Up @@ -654,25 +593,6 @@ function registerTakerHandlers() {
}
});

// Check if offerbook is syncing
ipcMain.handle('taker:isOfferbookSyncing', async () => {
try {
if (!api1State.takerInstance) {
return { success: false, error: 'Taker not initialized' };
}

const isSyncing = api1State.takerInstance.isOfferbookSyncing();

return {
success: true,
isSyncing: isSyncing,
};
} catch (error) {
console.error('Failed to check offerbook sync status:', error);
return { success: false, error: error.message, isSyncing: false };
}
});

// Get UTXOs
ipcMain.handle('taker:getUtxos', async () => {
try {
Expand Down Expand Up @@ -865,9 +785,8 @@ function registerTakerHandlers() {
}
);

// Sync offerbook
ipcMain.handle('taker:syncOfferbook', async () => {
return await startOfferbookSync('manual');
ipcMain.handle('taker:syncOfferbookAndWait', () => {
return startSyncWorker('manual');
});

// Get sync status
Expand Down Expand Up @@ -1047,8 +966,8 @@ function registerCoinswapHandlers() {

while (retries < maxRetries) {
try {
// Check if sync is still running
const isSyncing = api1State.takerInstance.isOfferbookSyncing();
// Check if app-level sync is still running.
const isSyncing = api1State.syncState.isRunning;

if (!isSyncing) {
// Sync complete - now check if we have enough makers
Expand Down
39 changes: 39 additions & 0 deletions offerbook-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const { parentPort, workerData } = require('worker_threads');

/**
* Worker thread for running offerbook sync operations.
* Creates its own Taker instance so the sync doesn't block the main process IPC.
*/

(async () => {
try {
const coinswapNapi = require('coinswap-napi');
const { config } = workerData;

const protocol = config.protocol || 'v1';
const TakerClass =
protocol === 'v2' ? coinswapNapi.TaprootTaker : coinswapNapi.Taker;

if (!TakerClass) {
throw new Error(
`${protocol === 'v2' ? 'TaprootTaker' : 'Taker'} class not found. Please rebuild coinswap-napi.`
);
}

const taker = new TakerClass(
config.dataDir,
config.walletName || 'taker-wallet',
config.rpcConfig,
config.controlPort || 9051,
config.torAuthPassword || undefined,
config.zmqAddr,
config.password || ''
);

taker.syncOfferbookAndWait();

parentPort.postMessage({ type: 'completed' });
} catch (err) {
parentPort.postMessage({ type: 'error', error: err.message });
}
})();
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ contextBridge.exposeInMainWorld('api', {
getBalance: () => ipcRenderer.invoke('taker:getBalance'),
getNextAddress: () => ipcRenderer.invoke('taker:getNextAddress'),
sync: () => ipcRenderer.invoke('taker:sync'),
syncOfferbook: () => ipcRenderer.invoke('taker:syncOfferbook'),
syncOfferbookAndWait: () =>
ipcRenderer.invoke('taker:syncOfferbookAndWait'),
getSyncStatus: (syncId) =>
ipcRenderer.invoke('taker:getSyncStatus', syncId),
getOffers: () => ipcRenderer.invoke('taker:getOffers'),
Expand All @@ -30,7 +31,6 @@ contextBridge.exposeInMainWorld('api', {
testTorConnection: (config) =>
ipcRenderer.invoke('tor:testConnection', config),
getProtocol: () => ipcRenderer.invoke('taker:getProtocol'),
isOfferbookSyncing: () => ipcRenderer.invoke('taker:isOfferbookSyncing'),
setupLogging: (dataDir, level) =>
ipcRenderer.invoke('taker:setupLogging', { dataDir, level }),

Expand Down
Loading
Loading