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
18 changes: 18 additions & 0 deletions api1.js
Original file line number Diff line number Diff line change
Expand Up @@ -1921,6 +1921,23 @@ function registerTorHandlers() {
// MAIN REGISTRATION FUNCTION
// ============================================================================

function registerAppHandlers() {
ipcMain.handle('app:getVersionInfo', async () => {
try {
const appVersion = app.getVersion();
let binaryVersion = 'unknown';
try {
const napiPkgPath = require.resolve('coinswap-napi/package.json');
const napiPkg = JSON.parse(fs.readFileSync(napiPkgPath, 'utf8'));
binaryVersion = napiPkg.version || 'unknown';
} catch (_) {}
return { success: true, appVersion, binaryVersion };
} catch (error) {
return { success: false, error: error.message };
}
});
}

function registerAPI1() {
console.log('📦 Registering API v1 handlers...');

Expand All @@ -1932,6 +1949,7 @@ function registerAPI1() {
registerDialogHandlers();
registerShellHandlers();
registerTorHandlers();
registerAppHandlers();

console.log('✅ API v1 handlers registered');
}
Expand Down
4 changes: 4 additions & 0 deletions preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ contextBridge.exposeInMainWorld('api', {
set: (key, value) => ipcRenderer.invoke('preferences:set', key, value),
},

app: {
getVersionInfo: () => ipcRenderer.invoke('app:getVersionInfo'),
},

// File dialogs - TOP LEVEL, NOT INSIDE TAKER!
openFile: (options) => ipcRenderer.invoke('dialog:openFile', options),
saveFile: (options) => ipcRenderer.invoke('dialog:saveFile', options),
Expand Down
7 changes: 7 additions & 0 deletions src/components/Nav.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ export async function NavComponent(container) {
</svg>
<span class="text-sm font-semibold text-lg">Settings</span>
</a>

<a href="#about" data-nav="about" class="nav-item flex flex-col items-center justify-center p-4 rounded-lg bg-[#242d3d] text-gray-400 hover:bg-[#2d3748] hover:text-white transition-colors">
<svg class="w-6 h-6 mb-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span class="text-sm font-semibold text-lg">About</span>
</a>
</nav>
`;

Expand Down
50 changes: 50 additions & 0 deletions src/components/about/About.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
export async function AboutComponent(container) {
container.innerHTML = `
<div class="flex-1 p-8 overflow-auto">

<!-- Header -->
<div class="flex items-start justify-between mb-8">
<div>
<h2 class="text-3xl font-bold text-[#FF6B35]">Coinswap</h2>
<p class="text-gray-400 text-sm mt-1">Taker Wallet — Bitcoin Privacy Tool</p>
</div>
<div class="flex items-center gap-3 mt-1">
<span class="text-gray-500 text-sm">Version</span>
<span id="app-version" class="text-white font-mono text-sm bg-[#1a2332] px-3 py-1 rounded">Loading...</span>
</div>
</div>

<!-- What is Coinswap -->
<div class="bg-[#1a2332] rounded-lg p-8 max-w-3xl space-y-4">
<h3 class="text-xl font-semibold text-white">About Coinswap</h3>
<p class="text-gray-300 text-sm leading-relaxed">
Coinswap is a trustless, self-custodial atomic swap protocol built on Bitcoin. Unlike existing solutions
that rely on centralized servers, Coinswap's marketplace is seeded in the Bitcoin blockchain itself —
no central host required. Sybil resistance is achieved through
<span class="text-white font-medium">Fidelity Bonds</span>: time-locked UTXOs that make Sybil attacks
economically costly while bootstrapping the marketplace on-chain.
</p>
<p class="text-gray-300 text-sm leading-relaxed">
There are two roles. <span class="text-white font-medium">Makers</span> are swap service providers who
earn fees for supplying liquidity and run in an install-fund-forget mode — no active management needed.
<span class="text-white font-medium">Takers</span> (this app) initiate swaps, pay all fees, and select
makers based on bond validity, available liquidity, and fee rates.
</p>
<p class="text-gray-300 text-sm leading-relaxed">
Swaps are routed through multiple makers — no single maker sees the full route. The taker relays all
messages between makers over Tor, keeping each maker's view partial. This app is a production-grade
implementation of Chris Belcher's teleport-transactions proof-of-concept.
</p>
</div>

</div>
`;

try {
const result = await window.api.app.getVersionInfo();
const version = result.success ? `v${result.appVersion}` : 'unavailable';
container.querySelector('#app-version').textContent = version;
} catch (_) {
container.querySelector('#app-version').textContent = 'unavailable';
}
Comment on lines +43 to +49
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider displaying the binary version as well.

The app:getVersionInfo handler returns both appVersion and binaryVersion, but only appVersion is displayed. For debugging and support purposes, showing the coinswap-napi binary version could be valuable.

💡 Optional enhancement
   try {
     const result = await window.api.app.getVersionInfo();
-    const version = result.success ? `v${result.appVersion}` : 'unavailable';
+    const version = result.success 
+      ? `v${result.appVersion} (napi: ${result.binaryVersion})`
+      : 'unavailable';
     container.querySelector('#app-version').textContent = version;
   } catch (_) {
     container.querySelector('#app-version').textContent = 'unavailable';
   }
📝 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
try {
const result = await window.api.app.getVersionInfo();
const version = result.success ? `v${result.appVersion}` : 'unavailable';
container.querySelector('#app-version').textContent = version;
} catch (_) {
container.querySelector('#app-version').textContent = 'unavailable';
}
try {
const result = await window.api.app.getVersionInfo();
const version = result.success
? `v${result.appVersion} (napi: ${result.binaryVersion})`
: 'unavailable';
container.querySelector('#app-version').textContent = version;
} catch (_) {
container.querySelector('#app-version').textContent = 'unavailable';
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/about/About.js` around lines 43 - 49, The current code calling
window.api.app.getVersionInfo only sets
container.querySelector('#app-version').textContent using result.appVersion;
update the handler usage to also read result.binaryVersion and set a DOM element
(e.g., container.querySelector('#binary-version').textContent) so the
coinswap-napi binary version is displayed alongside the app version; keep the
existing fallback for errors/unavailable and ensure you only update the binary
element when result.success is true (or set it to 'unavailable' on error) so
both '#app-version' and '#binary-version' reflect the version info.

}
11 changes: 10 additions & 1 deletion src/components/connection/ConnectionStatus.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,16 @@ export function ConnectionStatusComponent(container, onConnected) {

try {
const result = await bitcoindConnection.connect();
if (!result.success) {
if (result.alreadyConnected) {
// Connection existed before our patch was in place — verify and proceed
const check = await bitcoindConnection.testConnection();
if (check.success) {
showSuccess(check.info);
} else {
bitcoindConnection.disconnect();
startConnection(); // retry from scratch
}
} else if (!result.success) {
showError(result.finalError || result.error);
}
Comment on lines +231 to 242
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

Potential infinite recursion when existing connection verification fails.

If testConnection() repeatedly fails after alreadyConnected, the code calls startConnection() recursively without any retry limit. This could cause a stack overflow or infinite loop if the connection is persistently unhealthy.

Consider adding a retry counter or reusing the existing retry mechanism instead of direct recursion:

🛡️ Proposed fix
-            if (result.alreadyConnected) {
-                // Connection existed before our patch was in place — verify and proceed
-                const check = await bitcoindConnection.testConnection();
-                if (check.success) {
-                    showSuccess(check.info);
-                } else {
-                    bitcoindConnection.disconnect();
-                    startConnection(); // retry from scratch
-                }
+            if (result.alreadyConnected) {
+                // Connection existed before our patch was in place — verify and proceed
+                const check = await bitcoindConnection.testConnection();
+                if (check.success) {
+                    showSuccess(check.info);
+                } else {
+                    // Reset connection state and let the patched _performConnection handle retries
+                    bitcoindConnection.disconnect();
+                    bitcoindConnection.isConnected = false;
+                    const retryResult = await bitcoindConnection.connect();
+                    if (!retryResult.success) {
+                        showError(retryResult.finalError || retryResult.error);
+                    }
+                }
📝 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
if (result.alreadyConnected) {
// Connection existed before our patch was in place — verify and proceed
const check = await bitcoindConnection.testConnection();
if (check.success) {
showSuccess(check.info);
} else {
bitcoindConnection.disconnect();
startConnection(); // retry from scratch
}
} else if (!result.success) {
showError(result.finalError || result.error);
}
if (result.alreadyConnected) {
// Connection existed before our patch was in place — verify and proceed
const check = await bitcoindConnection.testConnection();
if (check.success) {
showSuccess(check.info);
} else {
// Reset connection state and let the patched _performConnection handle retries
bitcoindConnection.disconnect();
bitcoindConnection.isConnected = false;
const retryResult = await bitcoindConnection.connect();
if (!retryResult.success) {
showError(retryResult.finalError || retryResult.error);
}
}
} else if (!result.success) {
showError(result.finalError || result.error);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/connection/ConnectionStatus.js` around lines 231 - 242, The
existing branch handling result.alreadyConnected can recurse indefinitely
because it calls startConnection() directly when
bitcoindConnection.testConnection() fails; modify this to use a bounded retry
mechanism instead: add or pass a retry counter (e.g., maxRetries and
currentAttempt) into startConnection and decrement on each retry, or invoke the
component's existing retry handler rather than calling startConnection()
directly; update the code paths around result.alreadyConnected,
bitcoindConnection.testConnection(), bitcoindConnection.disconnect(), and
startConnection() so failures trigger a controlled retry (or a single scheduled
retry) and eventually showError when retries are exhausted.

} catch (error) {
Expand Down
31 changes: 0 additions & 31 deletions src/components/taker/TakerInitialization.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,6 @@ export function TakerInitializationComponent(container, config, onInitialized) {
</div>
<span id="step-taker-text" class="text-gray-400 text-sm">Initializing taker (creates wallet)</span>
</div>
<div id="step-offerbook" class="flex items-center space-x-3">
<div id="step-offerbook-icon" class="w-6 h-6 rounded-full bg-gray-600 flex items-center justify-center">
<span class="text-xs text-white">3</span>
</div>
<span id="step-offerbook-text" class="text-gray-400 text-sm">Syncing offerbook</span>
</div>
</div>
</div>

Expand Down Expand Up @@ -269,31 +263,6 @@ export function TakerInitializationComponent(container, config, onInitialized) {
}

updateStep('step-taker', 'complete', 'Taker ready (wallet loaded)');
updateProgress(66, 'Syncing offerbook...');
updateStep('step-offerbook', 'active', 'Syncing offerbook...');

const syncResult = await window.api.taker.syncOfferbookAndWait();
if (!syncResult.success) {
console.warn('⚠️ Offerbook sync failed on launch:', syncResult.error);
updateStep('step-offerbook', 'error', 'Sync failed (will retry later)');
} else {
const syncId = syncResult.syncId;
await new Promise((resolve) => {
const poll = setInterval(async () => {
try {
const status = await window.api.taker.getSyncStatus(syncId);
const done = !status.success || status.sync.status === 'completed' || status.sync.status === 'failed';
if (done) {
clearInterval(poll);
const ok = status.success && status.sync.status === 'completed';
updateStep('step-offerbook', ok ? 'complete' : 'error', ok ? 'Offerbook ready' : 'Sync failed (will retry later)');
resolve();
}
} catch { clearInterval(poll); updateStep('step-offerbook', 'error', 'Sync failed (will retry later)'); resolve(); }
}, 1000);
});
}

updateProgress(100, 'Initialization complete');
showSuccess();
} catch (error) {
Expand Down
58 changes: 4 additions & 54 deletions src/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { SwapComponent } from '../components/swap/Swap.js';
import { RecoveryComponent } from '../components/recovery/Recovery.js';
import { LogComponent } from '../components/log/Log.js';
import { SettingsComponent } from '../components/settings/Settings.js';
import { AboutComponent } from '../components/about/About.js';
import { FirstTimeSetupModal } from '../components/settings/FirstTimeSetup.js';
import { SwapStateManager } from '../components/swap/SwapStateManager.js';
import { ConnectionStatusComponent } from '../components/connection/ConnectionStatus.js';
Expand All @@ -23,6 +24,7 @@ const components = {
recovery: RecoveryComponent,
log: LogComponent,
settings: SettingsComponent,
about: AboutComponent,
};

// Background swap manager - runs independently of UI components
Expand Down Expand Up @@ -133,70 +135,17 @@ function setupNavigation() {
async function checkBitcoindConnection(config) {
console.log('🔌 Checking Bitcoin Core connection...');

// Update the connection manager with config from setup
if (config) {
bitcoindConnection.updateConfig(config);
}

// Show connection status component
const appContainer = document.querySelector('body');
ConnectionStatusComponent(appContainer, (connectionInfo) => {
console.log('✅ Bitcoin Core connected, starting app...', connectionInfo);
checkTakerInitialization(config);
startTakerInitWithConfig(config);
});
}

async function checkTakerInitialization(config) {
console.log('🔄 Checking Taker initialization...');

if (!config || !config.rpc) {
console.log('⚠️ RPC configuration missing, skipping taker initialization');
startMainApp();
return;
}

try {
// Extract wallet name from config
const walletName =
config.wallet?.name || config.wallet?.fileName || 'taker-wallet';

console.log('🔍 Checking wallet:', walletName);

// Store it in config so showPasswordPrompt can use it
if (!config.wallet.name && !config.wallet.fileName) {
config.wallet.name = walletName;
}

// Check if wallet file is encrypted
const isEncrypted = await window.api.taker.isWalletEncrypted(
null,
walletName
);
console.log('🔐 Wallet file encrypted:', isEncrypted);

if (isEncrypted) {
console.log('🔓 Wallet is encrypted, showing password prompt...');
await showPasswordPrompt(config); // ✅ Config has correct wallet name
} else {
console.log('🔓 Wallet is not encrypted');

const result = await window.api.taker.initialize(config);

if (result.success) {
console.log('✅ Taker initialized');
startMainApp();
startBackgroundOfferbookSync();
} else {
console.error('❌ Taker initialization failed:', result.error);
alert('Failed to initialize: ' + result.error);
}
}
} catch (error) {
console.error('❌ Initialization check failed:', error);
alert('Initialization failed: ' + error.message);
}
}

function startTakerInitWithConfig(config) {
const appContainer = document.querySelector('body');
TakerInitializationComponent(appContainer, config, (result) => {
Expand Down Expand Up @@ -349,6 +298,7 @@ async function startBackgroundOfferbookSync() {
}
}


// Start the main app after bitcoind connection is established
async function startMainApp() {
const activeSwap = await SwapStateManager.getActiveSwap();
Expand Down
12 changes: 9 additions & 3 deletions src/styles/output.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
--spacing: 0.25rem;
--container-md: 28rem;
--container-2xl: 42rem;
--container-3xl: 48rem;
--container-5xl: 64rem;
--container-7xl: 80rem;
--text-xs: 0.75rem;
Expand All @@ -66,6 +67,7 @@
--font-weight-semibold: 600;
--font-weight-bold: 700;
--tracking-wide: 0.025em;
--leading-relaxed: 1.625;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--ease-out: cubic-bezier(0, 0, 0.2, 1);
Expand Down Expand Up @@ -515,6 +517,9 @@
.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 @@ -588,9 +593,6 @@
.grid-cols-7 {
grid-template-columns: repeat(7, minmax(0, 1fr));
}
.grid-cols-8 {
grid-template-columns: repeat(8, minmax(0, 1fr));
}
.flex-col {
flex-direction: column;
}
Expand Down Expand Up @@ -1252,6 +1254,10 @@
--tw-leading: 1;
line-height: 1;
}
.leading-relaxed {
--tw-leading: var(--leading-relaxed);
line-height: var(--leading-relaxed);
}
.font-bold {
--tw-font-weight: var(--font-weight-bold);
font-weight: var(--font-weight-bold);
Expand Down