Skip to content

Feat: Firo network#799

Open
kvhnuke wants to merge 115 commits into
developfrom
firoorg-feature/add-firo-network
Open

Feat: Firo network#799
kvhnuke wants to merge 115 commits into
developfrom
firoorg-feature/add-firo-network

Conversation

@kvhnuke
Copy link
Copy Markdown
Contributor

@kvhnuke kvhnuke commented May 13, 2026

Summary by CodeRabbit

  • New Features

    • Added Firo (mainnet and testnet) network support with Spark address functionality.
    • Implemented anonymize funds capability for Firo transactions.
    • Added send and receive features for Spark addresses.
    • Introduced private balance tracking for Firo accounts.
    • Added transaction verification UI for Spark-based transfers.
  • Documentation

    • Updated supported networks list to include Firo.
  • Chores

    • Added required dependencies for Firo integration.
    • Updated project configuration files.

kvhnuke and others added 30 commits December 23, 2024 15:39
…ture/add-firo-network

# Conflicts:
#	packages/extension/src/libs/utils/wasmModule/spark.js
Add TX activity history after sending from Spark address
…ed-spark

Disable activity when spark to spark transaction is failed or dropped
narekpetrosyan and others added 26 commits February 11, 2026 00:08
Fix activity state and add reset DB on reset
2. Fix state init
3. In QR now shows Firo:{address}
1. Accounts list show spark addresses
In getSparkCoinInfo, the WASM buffer for the serial context was allocated
with serializedCoin.length and then populated with serialContext via
HEAPU8.set(serialContext, ...). When serialContext.length exceeds
serializedCoin.length, this writes past the allocated region, corrupting
WASM heap memory and producing non-deterministic failures or undefined
behavior in js_deserializeCoin. Use serialContext.length, matching the
pattern already used in libs/spark-handler/index.ts.

Co-authored-by: Reuben Yap <reuben@firo.org>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 13, 2026

💼 Build Files
chrome: enkrypt-chrome-1ee08ae4.zip
firefox: enkrypt-firefox-1ee08ae4.zip

💉 Virus total analysis
chrome: 1ee08ae4
firefox: 1ee08ae4

@kvhnuke kvhnuke mentioned this pull request May 13, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

Walkthrough

Adds Firo mainnet/testnet and Spark privacy: wallet/Electrum, WASM bridge, IndexedDB sync/worker, provider/activity, and UI for transparent/Spark send and verification, with private balance aggregation.

Changes

Firo + Spark end-to-end integration

Layer / File(s) Summary
Dependencies, network enums/configs, and Bitcoin network typing
.gitignore, README.md, package.json, packages/extension/package.json, packages/types/src/networks.ts, packages/extension/src/providers/bitcoin/networks/*, packages/extension/src/types/provider.ts, packages/extension/src/providers/bitcoin/types/{bitcoin-network.ts,index.ts}
Adds deps, Firo mainnet/testnet configs, provider/type updates, and total balance including Spark private balance.
Electrum client, WASM loaders, BaseFiroWallet and helpers
.../electrum-client/*, .../utils/wasm-*.ts, .../firo-wallet/*
Implements Electrum models/client; loads Spark WASM; wallet derives keys/addresses, fetches UTXOs/anonymity sets, and broadcasts.
Spark transaction helpers and orchestration
packages/extension/src/libs/spark-handler/*
Adds constants, PSBT/temp tx builders, fee/mint data, coin info, context serialization, and full spend assembly via WASM and DB.
Background unlock, keyring additions, and account filtering/init
.../libs/background, .../libs/keyring/*, packages/keyring/*, .../utils/accounts.ts, .../utils/initialize-wallet.ts, .../number-formatter.ts
Injects mnemonic into wallet on unlock; keyring exposes mnemonic/private key; filters/initializes Firo accounts; TS ignore.
Firo Provider API, activity handler, and utility tweaks
.../libs/api-firo.ts, .../activity-handlers/providers/firo, .../libs/utils.ts, .../filter-ordinals.ts, .../libs/api.ts
Adds Firo API and activity mapping; Spark address validator; minor import/log tweaks.
Spark sync (coin/tag), worker, IndexedDB, and activity updates
.../ui/action/db/indexedDB.ts, .../composables/*, .../workers/sparkCoinInfoWorker.ts, .../views/network-activity/*
Adds IndexedDB helper, coin/tag sync loops, worker for coin info, async-computed utilities, and Spark activity rendering.
UI send flows (transparent/Spark) and signing
.../ui/libs/signer.ts, .../ui/send-transaction/*, .../ui/action/views/verify-send-*.vue, .../ui/types.ts
Adds Firo signer; send tabs with Spark; Spark address input; verify views for mint/spend flows.
UI wiring: App, accounts/deposit, assets, main/router, app-menu
.../ui/action/App.vue, .../accounts-*/*, .../views/deposit/index.vue, .../views/network-assets/*, .../asset-detail-view/index.vue, .../main.ts, .../router/index.ts, .../components/app-menu/index.vue
Wires Spark state; displays Spark address; deposit tabs; assets totals; mount after Electrum connect; routes and dev menu.
Tests, Ethereum formatting, on-board, tsconfig, vite, lock/reset
.../tests/*, .../ethereum/libs/transaction/types.ts, .../onboard/App.vue, tsconfig.app.json, vite.config.ts, .../views/lock-screen/*
Test updates/mocks; formatting; TS config; build import reorder; DB reset on lock/reset.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • gamalielhere
  • NickKelly1
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch firoorg-feature/add-firo-network

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/extension/src/ui/action/views/reset-wallet/index.vue (1)

69-78: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Ensure isProcessing is reset when reset flow fails.

If Line 72 or Line 73 throws, the button remains disabled indefinitely.

Proposed fix
 const resetAction = async () => {
-  isProcessing.value = true;
-  const keyring = new KeyRingBase();
-  await keyring.reset();
-  await db.resetDB();
-  emit('action:lock');
-  openOnboard().then(() => {
-    window.close();
-  });
+  isProcessing.value = true;
+  try {
+    const keyring = new KeyRingBase();
+    await keyring.reset();
+    await db.resetDB();
+    emit('action:lock');
+    await openOnboard();
+    window.close();
+  } finally {
+    isProcessing.value = false;
+  }
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/ui/action/views/reset-wallet/index.vue` around lines
69 - 78, The resetAction currently sets isProcessing.value = true but never
resets it if KeyRingBase().reset() or db.resetDB() throws; wrap the async steps
in a try/catch/finally (or use .catch/.finally) so that any thrown error is
handled and isProcessing.value is set back to false in the finally block; keep
the existing emit('action:lock') and openOnboard() logic but ensure
isProcessing.value = false runs after success or failure, and optionally rethrow
or handle/log the error in the catch to preserve behavior.
packages/extension/src/ui/action/views/lock-screen/components/lock-screen-forgot.vue (1)

71-77: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add failure recovery for processing state in reset flow.

If Line 74 or Line 75 throws, the action stays locked because isProcessing never returns to false.

Proposed fix
 const resetAction = async () => {
-  isProcessing.value = true;
-  const keyring = new KeyRingBase();
-  await keyring.reset();
-  await db.resetDB();
-  openOnboard();
+  isProcessing.value = true;
+  try {
+    const keyring = new KeyRingBase();
+    await keyring.reset();
+    await db.resetDB();
+    await openOnboard();
+  } finally {
+    isProcessing.value = false;
+  }
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/extension/src/ui/action/views/lock-screen/components/lock-screen-forgot.vue`
around lines 71 - 77, The resetAction currently sets isProcessing.value = true
but never guarantees it is set back to false if KeyRingBase().reset() or
db.resetDB() throws; wrap the async work in a try/catch/finally (or use
.finally) so that isProcessing.value is reset to false in the finally block,
optionally handling/logging errors from KeyRingBase.reset and db.resetDB before
calling openOnboard; ensure references to isProcessing, resetAction,
KeyRingBase.reset, db.resetDB and openOnboard are updated so the processing
state is always cleared even on failure.
packages/extension/src/ui/action/App.vue (1)

392-433: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Set sparkAccount to null for non-Firo networks.

Initializing sparkAccount as {} makes it truthy on all networks and can bypass null checks in consumers expecting either a populated SparkAccount or null.

Suggested fix
-  const sparkAccount = {} as SparkAccount;
+  let sparkAccount: SparkAccount | null = null;
...
-  if (network.name === NetworkNames.Firo) {
+  if (network.name === NetworkNames.Firo) {
+    sparkAccount = {} as SparkAccount;
...
-    sparkAccount['sparkBalance'] = { availableBalance };
+    sparkAccount.sparkBalance = { availableBalance };
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/ui/action/App.vue` around lines 392 - 433, The code
always initializes sparkAccount as an empty object which is truthy on non-Firo
networks; change the logic so sparkAccount is only populated for
NetworkNames.Firo and set to null otherwise. Specifically, update the
initialization and branching around the sparkAccount variable (the const
sparkAccount declaration and the if (network.name === NetworkNames.Firo) block)
so that when the network is not Firo you assign null to sparkAccount, and when
it is Firo you build the object as currently done; ensure
accountHeaderData.value uses that null value for sparkAccount so consumers can
reliably check for presence.
🟡 Minor comments (17)
packages/extension/src/libs/spark-handler/utils.ts-14-23 (1)

14-23: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add error handling for invalid base64 input.

The atob function on Line 15 will throw an error if the input is not valid base64. This could cause uncaught exceptions in the caller.

🛡️ Proposed fix with validation
 export const base64ToReversedHex = (base64: string): string => {
+  if (!base64) {
+    throw new Error('Base64 input cannot be empty');
+  }
+
+  let binary: string;
+  try {
-  const binary = atob(base64);
+    binary = atob(base64);
+  } catch (error) {
+    throw new Error(`Invalid base64 input: ${error instanceof Error ? error.message : 'unknown error'}`);
+  }
+
   const bytes = new Uint8Array(binary.length);
 
   for (let i = 0; i < binary.length; i++) {
     bytes[i] = binary.charCodeAt(i);
   }
   const reversed = Array.from(bytes).reverse();
   return reversed.map(b => b.toString(16).padStart(2, '0')).join('');
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/libs/spark-handler/utils.ts` around lines 14 - 23, The
function base64ToReversedHex can throw when atob receives invalid base64; wrap
the decoding in a try/catch (or validate the input before calling atob) inside
base64ToReversedHex so the function either returns a clear error value or throws
a controlled Error with a descriptive message; ensure the catch handles
malformed input and does not allow uncaught exceptions to propagate to callers.
packages/extension/src/providers/bitcoin/libs/firo-wallet/utils.ts-1-19 (1)

1-19: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix callback signature mismatch and improve type safety.

The callback signature declares an optional index parameter, but Line 11 never passes it when calling callback(data[i]). Additionally, using any[] reduces type safety.

🔧 Proposed fix
-export const chunkedEvery = (
-  data: any[],
+export const chunkedEvery = <T>(
+  data: T[],
   chunksize: number,
-  callback: (data: any, index?: number) => void,
+  callback: (data: T, index: number) => void,
   finished: () => void,
 ) => {
   let i = 0;
   (function chunk() {
     const end = Math.min(i + chunksize, data.length);
     for (; i < end; ++i) {
-      callback(data[i]);
+      callback(data[i], i);
     }
     if (i < data.length) {
       setTimeout(chunk, 0);
     } else {
       finished.call(null);
     }
   })();
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/providers/bitcoin/libs/firo-wallet/utils.ts` around
lines 1 - 19, The function chunkedEvery has a callback signature mismatch and
weak typing; change it to be generic (e.g., <T>) and type the parameters as
data: T[], chunksize: number, callback: (item: T, index: number) => void,
finished: () => void, and when invoking the callback inside chunk() pass both
the item and its index (call callback(data[i], i)); update any other related
signatures in chunkedEvery to use the new generic types for improved type
safety.
packages/extension/src/libs/spark-handler/utils.ts-1-12 (1)

1-12: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix return type and add input validation.

The function has type safety and validation issues:

  1. Lines 9-11 use optional chaining and can return undefined, but the return type isn't declared as string | undefined
  2. No validation for negative numbers, which would produce unexpected results
  3. The regex match can return null if no matches are found (though unlikely with the current logic)
🔧 Proposed fix
-export const numberToReversedHex = (num: number) => {
+export const numberToReversedHex = (num: number): string => {
+  if (num < 0) {
+    throw new Error('Number must be non-negative');
+  }
+
   let hex = num.toString(16);
 
   if (hex.length % 2 !== 0) {
     hex = '0' + hex;
   }
 
-  const bytes = hex.match(/.{2}/g);
-  const reversed = bytes?.reverse();
-
-  return reversed?.join('');
+  const bytes = hex.match(/.{2}/g) || [];
+  return bytes.reverse().join('');
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/libs/spark-handler/utils.ts` around lines 1 - 12,
numberToReversedHex currently can return undefined and accepts invalid inputs;
change its signature to return a string and add input validation: in
numberToReversedHex validate that num is a finite, non-negative safe integer
(e.g., Number.isFinite and Number.isSafeInteger and num >= 0) and throw a
TypeError for invalid inputs; compute hex = Math.floor(num).toString(16), pad
with a leading '0' when needed, call hex.match(/.{2}/g) and assert the result
(bytes) is non-null before reversing, then join and return the resulting string
so bytes/reversed never produce undefined (use local variables bytes and
reversed from the function to locate the logic to change).
packages/extension/src/ui/action/views/lock-screen/index.vue-59-59 (1)

59-59: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove the unused update:init-spark-state emit event.

The update:init-spark-state event is declared but never emitted in this component and has no listeners in parent components. Remove it if it's not needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/ui/action/views/lock-screen/index.vue` at line 59,
Remove the unused emit declaration for 'update:init-spark-state' from the
component's emits/type signature (the line declaring (e:
'update:init-spark-state', password: string): void;), since the event is never
emitted or listened to; update the emits/type definition to exclude
'update:init-spark-state' and run type checks to ensure no other code relies on
it (search for references to 'update:init-spark-state' to confirm safe removal).
packages/extension/src/libs/utils/wasm-loader.ts-34-44 (1)

34-44: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove async Promise executor anti-pattern.

Using async in a Promise executor is unnecessary and can hide errors. The static analysis tool correctly flags this.

🔧 Proposed fix
 async function loadWasm(): Promise<WasmModule> {
-  return new Promise(async (resolve, reject) => {
-    const wasmModule = await initSpark();
-
-    if (typeof wasmModule !== 'undefined') {
-      resolve(wasmModule);
-    } else {
-      reject(new Error('Failed to load WASM module.'));
-    }
-  });
+  const wasmModule = await initSpark();
+  
+  if (typeof wasmModule === 'undefined') {
+    throw new Error('Failed to load WASM module.');
+  }
+  
+  return wasmModule;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/libs/utils/wasm-loader.ts` around lines 34 - 44, The
Promise executor in loadWasm uses an unnecessary async function which can hide
errors; remove the async Promise-executor anti-pattern by not passing an async
function into new Promise. Instead, call initSpark() outside of a new Promise
(or use a synchronous executor) — await initSpark() directly in loadWasm, then
resolve or throw based on the returned wasmModule (refer to loadWasm and
initSpark) so errors propagate normally instead of being swallowed by an async
executor.
packages/extension/src/providers/bitcoin/ui/send-transaction/components/send-spark-address-input.vue-86-88 (1)

86-88: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Run Spark validation for the initial value.

This watcher is lazy, so a prefilled or restored address renders as invalid until the user edits it. That makes valid Spark destinations start in the red/error state on first paint.

Suggested fix
-watch(btcAddress, async () => {
-  isValidSparkAddress.value = await isSparkAddress(btcAddress.value);
-});
+watch(
+  btcAddress,
+  async () => {
+    isValidSparkAddress.value = await isSparkAddress(btcAddress.value);
+  },
+  { immediate: true },
+);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/extension/src/providers/bitcoin/ui/send-transaction/components/send-spark-address-input.vue`
around lines 86 - 88, The watcher on btcAddress only validates on changes so
prefilled/restored addresses remain unvalidated; update the logic to validate
the initial value as well by either making the watcher immediate
(watch(btcAddress, ..., { immediate: true })) or invoking
isSparkAddress(btcAddress.value) once during component setup/onMounted and
assigning the result to isValidSparkAddress.value; reference btcAddress,
isValidSparkAddress, and isSparkAddress so you update the existing watcher or
add the one-time initial validation call.
packages/extension/src/providers/bitcoin/ui/send-transaction/components/send-spark-address-input.vue-90-93 (1)

90-93: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Emit the blur transition too.

toggle:showContacts is only emitted on focus. After a blur, the parent never receives false, so any contacts UI opened from this field can stay visible with stale state.

Suggested fix
 const changeFocus = (val: FocusEvent) => {
   isFocus.value = val.type === 'focus';
-  if (isFocus.value) emit('toggle:showContacts', isFocus.value);
+  emit('toggle:showContacts', isFocus.value);
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/extension/src/providers/bitcoin/ui/send-transaction/components/send-spark-address-input.vue`
around lines 90 - 93, The changeFocus handler only emits toggle:showContacts on
focus, leaving the parent with stale true after blur; update the changeFocus
function (changeFocus and isFocus.value) to emit('toggle:showContacts',
isFocus.value) for both focus and blur (i.e., remove the conditional so the emit
always runs after setting isFocus.value) so the parent receives false on blur
and the contacts UI state stays in sync.
packages/extension/src/providers/bitcoin/libs/api-firo.ts-76-91 (1)

76-91: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use Error objects for rejected broadcast failures.

On Line 88, Promise.reject(response.message) rejects a string. Downstream handlers commonly read error.message, which becomes undefined and weakens telemetry/UI reporting.

Suggested fix
       .then(response => {
         if (response.error) {
-          return Promise.reject(response.message);
+          return Promise.reject(
+            new Error(
+              typeof response.message === 'string'
+                ? response.message
+                : 'Firo broadcast failed',
+            ),
+          );
         }
         return response;
       });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/providers/bitcoin/libs/api-firo.ts` around lines 76 -
91, The broadcastTx function currently rejects with a raw string
(Promise.reject(response.message)) which loses error.message; change the
rejection to an Error object so callers can read error.message — in broadcastTx,
where it checks if (response.error) replace Promise.reject(response.message)
with rejecting a new Error constructed from response.message (fallback to
JSON.stringify(response) if message is missing) so the rejected value is an
Error instance.
packages/extension/src/ui/action/views/asset-detail-view/index.vue-66-73 (1)

66-73: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard fiat recomputation against non-numeric values.

Line 70 can yield NaN when token.valuef is non-numeric/formatted, causing invalid USD output instead of a safe fallback.

Suggested fix
           {{
             $filters.parseCurrency(
-              token.balanceTotal
-                ? Number(token.balanceTotal) * Number(token.valuef)
-                : token.balanceUSDf,
+              token.balanceTotal &&
+                Number.isFinite(Number(token.balanceTotal)) &&
+                Number.isFinite(Number(token.value))
+                ? Number(token.balanceTotal) * Number(token.value)
+                : token.balanceUSDf,
             )
           }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/ui/action/views/asset-detail-view/index.vue` around
lines 66 - 73, The template is computing fiat with Number(token.balanceTotal) *
Number(token.valuef) which can produce NaN when token.valuef is non-numeric;
update the expression (or move to a small computed helper) to coerce and
validate both values (e.g., safeBalance = Number(token.balanceTotal), safeValue
= Number(token.valuef)), check isFinite/safeValue and isFinite/safeBalance,
compute fiat = isFinite(safeBalance) && isFinite(safeValue) ? safeBalance *
safeValue : Number(token.balanceUSDf) || 0, and pass that fiat to
$filters.parseCurrency to ensure parseCurrency never receives NaN (use the
existing symbols token.balanceTotal, token.valuef, token.balanceUSDf, and
$filters.parseCurrency).
packages/extension/src/ui/action/components/accounts-header/components/header-accounts.vue-6-10 (1)

6-10: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate that sparkAddress is provided when network is Firo.

When network.name === NetworkNames.Firo, the code uses sparkAddress for the identicon and display address. However, sparkAddress has a default value of empty string (Line 145), which could result in rendering an empty identicon and truncated empty address if the parent component doesn't provide it.

🛡️ Proposed fix to add validation or fallback
       <img
         :src="
           network.identicon(
-            network.name === NetworkNames.Firo ? sparkAddress : address,
+            network.name === NetworkNames.Firo ? (sparkAddress || address) : address,
           )
         "
         class="account__info-images__identicon"
       />
       <span>{{
         $filters.replaceWithEllipsis(
-          network.name === NetworkNames.Firo ? sparkAddress : address,
+          network.name === NetworkNames.Firo ? (sparkAddress || address) : address,
           6,
           4,
         )
       }}</span>

Also applies to: 22-28

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/extension/src/ui/action/components/accounts-header/components/header-accounts.vue`
around lines 6 - 10, When rendering identicons/addresses for Firo in
header-accounts.vue, ensure sparkAddress is validated before use: in the
conditional that calls network.identicon(network.name === NetworkNames.Firo ?
sparkAddress : address) and the display/address truncation logic (also at the
22-28 block), check that sparkAddress is non-empty and falls back to address or
a known placeholder (e.g., a default identicon seed or "—") when empty; if
fallback is used, optionally emit a warning via console or a local logger.
Update the conditional(s) to prefer sparkAddress only when truthy, e.g., use
(network.name === NetworkNames.Firo && sparkAddress) ? sparkAddress : address,
and ensure the displayed text similarly falls back to address so no empty
identicon or truncated empty string is rendered.
packages/extension/src/providers/bitcoin/types/bitcoin-network.ts-40-41 (1)

40-41: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Return '0' instead of empty string for consistency.

When no private balance resolver is found, the function returns an empty string. This is inconsistent with calculateCurrentSparkBalance (Line 11) which returns '0' for empty/missing balances. An empty string could cause unexpected behavior in UI components expecting numeric strings.

🛡️ Proposed fix for consistency
   const resolvePrivateBalance = privateBalanceResolvers[networkName.toUpperCase()];
-  if (!resolvePrivateBalance) return '';
+  if (!resolvePrivateBalance) return '0';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/providers/bitcoin/types/bitcoin-network.ts` around
lines 40 - 41, The resolver lookup currently returns an empty string when no
resolver is found; change this to return the string '0' to match
calculateCurrentSparkBalance's behavior. Update the branch that checks
resolvePrivateBalance (using privateBalanceResolvers and
networkName.toUpperCase()) so that instead of returning '' it returns '0',
ensuring callers and UI components always receive a numeric string.
packages/extension/src/libs/utils/updateAndSync/calculateCurrentSparkBalance.ts-7-18 (1)

7-18: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add error handling for IndexedDB operations.

The function performs an IndexedDB read without a try-catch block. If db.readData fails or if the reduce encounters unexpected data, the promise will reject without graceful handling.

🛡️ Proposed fix to add error handling
 export const calculateCurrentSparkBalance = async () => {
+  try {
     const myCoins = await db.readData<MyCoinModel[]>(DB_DATA_KEYS.myCoins);
 
     if (!myCoins?.length) {
       return '0';
     }
 
     const unusedCoins = myCoins.filter(el => !el.isUsed);
     const balance = unusedCoins.reduce((a: bigint, c) => a + c.value, 0n);
 
     return new BigNumber(balance.toString()).toString();
+  } catch (error) {
+    console.error('Failed to calculate Spark balance:', error);
+    return '0';
+  }
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/extension/src/libs/utils/updateAndSync/calculateCurrentSparkBalance.ts`
around lines 7 - 18, Wrap the body of calculateCurrentSparkBalance in a
try-catch: call db.readData(DB_DATA_KEYS.myCoins) inside the try, validate
myCoins is an array before using filter/reduce, protect the reduce by ensuring
each coin has a numeric/BigInt value (or coerce with BigInt) and use 0n as the
initial accumulator; on any error log the error (console.error or the project
logger) and return '0' to fail gracefully instead of letting the promise reject.
packages/extension/src/libs/utils/updateAndSync/handleCoinSetUpdates.ts-54-67 (1)

54-67: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add bounds checking for setId.

The code checks setId < 1 but doesn't validate the upper bound. Accessing updatedSets[setIndex] when setIndex >= updatedSets.length would create sparse array entries or undefined behavior.

🛡️ Proposed fix to add upper bound check
   normalizedUpdates.forEach(update => {
     const setId = update.setId;
     if (!setId || setId < 1) return;
     const setIndex = setId - 1;
+    
+    // Ensure array is large enough
+    while (updatedSets.length <= setIndex) {
+      updatedSets.push({ blockHash: '', setHash: '', coins: [] });
+    }
 
     const existingSet = updatedSets[setIndex];
     const mergedCoins = mergeCoins(existingSet?.coins ?? [], update.set.coins);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/libs/utils/updateAndSync/handleCoinSetUpdates.ts`
around lines 54 - 67, The loop over normalizedUpdates does not check the upper
bound for setId, so setIndex may exceed updatedSets.length and create sparse
entries; modify the forEach block that uses setId/setIndex (references:
normalizedUpdates, setId, setIndex, updatedSets, mergeCoins) to validate that
setIndex is within [0, updatedSets.length - 1] before reading/updating
updatedSets—if setIndex is out of range, skip the update (return) or handle it
explicitly (e.g., log/warn) so you never assign to updatedSets[setIndex] when it
would extend the array unintentionally.
packages/extension/src/ui/action/views/network-activity/index.vue-92-95 (1)

92-95: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unused isSyncing prop.

The isSyncing prop is defined but never referenced in the template, script logic, or passed to any child components. The child component network-activity-total uses its own isSyncing computed property derived from different props (isCoinSyncing and isTagSyncing), making this parent prop dead code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/ui/action/views/network-activity/index.vue` around
lines 92 - 95, Remove the dead prop definition isSyncing from the props block in
the parent component (index.vue) since it is never read or passed down; update
the props object to no longer declare isSyncing and run a quick search for any
references to isSyncing in this component to ensure nothing else relies on it
(the child component network-activity-total computes its own isSyncing from
isCoinSyncing/isTagSyncing so no changes are needed there).
packages/extension/src/ui/action/composables/update-activity-state.ts-2-2 (1)

2-2: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update import to use the public entrypoint @enkryptcom/types.

The import from @enkryptcom/types/dist is inconsistent with the codebase standard and breaks with export-map changes. Change to import { NetworkNames } from '@enkryptcom/types'; to match the rest of the codebase. Note: get-spark-address.ts, synchronize-state.vue, accounts-list-item.vue, and transparent-send-tab.vue have the same issue.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/ui/action/composables/update-activity-state.ts` at
line 2, Replace the non-public import of NetworkNames from
'@enkryptcom/types/dist' with the public package entrypoint by changing
occurrences like "import { NetworkNames } from '@enkryptcom/types/dist';" to
"import { NetworkNames } from '@enkryptcom/types';" (apply the same change in
the other affected files get-spark-address.ts, synchronize-state.vue,
accounts-list-item.vue, and transparent-send-tab.vue) so the code uses the
documented public export and avoids export-map breakage.
packages/extension/src/providers/bitcoin/ui/send-transaction/index.vue-296-303 (1)

296-303: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid global error styling for all <p> in this view.

This forces every paragraph in the send screen to use @error color, including non-error text. Scope it to a dedicated error class instead.

Suggested fix
-p {
-  font-weight: 400;
-  font-size: 14px;
-  line-height: 20px;
-  letter-spacing: 0.25px;
-  color: `@error`;
-  margin: 0;
-}
+.send-transaction__error {
+  font-weight: 400;
+  font-size: 14px;
+  line-height: 20px;
+  letter-spacing: 0.25px;
+  color: `@error`;
+  margin: 0;
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/providers/bitcoin/ui/send-transaction/index.vue`
around lines 296 - 303, The stylesheet currently targets all p elements and
forces the error color; change the selector to a dedicated class (e.g.,
.error-text or .send-error) instead of p, remove the global p color rule, and
update the template in index.vue to apply that class only to the paragraph(s)
that display error messages (replace <p> used for errors with <p
class="error-text"> or similar); ensure the style is scoped to the component if
needed so other views aren’t affected.
packages/extension/src/libs/utils/updateAndSync/markCoinsAsUsed.ts-72-80 (1)

72-80: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Return a numeric fallback value here.

SparkUnusedTxDetails.value is typed as number, but this branch returns '0' as a string. That will leak a mixed type into downstream arithmetic/sorting even though the predicate later asserts the result is SparkUnusedTxDetails.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/libs/utils/updateAndSync/markCoinsAsUsed.ts` around
lines 72 - 80, The branch that handles missing coinValue returns value: '0' (a
string), which conflicts with the expected numeric SparkUnusedTxDetails.value
and can break downstream arithmetic/sorting; update the return in that branch to
use a numeric fallback (value: 0) and ensure the object shape returned by the
function (e.g., in markCoinsAsUsed) preserves the typed SparkUnusedTxDetails;
verify coinValue, txId and index logic still match before returning the fallback
so TypeScript and consumers receive a number not a string.
🧹 Nitpick comments (18)
packages/extension/src/libs/utils/number-formatter.ts (1)

316-317: ⚡ Quick win

Remove blanket TypeScript suppression in percentage parsing.

// @ts-ignore`` on Line 316 hides real type errors on Line 317. Prefer a safe coercion path instead of suppressing checks.

Suggested fix
-  // `@ts-ignore`
-  const value = new BigNumber(_value.toString().replaceAll('%', ''));
+  const sanitized = String(_value).replace(/%/g, '');
+  const value = new BigNumber(sanitized);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/libs/utils/number-formatter.ts` around lines 316 -
317, Remove the blanket "// `@ts-ignore`" before the percentage parsing and
instead coerce and sanitize _value safely before constructing BigNumber: ensure
_value is not null/undefined, convert it to a string using a safe coercion
(e.g., String(_value)), strip percent signs using a regex-based replace (avoid
replaceAll for compatibility), and then pass the sanitized string into new
BigNumber(...); update the variable references (_value, value, BigNumber,
replaceAll) accordingly and add a guard to handle empty or invalid sanitized
input (e.g., fallback to "0" or throw a clear error).
packages/name-resolution/tests/resolver.test.ts (2)

40-40: ⚡ Quick win

Update assertions to Vitest style for consistency.

These assertions still use Chai-style .to.be.eq() syntax. For consistency with the updates made in ud.test.ts and to align with Vitest best practices, consider updating to .toBe().

Example fix for line 40
-    expect(address).to.be.eq("0x54fd548fFD6Ff54943d6863F1d485Cc2Fb495574");
+    expect(address).toBe("0x54fd548fFD6Ff54943d6863F1d485Cc2Fb495574");

Also applies to: 42-42, 45-45, 63-63, 67-67

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/name-resolution/tests/resolver.test.ts` at line 40, Replace
Chai-style assertions in the resolver tests with Vitest-style matchers: for each
occurrence of expect(...).to.be.eq(...) or expect(...).to.be.eql(...), update to
expect(...).toBe(...) or expect(...).toEqual(...) as appropriate (e.g. the
assertion using the variable address on line 40 and the similar assertions at
lines 42, 45, 63, 67 in resolver.test.ts). Ensure you only change the matcher
syntax (keep the same expected values and variables like address, addressEns,
etc.) so tests use Vitest's .toBe/.toEqual consistently.

4-23: ⚡ Quick win

Remove commented-out code or implement it properly.

Similar to sid.test.ts, this commented-out mock code should either be properly implemented or removed. If uncommented, it would fail due to the missing vi import from vitest.

Additionally, these changes appear unrelated to the PR objective of adding Firo network support.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/name-resolution/tests/resolver.test.ts` around lines 4 - 23, The
commented-out vi.mock blocks in resolver.test.ts (the mocks for createWeb3Name,
createSolName, createPaymentIdName using vi.mock) should be either removed or
properly implemented: either delete these commented lines since they are
unrelated to the Firo work, or uncomment and add the proper Vitest import (e.g.,
import { vi } from 'vitest') and ensure the mock factories (createWeb3Name,
createSolName, createPaymentIdName) return the mocked methods (getDomainName,
getAddress) as shown; keep only the approach you choose to avoid unrelated test
noise.
packages/name-resolution/tests/sid.test.ts (2)

36-36: ⚡ Quick win

Update assertions to Vitest style for consistency.

The assertions in this file still use Chai-style .to.be.eq() syntax, while ud.test.ts was updated to use Vitest's .toBe() syntax. For consistency across the test suite, consider updating these assertions.

Example fix for line 36
-    expect(address).to.be.eq("0xb5932a6B7d50A966AEC6C74C97385412Fb497540");
+    expect(address).toBe("0xb5932a6B7d50A966AEC6C74C97385412Fb497540");

Also applies to: 50-50, 64-64, 69-69

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/name-resolution/tests/sid.test.ts` at line 36, Tests in sid.test.ts
use Chai-style assertions (e.g., expect(address).to.be.eq(...)); replace them
with Vitest-style matchers by changing calls like
expect(address).to.be.eq("...") to expect(address).toBe("...") (and similarly
update the other occurrences noted around the other expect calls at the same
file). Locate the expect(...) usages for the variable/address assertions and
update the matcher to toBe(...), ensuring you keep the same expected string
values and do not reintroduce Chai chain syntax.

4-23: ⚡ Quick win

Remove commented-out code or implement it properly.

Commented-out code is generally considered a code smell and should be removed. Additionally, if these mocks were uncommented, they would fail because vi is not imported from vitest.

If mocking is needed to make tests pass, either:

  1. Properly implement the mocks with the necessary imports, or
  2. Remove the commented code entirely

Also note that these changes appear unrelated to the PR objective of adding Firo network support.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/name-resolution/tests/sid.test.ts` around lines 4 - 23, Remove the
large commented-out mock block in the tests (the createWeb3Name / createSolName
/ createPaymentIdName mocks) or implement them correctly: if you need the mocks,
import vi from vitest at the top (import { vi } from 'vitest') and uncomment and
adapt the mock factories so they export the mocked functions used by the tests;
otherwise simply delete the commented code to avoid unrelated noise in
sid.test.ts. Ensure any retained mocks reference the exact factory names
createWeb3Name, createSolName, and createPaymentIdName so the test harness picks
them up.
packages/extension/tsconfig.app.json (1)

13-14: ⚡ Quick win

Document why skipLibCheck and allowJs are required.

These options are necessary for the current setup:

  • allowJs: true is required to import spark.js WASM modules (Emscripten-generated files in libs/utils/wasmModule/ and wasmWorkerModule/)
  • skipLibCheck: true is needed because bitcoinjs-lib-firo is a custom GitHub fork without TypeScript type definitions

Consider adding a comment in tsconfig.app.json briefly explaining these requirements so future maintainers understand why they cannot be safely removed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/tsconfig.app.json` around lines 13 - 14, Add inline
comments in tsconfig.app.json next to the "skipLibCheck" and "allowJs" compiler
options explaining why they are required: note that "allowJs": true is needed to
import Emscripten-generated spark.js WASM modules from libs/utils/wasmModule and
wasmWorkerModule, and "skipLibCheck": true is required because we depend on a
custom fork (bitcoinjs-lib-firo) that lacks TypeScript definitions; update the
comment to warn maintainers not to remove these flags without adding proper
typings or replacing the WASM build artifacts.
packages/extension/src/libs/spark-handler/serializeMintContext.ts (1)

14-49: ⚡ Quick win

Consider adding input validation.

The function doesn't validate that:

  • txHashLength doesn't exceed the actual txHash buffer length (Line 20)
  • vout is within valid uint32 range
  • inputs array is not empty

Invalid inputs could produce malformed serialization that may cause issues in the WASM layer.

🛡️ Proposed validation checks
 export const serializeMintContext = (
   inputs: DartInputData[],
 ): SerializedMintContextResult => {
+  if (inputs.length === 0) {
+    throw new Error('Inputs array cannot be empty');
+  }
+
   const serialContextStream: Buffer[] = [];
 
   for (let i = 0; i < inputs.length; i++) {
+    if (inputs[i].txHashLength > inputs[i].txHash.length) {
+      throw new Error(`Invalid txHashLength at input ${i}`);
+    }
+    if (inputs[i].vout < 0 || inputs[i].vout > 0xffffffff) {
+      throw new Error(`Invalid vout value at input ${i}`);
+    }
+
     const txHashBuffer = inputs[i].txHash.slice(0, inputs[i].txHashLength);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/libs/spark-handler/serializeMintContext.ts` around
lines 14 - 49, In serializeMintContext validate inputs before serializing:
ensure inputs.length > 0 and throw/return a clear error if empty; for each input
verify inputs[i].txHashLength is <= inputs[i].txHash.length and that
inputs[i].txHash is a Buffer (or convert/throw if not); check inputs[i].vout is
a safe unsigned 32-bit integer (0 <= vout <= 0xFFFFFFFF) and throw/return on
invalid values; perform these checks at the top of the loop (referencing
txHashLength, txHash, vout, and serializeMintContext) so malformed data is
rejected before building serialContextStream.
packages/extension/src/ui/action/utils/set-utils.ts (1)

11-19: 💤 Low value

Consider clarifying the function name or parameter order.

The differenceSets function computes update \ set (elements in update but not in set), which is the opposite of the standard mathematical set difference operation set \ update. This could be confusing for maintainers.

Consider either:

  1. Renaming to make the direction explicit (e.g., elementsNotInOriginal, newElementsIn)
  2. Swapping parameters to match standard set difference semantics
  3. Adding a JSDoc comment explaining the behavior
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/ui/action/utils/set-utils.ts` around lines 11 - 19,
The function differenceSets(set, update) currently returns elements in update
that are not in set (i.e., update \ set) which is counter‑intuitive; rename the
function (e.g., newElementsIn or elementsNotInOriginal) and add a short JSDoc to
clarify the behavior, then update all references to differenceSets to the new
name; alternatively, if you prefer standard set difference semantics, swap the
parameter order to (update, set) and update callers accordingly—refer to the
function symbol differenceSets and its parameters set and update when making the
change.
packages/extension/package.json (1)

66-66: ⚡ Quick win

Document the purpose of the forked dependency.

The bitcoinjs-lib-firo fork is used to support Firo network functionality (Spark balance, anonymization features). The dependency is pinned to a specific verified commit (4b0c4322cb9f308cb84b2f655d16a9e9abc7b4e1), which ensures reproducible builds. Consider adding an inline comment explaining that this fork is required for Firo-specific features and documenting any blockers to upstreaming these changes to the official bitcoinjs-lib.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/package.json` at line 66, Add an inline comment and brief
documentation explaining why the forked dependency "bitcoinjs-lib-firo" is used:
state it provides Firo-specific functionality (Spark balance and anonymization
features), note that it's pinned to commit
4b0c4322cb9f308cb84b2f655d16a9e9abc7b4e1 for reproducible builds, and summarize
any known blockers to upstreaming these changes to the official bitcoinjs-lib so
future maintainers know the rationale and upgrade path; update the package.json
near the "bitcoinjs-lib-firo" entry and add a short note in the repo README or
deps.md referencing the same commit and blockers.
packages/extension/src/ui/action/composables/async-computed.ts (2)

21-21: ⚡ Quick win

Type error more precisely.

Casting to as unknown as any loses all type information. Use a more specific error type.

♻️ Proposed fix
     } catch (err) {
-      if (!cancelled) error.value = err as unknown as any;
+      if (!cancelled) error.value = err instanceof Error ? err : new Error(String(err));
     } finally {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/ui/action/composables/async-computed.ts` at line 21,
The assignment "if (!cancelled) error.value = err as unknown as any" loses type
information; change it to assert a specific error shape (for example cast to
Error or a union like Error | string) or narrow at runtime (e.g., if (err
instanceof Error) error.value = err else error.value = new Error(String(err)))
so error.value keeps a meaningful type; update the code around variables
"error", "cancelled", and "err" in async-computed.ts accordingly to preserve
proper typing instead of using "as unknown as any".

3-3: ⚡ Quick win

Improve type safety with generic types.

The function signature uses any for both the async function return and initial value, which loses type information and may lead to runtime errors.

♻️ Proposed fix with generic typing
-export default (asyncFn: () => any, initialValue: any) => {
+export default <T>(asyncFn: () => Promise<T>, initialValue: T) => {
-  const value = ref(initialValue);
+  const value = ref<T>(initialValue);
   const isPending = ref(false);
-  const error = ref(null);
+  const error = ref<Error | null>(null);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/ui/action/composables/async-computed.ts` at line 3,
The exported default function currently types both parameters as any; change it
to use a generic type parameter (e.g., <T>) so the signature becomes generic:
the asyncFn parameter should produce T (or Promise<T>) and initialValue should
be T, and update any internal types/returns to use T so callers retain strong
typing; locate the anonymous exported function declaration starting with "export
default (asyncFn: () => any, initialValue: any) =>" and replace the any types
with a generic type parameter throughout.
packages/extension/src/providers/bitcoin/libs/activity-handlers/providers/firo/index.ts (1)

89-96: ⚡ Quick win

Use optional chaining for safer access.

Lines 90 and 94 use non-null assertions when mapping inputs/outputs. If the API returns unexpected data (e.g., missing addr or addresses), this will throw during the map operation.

♻️ Proposed fix
             inputs: tx.vin.map(input => ({
-              address: input.addr,
+              address: input.addr || '',
               value: Number(input.value),
             })),
             outputs: tx.vout.map(output => ({
-              address: output.scriptPubKey.addresses![0],
+              address: output.scriptPubKey.addresses?.[0] || '',
               value: Number(output.value),
               pkscript: output.scriptPubKey.hex,
             })),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/extension/src/providers/bitcoin/libs/activity-handlers/providers/firo/index.ts`
around lines 89 - 96, The mapping over tx.vin and tx.vout uses non-null
assertions and can throw if fields are missing; update the tx.vin.map and
tx.vout.map handlers (the inputs and outputs objects) to use optional chaining
and safe fallbacks (e.g., input?.addr ?? null, Number(input?.value ?? 0),
output?.scriptPubKey?.addresses?.[0] ?? null, Number(output?.value ?? 0), and
output?.scriptPubKey?.hex ?? null) so missing properties don’t cause exceptions
during mapping; ensure the created inputs/outputs keep the same property names
(address, value, pkscript) but tolerate undefined/null values instead of using
the ! operator.
packages/extension/src/libs/utils/wasm-loader.ts (1)

46-68: ⚖️ Poor tradeoff

Consider exposing WASM initialization errors.

The constructor (lines 50-56) starts loading the WASM module but only logs errors to the console. Callers using getInstance() won't know if initialization failed until they try to use the module. This can make debugging difficult.

Consider either:

  1. Exposing an initializationError property that callers can check
  2. Having getInstance() reject with the initialization error instead of retrying
  3. Documenting that callers must handle cases where the WASM module fails to load
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/libs/utils/wasm-loader.ts` around lines 46 - 68, Add
an initializationError property to the WasmInstance class, set it in the
constructor catch block when loadWasm() fails (instead of only console.error),
and update getInstance() to first check if initializationError is set and reject
with that error (rather than silently retrying); reference the WasmInstance
class, its instance field, the constructor's loadWasm().then/.catch, and the
getInstance() method when making these changes so callers can detect
initialization failures reliably.
packages/extension/src/providers/bitcoin/libs/utils.ts (1)

10-29: ⚖️ Poor tradeoff

Clarify error handling semantics.

When the WASM module fails to load (lines 16-18), the function returns false and logs to console. This conflates "WASM unavailable" with "invalid Spark address," which may lead to silent failures if callers assume false always means "not a Spark address."

Consider either:

  1. Throwing an error when WASM is unavailable (forcing callers to handle initialization failures)
  2. Returning a more explicit result like { valid: boolean, error?: string }
  3. At minimum, document that false can mean either "invalid address" or "validation unavailable"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/providers/bitcoin/libs/utils.ts` around lines 10 - 29,
The current isSparkAddress function conflates "WASM unavailable" with "invalid
address"; change its behavior to throw a clear initialization error when
wasmInstance.getInstance() returns falsy instead of returning false — in
isSparkAddress replace the console.log+false path with throwing an Error (e.g.,
"WASM not loaded: spark address validation unavailable") so callers of
isSparkAddress (and any code invoking js_isValidSparkAddress) must handle
initialization failures explicitly; update call sites to catch this error or, if
you prefer a non-throwing API, implement an alternative return shape ({ valid:
boolean, error?: string }) consistently across usages.
packages/extension/src/libs/spark-handler/createTempTx.ts (1)

28-28: ⚡ Quick win

Type addressKeyPairs more precisely.

Record<string, any> loses type information. Consider defining an interface for the keypair structure.

♻️ Proposed improvement
+  addressKeyPairs: Record<string, ECPairInterface>;
-  addressKeyPairs: Record<string, any>;

This provides better type safety since you're already importing ECPairInterface and using it for spendableUtxos[].keyPair.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/libs/spark-handler/createTempTx.ts` at line 28, The
type declaration addressKeyPairs: Record<string, any> is too loose — define a
precise type/interface (e.g., AddressKeyPair or AddressKeyPairsMap) that models
the stored keypair structure and use ECPairInterface for the keyPair property so
TypeScript can validate usages like spendableUtxos[].keyPair; update the
addressKeyPairs declaration to use this new interface and adjust any related
references (e.g., where addressKeyPairs[...] is accessed) to match the stricter
shape.
packages/extension/src/libs/spark-handler/createTempFromSparkTx.ts (1)

36-40: 💤 Low value

Consider BigNumber overflow when converting to satoshis.

Line 38 uses .toNumber() after multiplying by 10^8. JavaScript's Number type has a safe integer range of ±2^53, which corresponds to approximately 90 million BTC. While this is unlikely to be exceeded in practice, if amount is extremely large or malformed, .toNumber() could produce Infinity or lose precision, leading to incorrect transaction outputs.

Consider adding a bounds check or documenting the expected range.

♻️ Optional bounds check
   if (!isSparkTransaction) {
+    const satoshis = new BigNumber(amount).multipliedBy(10 ** 8);
+    if (!satoshis.isFinite() || satoshis.gt(Number.MAX_SAFE_INTEGER)) {
+      throw new Error('Amount too large for transaction output');
+    }
     tempTx.addOutput(
       bitcoin.address.toOutputScript(to, network),
-      new BigNumber(amount).multipliedBy(10 ** 8).toNumber(),
+      satoshis.toNumber(),
     );
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/libs/spark-handler/createTempFromSparkTx.ts` around
lines 36 - 40, The code converts amount to satoshis with new
BigNumber(amount).multipliedBy(10 ** 8).toNumber() which can overflow or lose
precision; before calling tempTx.addOutput (and symbols
bitcoin.address.toOutputScript, tempTx.addOutput, BigNumber, amount), validate
and sanitize amount: compute satoshis = new
BigNumber(amount).multipliedBy(1e8).integerValue(BigNumber.ROUND_FLOOR), check
satoshis.isFinite() and sats.lte(Number.MAX_SAFE_INTEGER) and sats.gte(0) (or
throw a descriptive Error if out of bounds/malformed), then pass
sats.toNumber(); alternatively document allowed range in the function comment if
you prefer not to throw. Ensure you fail fast with a clear error message when
amount is invalid to avoid pushing Infinity/incorrect values into
tempTx.addOutput.
packages/extension/src/libs/utils/wasm-worker-loader.ts (1)

46-67: ⚡ Quick win

Deduplicate in-flight WASM initialization.

Line 50 preload and Line 63 fallback can race and initialize twice; cache a shared loading promise.

Refactor sketch
 class WasmWorkerInstance {
   instance: WasmModule | null = null;
+  private loadingPromise: Promise<WasmModule> | null = null;
 
   constructor() {
-    loadWasm()
+    this.loadingPromise = loadWasm()
       .then(module => {
         this.instance = module;
+        return module;
       })
       .catch(error => {
         console.error('Error loading WASM module:', error);
+        this.loadingPromise = null;
+        throw error;
       });
   }
 
   public async getInstance(): Promise<WasmModule> {
-    if (this.instance) {
-      return Promise.resolve(this.instance);
-    } else {
-      const wasm = await loadWasm();
-      this.instance = wasm;
-      return Promise.resolve(wasm);
-    }
+    if (this.instance) return this.instance;
+    if (!this.loadingPromise) {
+      this.loadingPromise = loadWasm().then(module => {
+        this.instance = module;
+        return module;
+      });
+    }
+    return this.loadingPromise;
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/libs/utils/wasm-worker-loader.ts` around lines 46 -
67, The constructor and getInstance of WasmWorkerInstance can race and call
loadWasm twice; add a shared loading promise (e.g., a private property
loadingPromise: Promise<WasmModule> | null) and assign it when starting loadWasm
in the constructor, await that same promise in getInstance instead of calling
loadWasm again, and on resolution set this.instance and clear loadingPromise on
error set it to null while propagating/logging the error; update references in
constructor and getInstance to use loadingPromise and avoid duplicate
initialization.
packages/extension/src/libs/utils/updateAndSync/handleCoinSetUpdates.ts (1)

27-42: ⚖️ Poor tradeoff

Consider more robust coin deduplication strategy.

Using JSON.stringify for deduplication (Lines 31, 35) is fragile because:

  1. JavaScript object property ordering is not guaranteed in all cases
  2. Memory-intensive for large coin arrays (creates string copies)
  3. Two semantically identical coins with different key orders would not be detected as duplicates
Alternative approach using stable coin identifiers

If coins have a unique identifier (e.g., txid + vout), use that directly:

-const mergeCoins = (existing: string[][], incoming: string[][]): string[][] => {
+const getCoinKey = (coin: string[]): string => {
+  // Adjust based on actual coin structure, e.g., coin[0] + coin[1]
+  return `${coin[0]}-${coin[1]}`;
+};
+
+const mergeCoins = (existing: string[][], incoming: string[][]): string[][] => {
   if (!existing?.length) return [...incoming];
   if (!incoming?.length) return [...existing];
 
-  const seen = new Set(existing.map(coin => JSON.stringify(coin)));
+  const seen = new Set(existing.map(getCoinKey));
   const updates: string[][] = [];
 
   incoming.forEach(coin => {
-    const key = JSON.stringify(coin);
+    const key = getCoinKey(coin);
     if (seen.has(key)) return;
     seen.add(key);
     updates.push(coin);
   });
 
   return [...updates, ...existing];
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/libs/utils/updateAndSync/handleCoinSetUpdates.ts`
around lines 27 - 42, The mergeCoins function currently deduplicates by
JSON.stringify which is brittle and memory-heavy; change mergeCoins to compute a
stable, lightweight key for each coin (e.g., if coin elements represent txid and
vout use `${txid}:${vout}` or build a deterministic key by ordering/normalizing
coin fields and joining with a fixed separator) and use a Set or Map of those
keys for deduplication instead of JSON.stringify; update references in
mergeCoins to generate the key (e.g., getCoinKey(coin)) when seeding seen and
when checking incoming coins so semantically identical coins are recognized
without expensive stringifying of whole arrays.

Comment on lines +262 to +267
privateRecipientsVector = Module.ccall(
'js_createPrivateRecipientsVectorForCreateSparkSpendTransaction',
'number',
['number'],
[1], // intended final size
);
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 | 🔴 Critical | ⚡ Quick win

Spark-to-Spark sends are missing a change recipient.

For private sends, the vector is always sized to 1 and Line 311 uses requestedAmount as the only private output. When coin selection overshoots the target—which the greedy selector will often do—there is no second private recipient returning change to the sender, so the extra value has nowhere explicit to go.

Also applies to: 304-332

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/libs/spark-handler/index.ts` around lines 262 - 267,
The private send code always creates privateRecipientsVector with size 1 (using
js_createPrivateRecipientsVectorForCreateSparkSpendTransaction) and then only
pushes the requestedAmount as the sole private output (around the logic that
references requestedAmount), which drops any change when coin selection
overshoots; update the logic to size the privateRecipientsVector to accommodate
a potential change recipient (e.g., size 2 or size = 1 + (changePossible ? 1 :
0)), and when the coin selector produces change, append a second private
recipient representing the sender's change (address = sender, amount = change)
before calling the native create function; ensure you only add the change
recipient when change > 0 and adjust any loops or indices that assume a single
recipient (symbols: privateRecipientsVector,
js_createPrivateRecipientsVectorForCreateSparkSpendTransaction, and the code
that inserts requestedAmount at index 0).

if (!supportedNetworks.includes(networkName as NetworkNames))
return Promise.resolve(utxos);
return getAllOrdinals(address, networkName, []).then(ordinals => {
console.log({ ordinals });
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 | 🔴 Critical | ⚡ Quick win

Remove debug console.log from production code.

This debug statement will expose ordinal data to the browser console in production builds, potentially leaking user privacy information.

🐛 Proposed fix
   return getAllOrdinals(address, networkName, []).then(ordinals => {
-    console.log({ ordinals });
-
     return utxos.filter(utxo => {
📝 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({ ordinals });
return getAllOrdinals(address, networkName, []).then(ordinals => {
return utxos.filter(utxo => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/providers/bitcoin/libs/filter-ordinals.ts` at line 49,
Remove the debug console.log that prints ordinals to the browser console: locate
the console.log({ ordinals }) in filter-ordinals.ts (the place where the
ordinals variable is handled) and delete it or replace it with a secure logging
call (e.g., a debug-only logger gated by NODE_ENV or a telemetry-safe logger
that redacts sensitive ordinal data) so no raw ordinal data is emitted in
production.

Comment on lines +34 to +51
private initDB(): void {
const request = indexedDB.open(this.dbName, this.dbVersion);

request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName);
}
};

request.onsuccess = () => {
this.db = request.result;
};

request.onerror = () => {
console.error('Error opening IndexedDB', request.error);
};
}
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 | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine the file in question
find . -type f -name "indexedDB.ts" | head -20

Repository: enkryptcom/enKrypt

Length of output: 113


🏁 Script executed:

# Check the repository structure to understand the context
git ls-files | grep -E "(indexedDB|db)" | head -20

Repository: enkryptcom/enKrypt

Length of output: 254


🏁 Script executed:

# Read the indexedDB.ts file to examine the code
cat -n packages/extension/src/ui/action/db/indexedDB.ts

Repository: enkryptcom/enKrypt

Length of output: 5662


🏁 Script executed:

# Check for test files or CI configuration that might reference this error
find . -type f \( -name "*.test.*" -o -name "*.spec.*" -o -name ".github" -o -name "ci.yml" -o -name "github-ci*" \) | grep -E "(test|spec|ci|github)" | head -20

Repository: enkryptcom/enKrypt

Length of output: 1135


🏁 Script executed:

# Search for any usage of IndexedDBHelper to understand impact
rg "IndexedDBHelper" --type ts --type js -l

Repository: enkryptcom/enKrypt

Length of output: 828


🏁 Script executed:

# Check how IndexedDBHelper is instantiated/used in one of the main files
head -100 packages/extension/src/ui/action/db/indexedDB.ts

Repository: enkryptcom/enKrypt

Length of output: 2800


🏁 Script executed:

# Look at sparkCoinInfoWorker to see if it runs in a browser context
head -50 packages/extension/src/ui/action/workers/sparkCoinInfoWorker.ts

Repository: enkryptcom/enKrypt

Length of output: 1287


Guard IndexedDB initialization for non-browser environments.

Line 35 assumes indexedDB exists. This breaks immediately when the module loads in non-browser contexts (Node.js tests, CI), since the constructor calls initDB() synchronously and indexedDB is undefined outside the browser.

Proposed fix
   private initDB(): void {
+    if (typeof indexedDB === 'undefined') {
+      console.warn('IndexedDB is unavailable in this environment');
+      return;
+    }
     const request = indexedDB.open(this.dbName, this.dbVersion);
🧰 Tools
🪛 GitHub Actions: Test all workspaces / 0_test.txt

[error] 35-35: Test run failed with ReferenceError: indexedDB is not defined (indexedDB.open(...)).

🪛 GitHub Actions: Test all workspaces / test

[error] 35-35: ReferenceError: indexedDB is not defined at IndexedDBHelper.initDB (indexedDB.open(...)).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/extension/src/ui/action/db/indexedDB.ts` around lines 34 - 51, The
initDB method assumes indexedDB exists and is called synchronously from the
constructor, which fails in non-browser environments; modify initDB (and the
code that invokes it) to first check for the presence of globalThis.indexedDB
(e.g., if (typeof globalThis.indexedDB === 'undefined') return;) and bail out
early if missing, so no indexedDB.open is invoked; update any constructor path
that calls initDB to still work when initDB returns early (ensure this.db
remains null/undefined and consumers handle that case).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants