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
177 changes: 170 additions & 7 deletions api1.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,169 @@ function saveSwapReport(swapId, swapData) {
}
}

function toNumber(value, fallback = 0) {
const normalized = Number(value);
return Number.isFinite(normalized) ? normalized : fallback;
}

function getAllSwapReportPaths() {
const reportsRoot = path.join(api1State.DATA_DIR, 'swap_reports');
const reportPaths = [];

function walk(dirPath) {
if (!fs.existsSync(dirPath)) return;

for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
const fullPath = path.join(dirPath, entry.name);
if (entry.isDirectory()) {
walk(fullPath);
} else if (entry.isFile() && entry.name.endsWith('.json')) {
reportPaths.push(fullPath);
}
}
}

walk(reportsRoot);
return reportPaths;
}
Comment on lines +117 to +136
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

Don't rebuild the historical swap index inside the balance hot path.

getHistoricalSwapOutputMap() synchronously walks and parses every swap_reports/**/*.json file each time balance is requested. Because taker:getBalance runs on Electron's main process and is hit on initial load, wallets with many reports will block the UI here. Cache this per wallet and invalidate it when saveSwapReport() writes a new report, or at minimum scope the scan to the active wallet directory.

Also applies to: 138-166

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

In `@api1.js` around lines 117 - 136,
getAllSwapReportPaths/getHistoricalSwapOutputMap currently synchronously scans
and parses all swap_reports/**/*.json on every balance request, blocking the
Electron main thread; change this to cache the parsed historical swap index per
wallet (keyed by wallet ID or active wallet path) and only rebuild the cache
when saveSwapReport writes a new report (or when wallet switches). Concretely:
scope the scan in getAllSwapReportPaths to the active wallet directory instead
of the global swap_reports root, add an in-memory cache map (e.g.,
historicalSwapCache[walletId]) and a last-modified marker, update
getHistoricalSwapOutputMap to return cached data when valid, and have
saveSwapReport invalidate or update that wallet's cache after writing a report
so UI balance requests no longer trigger synchronous full-disk rescans.


function getHistoricalSwapOutputMap() {
const swapOutputs = new Map();

for (const reportPath of getAllSwapReportPaths()) {
try {
const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
const outputSwapUtxos =
report.output_swap_utxos ||
report.outputSwapUtxos ||
report.report?.output_swap_utxos ||
report.report?.outputSwapUtxos ||
[];

outputSwapUtxos.forEach((entry) => {
if (!Array.isArray(entry) || entry.length < 2) return;
const [amount, address] = entry;
if (!address) return;
swapOutputs.set(address, toNumber(amount, 0));
});
Comment on lines +141 to +156
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

Match historical swap outputs on more than the address.

The parser records amount, but the matcher only checks historicalSwapOutputs.has(utxoEntry?.address). Any later non-swap UTXO paid to the same address will be reclassified as swap, inflating normalized.swap and shrinking regular. Match on (address, amount) at minimum, or (txid, vout) if the report format supports it.

🛠️ Suggested fix
 function getHistoricalSwapOutputMap() {
   const swapOutputs = new Map();
@@
       outputSwapUtxos.forEach((entry) => {
         if (!Array.isArray(entry) || entry.length < 2) return;
         const [amount, address] = entry;
         if (!address) return;
-        swapOutputs.set(address, toNumber(amount, 0));
+        const normalizedAmount = toNumber(amount, 0);
+        const amounts = swapOutputs.get(address) || new Set();
+        amounts.add(normalizedAmount);
+        swapOutputs.set(address, amounts);
       });
@@
   const matchedHistoricalSwapUtxos = rawUtxos.filter(([utxoEntry]) =>
-    historicalSwapOutputs.has(utxoEntry?.address)
+    historicalSwapOutputs
+      .get(utxoEntry?.address)
+      ?.has(toNumber(utxoEntry?.amount?.sats, 0))
   );

Also applies to: 213-218

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

In `@api1.js` around lines 141 - 156, The current parser in the
getAllSwapReportPaths loop stores swap outputs in swapOutputs keyed only by
address, causing later address-only matches to misclassify non-swap UTXOs;
change the key to include amount (and prefer txid+vout when present) when
populating outputSwapUtxos in the loop that iterates outputSwapUtxos (use the
same composite key logic used later when checking historicalSwapOutputs.has),
e.g., build a composite key from address and toNumber(amount) or from
report-provided txid and vout if available; update any lookup sites that call
historicalSwapOutputs.has(…) to compute the same composite key so matching is
performed on (address,amount) or (txid,vout) instead of address-only.

} catch (error) {
console.warn('⚠️ Failed to parse swap report for balance derivation:', {
reportPath,
error: error.message,
});
}
}

return swapOutputs;
}

function deriveBalancesFromUtxos(rawUtxos = []) {
const derived = {
spendable: 0,
regular: 0,
swap: 0,
contract: 0,
fidelity: 0,
};

rawUtxos.forEach(([utxoEntry, spendInfo]) => {
const amount = toNumber(utxoEntry?.amount?.sats, 0);
const spendType = String(spendInfo?.spendType || '').toLowerCase();
const isSpendable = Boolean(utxoEntry?.spendable);

if (isSpendable) {
derived.spendable += amount;
}

if (spendType.includes('swap')) {
derived.swap += amount;
return;
}

if (spendType.includes('contract')) {
derived.contract += amount;
return;
}

if (spendType.includes('fidelity')) {
derived.fidelity += amount;
return;
}

if (spendType.includes('seed') || spendType.includes('regular')) {
derived.regular += amount;
}
});

return derived;
}

function normalizeBalancePayload(rawBalance = {}, rawUtxos = []) {
const derivedFromUtxos = deriveBalancesFromUtxos(rawUtxos);
const historicalSwapOutputs = getHistoricalSwapOutputMap();

const matchedHistoricalSwapUtxos = rawUtxos.filter(([utxoEntry]) =>
historicalSwapOutputs.has(utxoEntry?.address)
);
const historicalSwapBalance = matchedHistoricalSwapUtxos.reduce(
(sum, [utxoEntry]) => sum + toNumber(utxoEntry?.amount?.sats, 0),
0
);

const normalized = {
spendable: toNumber(
rawBalance.spendable ?? rawBalance.spendable_balance,
derivedFromUtxos.spendable
),
regular: toNumber(
rawBalance.regular ?? rawBalance.regular_balance,
derivedFromUtxos.regular
),
swap: toNumber(
rawBalance.swap ??
rawBalance.swap_balance ??
rawBalance.swapCoin ??
rawBalance.swapcoin,
derivedFromUtxos.swap
),
contract: toNumber(
rawBalance.contract ?? rawBalance.contract_balance,
derivedFromUtxos.contract
),
fidelity: toNumber(
rawBalance.fidelity ?? rawBalance.fidelity_balance,
derivedFromUtxos.fidelity
),
};

// Completed swap outputs can show up as SeedCoin/regular in the current UTXO API.
// Recover that provenance by matching active UTXOs against saved swap reports.
if (historicalSwapBalance > normalized.swap) {
normalized.swap = historicalSwapBalance;
normalized.regular = Math.max(
0,
normalized.spendable -
normalized.swap -
normalized.contract -
normalized.fidelity
);
}
Comment thread
keraliss marked this conversation as resolved.

return {
normalized,
debug: {
rawBalance,
derivedFromUtxos,
historicalSwapBalance,
historicalSwapMatches: matchedHistoricalSwapUtxos.map(([utxoEntry, spendInfo]) => ({
address: utxoEntry?.address,
amount: toNumber(utxoEntry?.amount?.sats, 0),
spendType: spendInfo?.spendType,
})),
},
};
}

async function initNAPI() {
try {
api1State.coinswapNapi = require('coinswap-napi');
Expand Down Expand Up @@ -469,16 +632,16 @@ function registerTakerHandlers() {
// Sync removed to prevent UI blocking on page load - relies on background sync
// api1State.takerInstance.syncAndSave();
const balance = api1State.takerInstance.getBalances();
const rawUtxos = api1State.takerInstance.listAllUtxoSpendInfo();
const { normalized, debug } = normalizeBalancePayload(balance, rawUtxos);

console.log('💰 Raw taker balance payload:', balance);
console.log('💰 Normalized taker balance payload:', normalized);
console.log('💰 Balance derivation details:', debug);
Comment on lines +638 to +640
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

Redact wallet metadata from these logs.

debug.historicalSwapMatches contains wallet addresses, amounts, and spend types, and these logs are emitted on every balance fetch. That exposes wallet history in main-process logs and any collected diagnostics. Keep only aggregate counts here or gate detailed output behind a dedicated diagnostic switch.

🛡️ Suggested fix
-      console.log('💰 Raw taker balance payload:', balance);
-      console.log('💰 Normalized taker balance payload:', normalized);
-      console.log('💰 Balance derivation details:', debug);
+      console.log('💰 Normalized taker balance payload:', normalized);
+      console.log('💰 Balance derivation summary:', {
+        historicalSwapBalance: debug.historicalSwapBalance,
+        historicalSwapMatchCount: debug.historicalSwapMatches.length,
+      });
As per coding guidelines "Never log sensitive data (passwords, private keys)".
📝 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
console.log('💰 Raw taker balance payload:', balance);
console.log('💰 Normalized taker balance payload:', normalized);
console.log('💰 Balance derivation details:', debug);
console.log('💰 Normalized taker balance payload:', normalized);
console.log('💰 Balance derivation summary:', {
historicalSwapBalance: debug.historicalSwapBalance,
historicalSwapMatchCount: debug.historicalSwapMatches.length,
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api1.js` around lines 638 - 640, The three console.log calls printing
balance, normalized, and debug currently expose sensitive wallet metadata
(notably debug.historicalSwapMatches); replace these with logs that redact or
omit per-wallet details—e.g., log only aggregate counts/totals or a sanitized
summary—and if you need full details keep them behind a dedicated diagnostic
flag (e.g., only emit debug when DIAGNOSTICS_ENABLED) or produce a redacted copy
of debug where fields like historicalSwapMatches are replaced with counts or
masked addresses; update the console.log statements that reference balance,
normalized, and debug to use the sanitized/aggregated output instead.


return {
success: true,
balance: {
spendable: balance.spendable,
regular: balance.regular,
swap: balance.swap,
contract: balance.contract,
fidelity: balance.fidelity,
},
balance: normalized,
};
Comment on lines +635 to 645
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

Keep both balance IPCs on the same normalization path.

After this change taker:getBalance returns normalized numbers, but taker:checkSwapLiquidity later in this file still derives spendable, regular, and swap from the raw native payload. The same wallet can now report different balance figures depending on which IPC route the renderer calls. Reuse normalizeBalancePayload() there as well.

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

In `@api1.js` around lines 635 - 645, taker:getBalance now returns normalized
numbers but taker:checkSwapLiquidity still computes spendable/regular/swap from
the raw native payload; update taker:checkSwapLiquidity to call
normalizeBalancePayload(balance, rawUtxos) (same as used in the taker:getBalance
path) and use the returned normalized values (e.g., normalized.spendable,
normalized.regular, normalized.swap) instead of deriving from the raw payload so
both IPC routes report the same figures.

} catch (error) {
console.error('❌ Failed to get balance:', error);
Expand Down
Loading