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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ node_modules/
package-lock.json
coinswap-ffi
dist/
squashfs-root/
squashfs-root/
.DS_Store
35 changes: 17 additions & 18 deletions src/components/market/Market.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,20 @@ export function Market(container) {
}
}

function formatTorEndpoint(address, start = 14, end = 16) {
function formatTorEndpoint(address, start = 6, end = 0) {
if (!address || typeof address !== 'string') return 'unknown';

const separatorIndex = address.lastIndexOf(':');
if (separatorIndex === -1) return address;

const host = address.slice(0, separatorIndex);
const host = address.slice(0, separatorIndex).replace(/\.onion$/i, '');
const port = address.slice(separatorIndex + 1);

if (host.length <= start + end + 3) {
return `${host}:${port}`;
}

return `${host.slice(0, start)}...${host.slice(-end)}:${port}`;
return end > 0 ? `${host.slice(0, start)}..${host.slice(-end)}:${port}` : `${host.slice(0, start)}..:${port}`;
}

// Check sync state every second
Expand Down Expand Up @@ -89,10 +89,16 @@ export function Market(container) {

function transformMaker(item, index) {
const offer = item.offer;
const addressObj = item.address || {};
const onionAddr = addressObj.onion_addr || '';
const port = addressObj.port || '6102';
const fullAddress = `${onionAddr}:${port}`;
const addr = item.address;
let fullAddress;
if (typeof addr === 'string') {
fullAddress = addr.includes(':') ? addr : `${addr}:6102`;
} else {
const addressObj = addr || {};
const onionAddr = addressObj.onion_addr || '';
const port = addressObj.port || '6102';
fullAddress = `${onionAddr}:${port}`;
}

// Handle null offers (unresponsive makers)
if (!offer) {
Expand Down Expand Up @@ -584,7 +590,7 @@ export function Market(container) {
<div class="bg-[#0f1419] p-4 rounded-lg">
<p class="text-sm text-gray-400 mb-1">Bond Txid</p>
<button
onclick="window.open('https://mempool.space/tx/${maker.bondTxid}', '_blank')"
onclick="window.open('http://170.75.166.88:8080/tx/${maker.bondTxid}', '_blank')"
class="text-cyan-400 hover:text-cyan-300 underline font-mono text-sm break-all text-left w-full"
>
${maker.bondTxid}
Expand Down Expand Up @@ -763,15 +769,9 @@ export function Market(container) {
tableBody.innerHTML = displayedMakers
.map(
(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 ${protocolBadge.classes} rounded text-xs font-semibold text-lg">
${protocolBadge.icon} ${protocolBadge.label}
</span>
</div>
<div class="grid grid-cols-7 gap-4 p-4 hover:bg-[#242d3d] transition-colors">

<div class="text-gray-300 font-mono text-sm truncate" title="${maker.address}">${formatTorEndpoint(maker.address)}</div>
<div class="text-green-400">${maker.baseFee}</div>
<div class="text-blue-400">${maker.volumeFee}%</div>
Expand Down Expand Up @@ -871,8 +871,7 @@ export function Market(container) {



<div class="grid grid-cols-8 gap-4 bg-[#FF6B35] p-4 text-xs">
<div class="font-semibold">Protocol</div>
<div class="grid grid-cols-7 gap-4 bg-[#FF6B35] p-4 text-xs">
<div class="font-semibold">Tor Address</div>
<div class="font-semibold">Base Fee</div>
<div class="font-semibold">% Fee Rate</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/send/Send.js
Original file line number Diff line number Diff line change
Expand Up @@ -781,7 +781,7 @@ export function SendComponent(container, preSelectedUtxos = null) {
<p class="text-xs text-gray-400 mb-2">Recipient: ${address}</p>
<div class="flex items-center gap-2">
<span class="text-xs text-gray-400">TXID:</span>
<a href="https://mempool.space/testnet/tx/${txid}" target="_blank"
<a href="http://170.75.166.88:8080/tx/${txid}" target="_blank"
class="text-sm font-mono text-blue-400 hover:text-blue-300 underline flex-1 truncate">
Comment on lines +784 to 785
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

Harden the external transaction link.

Add rel="noopener noreferrer" and encode the txid path segment before opening a new tab/window.

Proposed fix
-                <a href="http://170.75.166.88:8080/tx/${txid}" target="_blank"
+                <a href="http://170.75.166.88:8080/tx/${encodeURIComponent(String(txid))}" target="_blank" rel="noopener noreferrer"
                    class="text-sm font-mono text-blue-400 hover:text-blue-300 underline flex-1 truncate">
📝 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
<a href="http://170.75.166.88:8080/tx/${txid}" target="_blank"
class="text-sm font-mono text-blue-400 hover:text-blue-300 underline flex-1 truncate">
<a href="http://170.75.166.88:8080/tx/${encodeURIComponent(String(txid))}" target="_blank" rel="noopener noreferrer"
class="text-sm font-mono text-blue-400 hover:text-blue-300 underline flex-1 truncate">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/send/Send.js` around lines 784 - 785, The anchor in Send.js
that builds the external transaction URL should be hardened: add rel="noopener
noreferrer" to the <a> element and ensure the txid path segment is URL-encoded
(use encodeURIComponent(txid) or encodeURIComponent(txid.trim()) where txid is
referenced) when constructing the href (the template that currently uses
${txid}); update the href construction and add the rel attribute on the same <a>
element to prevent tab-napping and broken URLs.

${txid}
</a>
Expand Down
2 changes: 1 addition & 1 deletion src/components/swap/Swap.js
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ export async function SwapComponent(container) {
async function fetchNetworkFees() {
try {
const response = await fetch(
'https://mempool.space/api/v1/fees/recommended'
'http://170.75.166.88:8080/api/v1/fees/recommended'
);
const data = await response.json();
networkFeeRate = data.halfHourFee;
Comment on lines 520 to 524
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:

#!/bin/bash
curl -fsS --max-time 10 'http://170.75.166.88:8080/api/v1/fees/recommended' \
  | jq '{halfHourFee, fastestFee, economyFee, minimumFee}'

Repository: citadel-tech/taker-app

Length of output: 145


🏁 Script executed:

#!/bin/bash
# Verify the file exists and check the exact lines referenced
if [ -f "src/components/swap/Swap.js" ]; then
  echo "=== Lines 515-530 of src/components/swap/Swap.js ==="
  sed -n '515,530p' src/components/swap/Swap.js | cat -n
else
  echo "File not found at src/components/swap/Swap.js"
  # Try alternate patterns
  find . -name "Swap.js" -type f 2>/dev/null | head -5
fi

Repository: citadel-tech/taker-app

Length of output: 600


🏁 Script executed:

#!/bin/bash
# Get broader context around the fetch call to see error handling
if [ -f "src/components/swap/Swap.js" ]; then
  echo "=== Context: Lines 500-550 ==="
  sed -n '500,550p' src/components/swap/Swap.js | cat -n
fi

Repository: citadel-tech/taker-app

Length of output: 1817


Add validation before assigning the fee response.

The fetchNetworkFees() function (lines 519-527) lacks defensive checks. If the endpoint returns a non-2xx status, non-numeric halfHourFee, or missing field, networkFeeRate becomes undefined and causes NaN in calculations. Add an response.ok check and validate that halfHourFee is a finite positive number before assignment.

Proposed fix
async function fetchNetworkFees() {
  try {
    const response = await fetch(
      'http://170.75.166.88:8080/api/v1/fees/recommended'
    );
+   if (!response.ok) {
+     throw new Error(`Fee endpoint returned ${response.status}`);
+   }
    const data = await response.json();
-   networkFeeRate = data.halfHourFee;
+   const nextFeeRate = Number(data.halfHourFee);
+   if (!Number.isFinite(nextFeeRate) || nextFeeRate <= 0) {
+     throw new Error('Fee endpoint returned invalid halfHourFee');
+   }
+   networkFeeRate = nextFeeRate;
    updateSummary();
  } catch (error) {
    console.error('Failed to fetch network fees:', 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
const response = await fetch(
'https://mempool.space/api/v1/fees/recommended'
'http://170.75.166.88:8080/api/v1/fees/recommended'
);
const data = await response.json();
networkFeeRate = data.halfHourFee;
async function fetchNetworkFees() {
try {
const response = await fetch(
'http://170.75.166.88:8080/api/v1/fees/recommended'
);
if (!response.ok) {
throw new Error(`Fee endpoint returned ${response.status}`);
}
const data = await response.json();
const nextFeeRate = Number(data.halfHourFee);
if (!Number.isFinite(nextFeeRate) || nextFeeRate <= 0) {
throw new Error('Fee endpoint returned invalid halfHourFee');
}
networkFeeRate = nextFeeRate;
updateSummary();
} catch (error) {
console.error('⚠️ Failed to fetch network fees:', error);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/swap/Swap.js` around lines 520 - 524, The fetchNetworkFees
function should validate the HTTP response and the returned fee before assigning
networkFeeRate: check response.ok after fetch (and handle non-2xx by using a
fallback or throwing), parse response.json() safely, verify that
data.halfHourFee exists and is a finite positive number (e.g.,
Number.isFinite(+data.halfHourFee) && +data.halfHourFee > 0) before assigning to
networkFeeRate, and otherwise keep the current safe default or log/throw an
error; update references to fetchNetworkFees, networkFeeRate, and halfHourFee
when applying this validation.

Expand Down
2 changes: 1 addition & 1 deletion src/components/wallet/TransactionsList.js
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ export function TransactionsListComponent(container) {

// Global function for opening transactions on mempool.space
window.openTxOnMempool = (txid) => {
const url = `https://mutinynet.com/tx/${txid}`;
const url = `http://170.75.166.88:8080/tx/${txid}`;
if (typeof require !== 'undefined') {
try {
const { shell } = require('electron');
Expand Down
2 changes: 1 addition & 1 deletion src/components/wallet/UtxoList.js
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ export function UtxoListComponent(container) {

// Global function for opening transactions on mempool.space
window.openTxOnMempool = (txid) => {
const url = `https://mutinynet.com/tx/${txid}`;
const url = `http://170.75.166.88:8080/tx/${txid}`;
if (typeof require !== 'undefined') {
try {
const { shell } = require('electron');
Expand Down
136 changes: 65 additions & 71 deletions src/components/wallet/Wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,6 @@ function saveWalletToCache(balance, transactions, utxos) {
export async function WalletComponent(container) {
console.log('🔍 WalletComponent called at', new Date().toISOString());

// ✅ LOAD FROM CACHE FIRST
const cached = loadWalletFromCache();
let shouldFetchFresh = !cached || cached.isStale;

console.log(`🎯 Should fetch fresh: ${shouldFetchFresh}`);

async function fetchBalance() {
try {
const data = await window.api.taker.getBalance();
Expand All @@ -69,6 +63,7 @@ export async function WalletComponent(container) {
async function fetchTransactions() {
try {
const data = await window.api.taker.getTransactions(5, 0);
console.log('[REFRESH] raw getTransactions response:', JSON.stringify(data, null, 2));

if (data.success) {
return data.transactions || [];
Expand All @@ -84,6 +79,7 @@ export async function WalletComponent(container) {
async function fetchUtxos() {
try {
const data = await window.api.taker.getUtxos();
console.log('[REFRESH] raw getUtxos response:', JSON.stringify(data, null, 2));

if (data.success) {
return data.utxos || [];
Expand Down Expand Up @@ -174,24 +170,8 @@ export async function WalletComponent(container) {
}

// UI Update Functions
async function updateBalance(useCache = false) {
async function updateBalance() {
try {
// Use cached data if requested
if (useCache && cached && cached.balance) {
console.log('💳 Wallet balance from cache:', cached.balance);
content.querySelector('#regular-balance').textContent =
satsToBtc(cached.balance.regular) + ' BTC';
content.querySelector('#swap-balance').textContent =
satsToBtc(cached.balance.swap) + ' BTC';
content.querySelector('#contract-balance').textContent =
satsToBtc(cached.balance.contract) + ' BTC';
content.querySelector('#spendable-balance').textContent =
satsToBtc(cached.balance.spendable) + ' BTC';
console.log('✅ Balance loaded from cache');
return;
}

// Fetch fresh data
const balance = await fetchBalance();
console.log('💳 Wallet balance used by UI:', balance);

Expand All @@ -211,7 +191,7 @@ export async function WalletComponent(container) {
}
}

async function updateTransactions(useCache = false) {
async function updateTransactions() {
const transactionsContainer = content.querySelector(
'#transactions-container'
);
Expand Down Expand Up @@ -265,16 +245,6 @@ export async function WalletComponent(container) {
};

try {
// Use cached data if requested
if (useCache && cached && cached.transactions) {
transactionsContainer.innerHTML = renderTransactions(
cached.transactions
);
console.log('✅ Transactions loaded from cache');
return;
}

// Fetch fresh data
const transactions = await fetchTransactions();
transactionsContainer.innerHTML = renderTransactions(transactions);
console.log('✅ Transactions updated from API:', transactions.length);
Expand All @@ -286,9 +256,25 @@ export async function WalletComponent(container) {
}
}

async function updateUtxos(useCache = false) {
async function updateUtxos() {
const utxoTableBody = content.querySelector('#utxo-table-body');

// Build a txid→confirmations map from Bitcoin Core's listtransactions
// (getUtxos uses in-memory wallet state which stores stale confirmation counts)
const txConfMap = new Map();
try {
const txData = await window.api.taker.getTransactions(200, 0);
if (txData.success) {
for (const tx of txData.transactions || []) {
const txid = typeof tx.info.txid === 'object' ? tx.info.txid.value : tx.info.txid;
txConfMap.set(txid, tx.info.confirmations);
}
}
console.log('[UTXO] built tx confirmation map:', Object.fromEntries(txConfMap));
} catch (e) {
console.warn('[UTXO] could not build tx confirmation map, using raw values:', e.message);
}

// Helper to render UTXOs
const renderUtxos = (utxos) => {
if (utxos.length === 0) {
Expand Down Expand Up @@ -320,17 +306,25 @@ export async function WalletComponent(container) {
};

try {
// Use cached data if requested
if (useCache && cached && cached.utxos) {
utxoTableBody.innerHTML = renderUtxos(cached.utxos);
console.log('✅ UTXOs loaded from cache');
return;
}
const rawUtxos = await fetchUtxos();

// Enrich confirmation counts using the live tx data
const utxos = rawUtxos.map(u => {
const txid = typeof u.utxo.txid === 'object' ? u.utxo.txid.value : u.utxo.txid;
const liveConfs = txConfMap.get(txid);
if (liveConfs !== undefined && liveConfs !== u.utxo.confirmations) {
console.log(`[UTXO] enriching ${txid.slice(0, 8)}: ${u.utxo.confirmations} → ${liveConfs}`);
return { ...u, utxo: { ...u.utxo, confirmations: liveConfs } };
}
return u;
});

console.log('[REFRESH] UTXOs after enrichment:', utxos.map(u => ({
txid: typeof u.utxo.txid === 'object' ? u.utxo.txid.value : u.utxo.txid,
confirmations: u.utxo.confirmations,
})));

// Fetch fresh data
const utxos = await fetchUtxos();
utxoTableBody.innerHTML = renderUtxos(utxos);
console.log('✅ UTXOs updated from API:', utxos.length);
return utxos;
} catch (error) {
console.error('❌ UTXO update failed:', error);
Expand All @@ -353,23 +347,32 @@ export async function WalletComponent(container) {
refreshBtn.disabled = true;

try {
await syncWalletState();
localStorage.removeItem(WALLET_CACHE_KEY);

try {
await syncWalletState();
} catch (syncErr) {
console.warn('⚠️ Wallet sync failed, refreshing data anyway:', syncErr.message);
}

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

if (balance) saveWalletToCache(balance, transactions, utxos);

console.log('[REFRESH] Complete summary:');
console.log(' balance:', balance);
console.log(' transactions confirmations:', transactions?.map(t => ({ txid: typeof t.info.txid === 'object' ? t.info.txid.value : t.info.txid, confirmations: t.info.confirmations })));
console.log(' utxo confirmations:', utxos?.map(u => ({ txid: typeof u.utxo.txid === 'object' ? u.utxo.txid.value : u.utxo.txid, confirmations: u.utxo.confirmations })));

refreshBtn.textContent = 'Refreshed!';
setTimeout(() => {
refreshBtn.textContent = originalText;
refreshBtn.disabled = false;
}, 2000);

console.log('✅ All data refreshed');
} catch (error) {
refreshBtn.textContent = 'Refresh Failed';
setTimeout(() => {
Expand Down Expand Up @@ -471,7 +474,7 @@ export async function WalletComponent(container) {

// Global function for opening transactions on mempool.space
window.openTxOnMempool = (txid) => {
const url = `https://mutinynet.com/tx/${txid}`;
const url = `http://170.75.166.88:8080/tx/${txid}`;
if (typeof require !== 'undefined') {
try {
const { shell } = require('electron');
Expand Down Expand Up @@ -511,25 +514,16 @@ export async function WalletComponent(container) {
});
}

if (shouldFetchFresh) {
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);
}
const [balance, transactions, utxos] = await Promise.all([
updateBalance(false),
updateTransactions(false),
updateUtxos(false),
]);
if (balance) saveWalletToCache(balance, transactions, utxos);
} else {
console.log('⚡ Using cached data (still fresh)');
await Promise.all([
updateBalance(true),
updateTransactions(true),
updateUtxos(true),
]);
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 +517 to 522
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

Check the initial sync result before caching refreshed data.

window.api.taker.sync() can return { success: false } without throwing; this path currently proceeds silently and can cache stale data.

Proposed fix
   console.log('🔄 Syncing and fetching fresh data...');
   try {
-    await window.api.taker.sync();
+    await syncWalletState();
   } catch (syncErr) {
     console.warn('⚠️ Initial wallet sync failed, proceeding anyway:', syncErr.message);
   }
🤖 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 517 - 522, The current
try/catch around window.api.taker.sync() ignores non-throwing failures; change
the call to capture its return (e.g., const syncResult = await
window.api.taker.sync()), check syncResult.success (or equivalent) before
proceeding to cache refreshed data, and if success is false log a warning and
skip the caching/update path; keep the existing catch to handle thrown errors
but ensure both failure cases (success:false and exceptions) do not allow stale
data to be cached.

const [balance, transactions, utxos] = await Promise.all([
updateBalance(),
updateTransactions(),
updateUtxos(),
]);
if (balance) saveWalletToCache(balance, transactions, utxos);
}
3 changes: 3 additions & 0 deletions src/styles/output.css
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,9 @@
.grid-cols-5 {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.grid-cols-7 {
grid-template-columns: repeat(7, minmax(0, 1fr));
}
.grid-cols-8 {
grid-template-columns: repeat(8, minmax(0, 1fr));
}
Expand Down