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
34 changes: 9 additions & 25 deletions src/components/wallet/Wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -355,23 +355,14 @@ export async function WalletComponent(container) {
try {
await syncWalletState();

// ✅ FORCE FRESH FETCH AFTER WALLET SYNC
const [balance, transactions, utxos] = await Promise.all([
fetchBalance(),
fetchTransactions(),
fetchUtxos(),
]);

// Save to cache
saveWalletToCache(balance, transactions, utxos);

// Update UI
await Promise.all([
updateBalance(false),
updateTransactions(false),
updateUtxos(false),
]);

if (balance) saveWalletToCache(balance, transactions, utxos);
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

Avoid marking partial wallet data as a fresh cache entry.

Saving when only balance exists can cache missing or fallback transactions/utxos as fresh for 15 minutes, hiding a failed transaction/UTXO refresh behind the cache.

🐛 Proposed fix
-      if (balance) saveWalletToCache(balance, transactions, utxos);
+      if (balance && Array.isArray(transactions) && Array.isArray(utxos)) {
+        saveWalletToCache(balance, transactions, utxos);
+      }
-    if (balance) saveWalletToCache(balance, transactions, utxos);
+    if (balance && Array.isArray(transactions) && Array.isArray(utxos)) {
+      saveWalletToCache(balance, transactions, utxos);
+    }

Also applies to: 526-526

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

In `@src/components/wallet/Wallet.js` at line 364, The code currently calls
saveWalletToCache(balance, transactions, utxos) whenever balance exists, which
can mark incomplete data (missing or fallback transactions/utxos) as fresh
cache; change the guard so you only call saveWalletToCache when all required
pieces are present and valid (e.g., balance is defined AND transactions is an
array (or has expected length) AND utxos is defined/array), and propagate the
same stricter check to the other occurrence that uses saveWalletToCache (the
similar call at the second occurrence). Use the function/variable names balance,
transactions, utxos, and saveWalletToCache to locate and update both places.


refreshBtn.textContent = 'Refreshed!';
setTimeout(() => {
refreshBtn.textContent = originalText;
Expand Down Expand Up @@ -520,28 +511,21 @@ export async function WalletComponent(container) {
});
}

// Initialize data
updateBalance();
updateTransactions();
updateUtxos();

// ✅ SMART INITIALIZATION
if (shouldFetchFresh) {
console.log('🔄 Fetching fresh data...');
// Fetch fresh data
console.log('🔄 Syncing and fetching fresh data...');
try {
await window.api.taker.sync();
} catch (syncErr) {
console.warn('⚠️ Initial wallet sync failed, proceeding anyway:', syncErr.message);
}
Comment on lines +515 to +520
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 sync failure responses and avoid overlapping startup syncs.

window.api.taker.sync() can return { success: false, error } without throwing, so this path proceeds to fetch/cache data after a failed sync. It can also overlap with the new background offerbook sync because the main sync handler does not check global sync state.

🐛 Proposed fix
     console.log('🔄 Syncing and fetching fresh data...');
     try {
-      await window.api.taker.sync();
+      const syncState = await window.api.taker.getCurrentSyncState?.();
+      if (!syncState?.isRunning) {
+        await syncWalletState();
+      } else {
+        console.log('⏳ Sync already running, skipping initial wallet sync');
+      }
     } catch (syncErr) {
-      console.warn('⚠️ Initial wallet sync failed, proceeding anyway:', syncErr.message);
+      console.error('⚠️ Initial wallet sync failed, proceeding anyway:', syncErr);
     }

As per coding guidelines, errors should be logged with context using console.error('⚠️ Context:', error).

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

In `@src/components/wallet/Wallet.js` around lines 515 - 520, The startup sync
block should handle non-thrown failure responses and avoid overlapping with the
background offerbook sync: call window.api.taker.sync(), inspect its return
value and treat { success: false, error } as a failure (log via
console.error('⚠️ Initial wallet sync failed:', error) including the error
object), and do not proceed to fetch/cache data when sync failed; also guard
this startup sync with the same global sync flag used by the background
offerbook sync (check/set the shared sync state used by the main sync handler
and background offerbook sync before calling window.api.taker.sync() to prevent
concurrent syncs), and ensure the flag is cleared in all failure and success
paths so subsequent background syncs can run.

const [balance, transactions, utxos] = await Promise.all([
updateBalance(false),
updateTransactions(false),
updateUtxos(false),
]);

// Save to cache
if (balance && transactions && utxos) {
saveWalletToCache(balance, transactions, utxos);
}
if (balance) saveWalletToCache(balance, transactions, utxos);
} else {
console.log('⚡ Using cached data (still fresh)');
// Just use cache
await Promise.all([
updateBalance(true),
updateTransactions(true),
Expand Down
220 changes: 25 additions & 195 deletions src/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,8 @@ async function checkTakerInitialization(config) {

if (result.success) {
console.log('✅ Taker initialized');
await performLaunchSync(startMainApp);
startMainApp();
startBackgroundOfferbookSync();
Comment on lines +187 to +188
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

🧩 Analysis chain

🏁 Script executed:

# First, let's explore the repository structure
git ls-files | grep -E '\.(js|ts|tsx)$' | head -20

Repository: citadel-tech/taker-app

Length of output: 603


🏁 Script executed:

# Check the file size before reading
wc -l src/js/app.js

Repository: citadel-tech/taker-app

Length of output: 83


🏁 Script executed:

# Read the specific lines mentioned in the review (187-188 and 287-288) with context
sed -n '180,195p' src/js/app.js
echo "---"
sed -n '280,295p' src/js/app.js

Repository: citadel-tech/taker-app

Length of output: 1129


🏁 Script executed:

# Find the startMainApp function definition
rg -n "^(async\s+)?function\s+startMainApp|const\s+startMainApp\s*=" src/js/app.js

Repository: citadel-tech/taker-app

Length of output: 101


🏁 Script executed:

# Search for startMainApp calls to understand the context better
rg -n "startMainApp\(" src/js/app.js

Repository: citadel-tech/taker-app

Length of output: 207


🏁 Script executed:

# Check the startBackgroundOfferbookSync function
rg -n "startBackgroundOfferbookSync" src/js/app.js | head -10

Repository: citadel-tech/taker-app

Length of output: 207


🏁 Script executed:

# Read the startMainApp function definition and implementation
sed -n '353,380p' src/js/app.js

Repository: citadel-tech/taker-app

Length of output: 1205


🏁 Script executed:

# Read the startBackgroundOfferbookSync function
sed -n '322,350p' src/js/app.js

Repository: citadel-tech/taker-app

Length of output: 1083


🏁 Script executed:

# Check all context around all startMainApp calls to see if any are awaited
sed -n '150,160p' src/js/app.js
echo "---"
sed -n '204,214p' src/js/app.js

Repository: citadel-tech/taker-app

Length of output: 589


🏁 Script executed:

# Check if there's error handling for unhandled rejections
rg -n "unhandledrejection|catch|\.then" src/js/app.js | head -20

Repository: citadel-tech/taker-app

Length of output: 342


Await startMainApp() before starting background sync.

startMainApp() is async and will reject if SwapStateManager.getActiveSwap() fails or throws, but these calls don't await it, leaving unhandled rejections. Lines 187-188 and 287-288 are in async contexts where await startMainApp() can be used. Keep startBackgroundOfferbookSync() non-blocking by not awaiting it.

Proposed fix
-        startMainApp();
-        startBackgroundOfferbookSync();
+        await startMainApp();
+        startBackgroundOfferbookSync();
-          startMainApp();
-          startBackgroundOfferbookSync();
+          await startMainApp();
+          startBackgroundOfferbookSync();

Per the **/*.{js,ts,tsx} guideline, use async/await for asynchronous operations.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
startMainApp();
startBackgroundOfferbookSync();
await startMainApp();
startBackgroundOfferbookSync();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/js/app.js` around lines 187 - 188, The call to startMainApp() is
currently unawaited and can produce unhandled rejections from async failures
(e.g., SwapStateManager.getActiveSwap()); update the places where startMainApp()
is invoked (the blocks that also call startBackgroundOfferbookSync()) to await
startMainApp() (these call sites are already inside async contexts so use
await), while leaving startBackgroundOfferbookSync() invoked without awaiting so
the background sync remains non-blocking; ensure any errors from startMainApp()
propagate or are caught where appropriate.

} else {
console.error('❌ Taker initialization failed:', result.error);
alert('Failed to initialize: ' + result.error);
Expand Down Expand Up @@ -283,7 +284,8 @@ async function showPasswordPrompt(config) {
if (result.success) {
modal.remove();
resolve(true);
await performLaunchSync(startMainApp);
startMainApp();
startBackgroundOfferbookSync();
} else if (result.wrongPassword) {
errorDiv.classList.remove('hidden');
errorDiv.querySelector('p').textContent =
Expand Down Expand Up @@ -317,206 +319,34 @@ async function showPasswordPrompt(config) {
});
}

/**
* Show an offerbook sync overlay, wait for sync to complete, then call onComplete.
* Used on launch so the user sees makers as soon as the app opens.
*/
async function performLaunchSync(onComplete) {
const escapeHtml = (value) => {
const div = document.createElement('div');
div.textContent = value || '';
return div.innerHTML;
};

const parseLogLine = (line) => {
const match = line.match(
/^(\d{4}-\d{2}-\d{2}T[\d:.]+)[^\s]*\s+(INFO|WARN|ERROR|DEBUG|TRACE)\s+(.+)$/
);
if (!match) {
return {
timestamp: Date.now(),
type: 'info',
message: line,
};
}

return {
timestamp: new Date(match[1]).getTime(),
type: match[2].toLowerCase(),
message: match[3],
};
};

const getTypeColor = (type) => {
switch (type) {
case 'error':
return 'text-red-400';
case 'warn':
return 'text-yellow-400';
case 'debug':
return 'text-blue-400';
case 'trace':
return 'text-purple-400';
default:
return 'text-green-400';
}
};

const formatTime = (timestamp) =>
new Date(timestamp).toLocaleTimeString('en-US', { hour12: false });

const overlay = document.createElement('div');
overlay.id = 'launch-sync-overlay';
overlay.className = 'fixed inset-0 bg-[#0f1419] flex items-center justify-center z-50';
overlay.innerHTML = `
<div class="bg-[#1a2332] rounded-lg max-w-3xl w-full mx-4 p-8 border border-[#FF6B35]/10 shadow-2xl">
<div class="text-center mb-6">
<div class="w-16 h-16 bg-[#FF6B35]/20 rounded-full flex items-center justify-center mx-auto mb-4">
<span class="text-3xl animate-spin inline-block">⏳</span>
</div>
<h2 class="text-xl font-bold text-white mb-2">Syncing Market Data</h2>
<p class="text-gray-400 text-sm mb-4">Discovering available makers via Tor. This may take a minute...</p>
<p id="launch-sync-status" class="text-xs uppercase tracking-[0.2em] text-[#FF6B35]">Starting offerbook sync...</p>
</div>

<div class="bg-gray-700 rounded-full h-2 overflow-hidden">
<div id="launch-sync-progress" class="bg-[#FF6B35] h-2 rounded-full transition-all duration-500" style="width: 18%"></div>
</div>

<div class="mt-6 bg-[#0f1419] rounded-xl border border-white/5 overflow-hidden">
<div class="flex items-center justify-between px-4 py-3 border-b border-white/5">
<div>
<p class="text-sm font-semibold text-white">Live sync logs</p>
<p class="text-xs text-gray-500">Latest coinswap and backend activity during startup</p>
</div>
<span id="launch-sync-log-count" class="text-xs text-gray-500">0 lines</span>
</div>
<div id="launch-sync-log-output" class="h-64 overflow-y-auto px-4 py-3 font-mono text-xs text-left"></div>
</div>
</div>
`;
document.body.appendChild(overlay);

const logOutput = overlay.querySelector('#launch-sync-log-output');
const logCount = overlay.querySelector('#launch-sync-log-count');
const statusLabel = overlay.querySelector('#launch-sync-status');
const progressBar = overlay.querySelector('#launch-sync-progress');
const syncStartedAt = Date.now();
let logPoll = null;

function renderLogs(logs) {
if (!logOutput) return;

if (logCount) {
logCount.textContent = `${logs.length} ${logs.length === 1 ? 'line' : 'lines'}`;
}

if (logs.length === 0) {
logOutput.innerHTML =
'<div class="text-gray-500 text-center py-12">Waiting for startup logs...</div>';
return;
}

logOutput.innerHTML = logs
.map((log) => {
return `
<div class="mb-1 rounded px-2 py-1 hover:bg-white/5">
<span class="text-gray-500">[${formatTime(log.timestamp)}]</span>
<span class="${getTypeColor(log.type)}">[${log.type.toUpperCase()}]</span>
<span class="text-gray-300">${escapeHtml(log.message)}</span>
</div>
`;
})
.join('');
logOutput.scrollTop = logOutput.scrollHeight;
}

async function refreshLaunchLogs() {
try {
const data = await window.api.logs.get(120);
if (!data.success || !Array.isArray(data.logs)) return;

const recentLogs = data.logs
.map(parseLogLine)
.filter((log) => log.timestamp >= syncStartedAt - 5000)
.slice(-50);

renderLogs(recentLogs);
} catch (error) {
console.error('Failed to refresh launch sync logs:', error);
}
}

async function startBackgroundOfferbookSync() {
try {
await refreshLaunchLogs();
logPoll = setInterval(refreshLaunchLogs, 1500);

const syncResult = await window.api.taker.syncOfferbookAndWait();
if (syncResult.success) {
if (statusLabel) statusLabel.textContent = 'Offerbook sync in progress...';
if (progressBar) progressBar.style.width = '52%';

const syncId = syncResult.syncId;
await new Promise((resolve) => {
const poll = setInterval(async () => {
try {
const status = await window.api.taker.getSyncStatus(syncId);
const sync = status.sync || {};
const syncStatus = sync.status || 'syncing';
const syncMessage =
sync.message ||
(syncStatus === 'completed'
? 'Offerbook ready'
: syncStatus === 'failed'
? 'Sync failed'
: 'Syncing offerbook...');

if (statusLabel) statusLabel.textContent = syncMessage;
if (progressBar) {
const nextWidth =
typeof sync.progress === 'number'
? Math.max(18, Math.min(100, sync.progress))
: syncStatus === 'completed'
? 100
: syncStatus === 'failed'
? 100
: 76;
progressBar.style.width = `${nextWidth}%`;
}

const done =
!status.success ||
syncStatus === 'completed' ||
syncStatus === 'failed';
if (done) {
clearInterval(poll);
resolve();
}
} catch (err) {
console.error('Sync polling error:', err);
if (!syncResult.success) {
console.warn('⚠️ Background offerbook sync failed to start:', syncResult.error);
return;
}
const syncId = syncResult.syncId;
await new Promise((resolve) => {
const poll = setInterval(async () => {
try {
const status = await window.api.taker.getSyncStatus(syncId);
const syncStatus = (status.sync || {}).status || 'syncing';
if (!status.success || syncStatus === 'completed' || syncStatus === 'failed') {
clearInterval(poll);
resolve();
}
}, 1000);
});
} else {
if (statusLabel) {
statusLabel.textContent =
syncResult.error || 'Unable to start offerbook sync';
}
if (progressBar) progressBar.style.width = '100%';
}
} catch (err) {
console.warn('⚠️ Sync poll error:', err.message);
clearInterval(poll);
resolve();
}
}, 1000);
});
console.log('✅ Background offerbook sync complete');
Comment on lines +330 to +346
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 | 🟡 Minor

Do not log failed offerbook syncs as complete.

The polling loop resolves on status.success === false and on syncStatus === 'failed', but line 346 still logs Background offerbook sync complete. That masks failed or missing syncs as success.

🐛 Proposed fix
-    await new Promise((resolve) => {
+    const finalSyncStatus = await new Promise((resolve) => {
       const poll = setInterval(async () => {
         try {
           const status = await window.api.taker.getSyncStatus(syncId);
           const syncStatus = (status.sync || {}).status || 'syncing';
-          if (!status.success || syncStatus === 'completed' || syncStatus === 'failed') {
+          if (!status.success) {
             clearInterval(poll);
-            resolve();
+            resolve('missing');
+            return;
+          }
+          if (syncStatus === 'completed' || syncStatus === 'failed') {
+            clearInterval(poll);
+            resolve(syncStatus);
           }
         } catch (err) {
-          console.warn('⚠️ Sync poll error:', err.message);
+          console.error('⚠️ Background offerbook sync poll error:', err);
           clearInterval(poll);
-          resolve();
+          resolve('error');
         }
       }, 1000);
     });
+    if (finalSyncStatus !== 'completed') {
+      console.error('⚠️ Background offerbook sync did not complete:', finalSyncStatus);
+      return;
+    }
     console.log('✅ Background offerbook sync complete');

As per coding guidelines, errors should be logged with context using console.error('⚠️ Context:', error).

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

In `@src/js/app.js` around lines 330 - 346, The poll currently resolves for both
failure and success but always prints "Background offerbook sync complete";
update the logic around window.api.taker.getSyncStatus(syncId) and the Promise
resolution so you only call console.log('✅ Background offerbook sync complete')
when status.success === true and syncStatus === 'completed'; otherwise call
console.error with context (e.g., console.error('⚠️ Offerbook sync failed:',
status) or the caught error) and ensure the catch block uses console.error('⚠️
Sync poll error:', err) instead of console.warn; reference the async poll using
setInterval, the variables status/syncStatus, and the final console.log to make
the conditional logging change.

} catch (err) {
console.warn('⚠️ Launch offerbook sync error:', err.message);
if (statusLabel) statusLabel.textContent = 'Offerbook sync hit an issue';
if (progressBar) progressBar.style.width = '100%';
} finally {
if (logPoll) clearInterval(logPoll);
await refreshLaunchLogs();
console.warn('⚠️ Background offerbook sync error:', err.message);
}

overlay.remove();
onComplete();
}

// Start the main app after bitcoind connection is established
Expand Down
27 changes: 0 additions & 27 deletions src/styles/output.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
--spacing: 0.25rem;
--container-md: 28rem;
--container-2xl: 42rem;
--container-3xl: 48rem;
--container-5xl: 64rem;
--container-7xl: 80rem;
--text-xs: 0.75rem;
Expand Down Expand Up @@ -516,9 +515,6 @@
.max-w-2xl {
max-width: var(--container-2xl);
}
.max-w-3xl {
max-width: var(--container-3xl);
}
.max-w-5xl {
max-width: var(--container-5xl);
}
Expand Down Expand Up @@ -792,9 +788,6 @@
.border-\[\#FF6B35\] {
border-color: #FF6B35;
}
.border-\[\#FF6B35\]\/10 {
border-color: color-mix(in oklab, #FF6B35 10%, transparent);
}
.border-\[\#FF6B35\]\/20 {
border-color: color-mix(in oklab, #FF6B35 20%, transparent);
}
Expand Down Expand Up @@ -882,12 +875,6 @@
.border-transparent {
border-color: transparent;
}
.border-white\/5 {
border-color: color-mix(in srgb, #fff 5%, transparent);
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklab, var(--color-white) 5%, transparent);
}
}
.border-yellow-500\/30 {
border-color: color-mix(in srgb, oklch(79.5% 0.184 86.047) 30%, transparent);
@supports (color: color-mix(in lab, red, red)) {
Expand Down Expand Up @@ -1278,10 +1265,6 @@
--tw-font-weight: var(--font-weight-semibold);
font-weight: var(--font-weight-semibold);
}
.tracking-\[0\.2em\] {
--tw-tracking: 0.2em;
letter-spacing: 0.2em;
}
.tracking-wide {
--tw-tracking: var(--tracking-wide);
letter-spacing: var(--tracking-wide);
Expand Down Expand Up @@ -1628,16 +1611,6 @@
}
}
}
.hover\:bg-white\/5 {
&:hover {
@media (hover: hover) {
background-color: color-mix(in srgb, #fff 5%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-white) 5%, transparent);
}
}
}
}
.hover\:text-\[\#FF6B35\] {
&:hover {
@media (hover: hover) {
Expand Down