Skip to content

Complete Ledger KYC Upload#54

Open
Satishpethani92 wants to merge 6 commits into
XinFinOrg:mainfrom
Satishpethani92:Check-KYC
Open

Complete Ledger KYC Upload#54
Satishpethani92 wants to merge 6 commits into
XinFinOrg:mainfrom
Satishpethani92:Check-KYC

Conversation

@Satishpethani92
Copy link
Copy Markdown
Contributor

@Satishpethani92 Satishpethani92 commented May 22, 2026

Summary by CodeRabbit

  • New Features

    • Improved Ledger/Trezor provider integration and hardware-wallet RPC bridging.
    • New signature-recovery helper for better personal-sign handling.
  • Bug Fixes

    • More reliable KYC submission with fallback signature recovery and longer expiry tolerance.
    • Improved transaction signing/submission, receipt confirmation, and address normalization.
    • Better user-facing wallet error formatting.
  • Refactor

    • Centralized wallet/error handling and streamlined signing flows.
  • Chores

    • Updated build assets, ignored build output, and local CORS config.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 63da1e81-e8ce-4f2a-9dae-e5ddeba9d446

📥 Commits

Reviewing files that changed from the base of the PR and between 886b7be and dfbcf02.

📒 Files selected for processing (1)
  • helpers/ledgerWeb3Provider.js

📝 Walkthrough

Walkthrough

Adds Ledger hardware-wallet provider and helpers, centralized wallet error normalization, robust personal-sign recovery, expanded KYC /addKYC verification, and client wiring so KYC upload and transactions can be signed and sent via hardware-wallet or standard flows with consistent receipt and error handling.

Changes

Wallet Integration & KYC Flow Refactor

Layer / File(s) Summary
Error Handling & Personal-sign helpers
helpers/walletError.js, helpers/personalSign.js
Adds formatWalletError() / toWalletError() and recoverPersonalSignAddress() plus address normalization for robust signer recovery and unified wallet error messages.
Ledger Web3 Provider
helpers/ledgerWeb3Provider.js
New createLedgerWeb3Provider() and toRpcAddress() providing RPC interception, address normalization (XDC/0x), personal-sign decoding, nonce fetch, prepare/sign hooks, and sendSignedTransaction delegation.
Server-side KYC Recovery
apis/ipfs.js
Refactors /addKYC to prefer recoverPersonalSignAddress(), adds normalizeValue() array handling, addressesMatch() comparisons, always includes reason in 401, extends expiry window to 15 minutes, and enhances missing-field warnings.
App Ledger & Transaction Infrastructure
app/app.js
Imports Ledger provider and wallet-error helpers; adds Ledger path derivation, tx normalization/serialization, signature verification before broadcast, RPC receipt polling/timeouts, MetaMask eth_requestAccounts handling, Ledger provider wiring via createLedgerWeb3Provider(), and global error logging using formatWalletError.
Settings: HD Path & Account Setup
app/components/Setting.vue
Initializes HTTP Web3 for Ledger/Trezor HD selection, persists HD derivation/network/address for hardware wallets, derives rpcAccount via toRpcAddress(), and uses formatWalletError() for toast details.
Apply: KYC Upload & On-chain Flow
app/components/candidates/Apply.vue
Adds kycLoading, signs KYC messages via hardware or web3 paths, posts account/message/signedMessage as params to /api/ipfs/addKYC, and routes on-chain submission via sendHardwareWalletTransaction() or sendContractTransaction() with improved error formatting.
Build & Config Edits
build/index.html, config/local.json, .gitignore, apis/candidates.js
Updates hashed asset script urls, adds https://localhost:5000 to CORS allowed origins, ignores build/, and comments out unused axios require in apis/candidates.js.

Sequence Diagrams

sequenceDiagram
  participant App
  participant LedgerProvider as Ledger Web3 Provider
  participant Hooks as Signing Hooks
  participant RPC as Underlying RPC
  App->>LedgerProvider: eth_sendTransaction
  LedgerProvider->>RPC: eth_getTransactionCount
  RPC-->>LedgerProvider: nonce
  LedgerProvider->>Hooks: prepareTransaction
  Hooks-->>LedgerProvider: normalized tx
  LedgerProvider->>Hooks: signTransaction
  Hooks-->>LedgerProvider: signed tx
  LedgerProvider->>Hooks: sendSignedTransaction
  Hooks->>RPC: eth_sendRawTransaction
  RPC-->>LedgerProvider: txHash
  LedgerProvider-->>App: JSON-RPC response
Loading
sequenceDiagram
  participant Component
  participant AppVM as Vue App Instance
  participant RPC as RPC Provider
  Component->>AppVM: sendSignedTransaction(signedTx)
  AppVM->>AppVM: verify signature & recover sender
  AppVM->>RPC: eth_sendRawTransaction
  RPC-->>AppVM: txHash
  AppVM->>RPC: poll eth_getTransactionReceipt
  alt Receipt found
    RPC-->>AppVM: receipt
    AppVM-->>Component: resolve(receipt)
  else Timeout
    AppVM-->>Component: reject(timeout error)
  end
Loading
sequenceDiagram
  participant User as User/Component
  participant App as Vue App
  participant IPFS as /api/ipfs/addKYC
  participant Contract as Smart Contract
  User->>App: uploadKYC()
  App->>App: signMessage (hardware or web3)
  App->>IPFS: POST {account, message, signature}
  IPFS-->>App: {hash, txData}
  alt Hardware Wallet
    App->>App: sendHardwareWalletTransaction
  else Standard Wallet
    App->>App: sendContractTransaction
  end
  App->>Contract: uploadKYC(hash)
  Contract-->>App: receipt
  App->>User: success toast
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • AnilChinchawale
  • RushabhParmar12
  • rajshah1609

"I hop through code with tiny paws,
Ledger paths and sigs I applause.
Errors wrapped and signatures neat,
KYC signed and transactions fleet,
A rabbit cheers each hardware cause."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Complete Ledger KYC Upload' directly summarizes the main change: comprehensive updates to Ledger hardware wallet support for KYC submission across multiple files (ledgerWeb3Provider, personalSign, walletError helpers, and KYC UI components).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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: 6

Caution

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

⚠️ Outside diff range comments (2)
app/app.js (2)

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

Wrap case 'ledger' block in braces to fix scope leakage.

The const/let declarations on lines 1145-1147 are accessible from other switch cases, which can cause unexpected behavior. This is flagged by the static analyzer.

Proposed fix
         case 'ledger':
+        {
             await ensureLedgerEth(path)
             const signature = await Vue.prototype.appEth.signPersonalMessage(
                 path,
                 Buffer.from(message).toString('hex')
             )
             const r = toHexBuffer(signature.r)
             const s = toHexBuffer(signature.s)
             let v = signature.v
             if (typeof v === 'string') {
                 v = parseInt(v, 16)
             }
             if (v < 27) {
                 v += 27
             }
             result = ethUtils.toRpcSig(v, r, s)
             break
+        }
🤖 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 `@app/app.js` around lines 1138 - 1155, The switch case for 'ledger' leaks
block-scoped variables (signature, r, s, v) because they are declared with
const/let without an enclosing block; wrap the entire case 'ledger' body in
braces (i.e., case 'ledger': { ... break } ) so ensureLedgerEth,
Vue.prototype.appEth.signPersonalMessage, toHexBuffer and ethUtils.toRpcSig
remain scoped to that case and do not bleed into other cases.

1156-1162: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Operator precedence bug: '0x' + sig.payload.signature || '' never falls back to empty string.

The expression evaluates as ('0x' + sig.payload.signature) || ''. If signature is undefined, the result is '0xundefined' rather than ''.

Proposed fix
         case 'trezor':
+        {
             const sig = await TrezorConnect.ethereumSignMessage({
                 path,
                 message
             })
-            result = '0x' + sig.payload.signature || ''
+            result = sig.payload && sig.payload.signature
+                ? '0x' + sig.payload.signature
+                : ''
             break
+        }
🤖 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 `@app/app.js` around lines 1156 - 1162, The current assignment to result uses
operator precedence incorrectly so '0x' + sig.payload.signature || '' yields
'0xundefined' if signature is undefined; update the trezor branch (the sig
variable returned by TrezorConnect.ethereumSignMessage and the result
assignment) to explicitly check for a valid signature before prefixing '0x' —
e.g., test sig && sig.payload && sig.payload.signature (or use a ternary) and
only then set result = '0x' + sig.payload.signature, otherwise set result = ''.
🧹 Nitpick comments (7)
config/local.json (1)

9-11: 💤 Low value

Consider formatting the CORS array for better readability.

The CORS origins array is on a single line. For maintainability, consider formatting it with one origin per line:

📝 Proposed formatting improvement
     "cors": [
-        "https://swagger.apothem.network","https://localhost:5000"
+        "https://swagger.apothem.network",
+        "https://localhost:5000"
     ],
🤖 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 `@config/local.json` around lines 9 - 11, The CORS origins array under the
"cors" key in config/local.json is currently on a single line; reformat it so
each origin is on its own line for readability and easier diffs—locate the
"cors" JSON array and split entries onto separate lines with proper JSON commas
and indentation (one origin per line inside the array) while preserving the
existing origin strings and valid JSON syntax.
helpers/ledgerWeb3Provider.js (1)

145-148: 💤 Low value

Callback signature missing second argument on error.

Standard Node.js callback convention is callback(err, result). Passing only the error without the second argument may cause issues with consumers that destructure both parameters.

Proposed fix
         run()
             .then((response) => callback(null, response))
-            .catch((error) => callback(toWalletError(error)))
+            .catch((error) => callback(toWalletError(error), 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 `@helpers/ledgerWeb3Provider.js` around lines 145 - 148, The promise catch
currently calls callback(toWalletError(error)) which violates Node.js (err,
result) convention; update the catch handler in the run().then(...).catch(...)
chain to call callback(toWalletError(error), null) (or undefined) so the second
argument is explicitly provided, referencing the existing run() promise chain,
the callback invocation, and the toWalletError conversion to locate the change.
app/components/candidates/Apply.vue (3)

592-604: ⚡ Quick win

Fallback from personal.sign to eth.sign may produce incompatible signatures.

personal.sign prefixes the message with "\x19Ethereum Signed Message:\n" while eth.sign may not. The server's recovery logic should handle this, but swallowing the first error without logging makes debugging difficult.

Proposed fix: Log the initial error
                     } else {
                         try {
                             signedMessage = await this.web3.eth.personal.sign(
                                 message,
                                 signerAccount,
                                 ''
                             )
                         } catch (e) {
+                            console.warn('personal.sign failed, falling back to eth.sign', e.message)
                             signedMessage = await this.web3.eth.sign(message, signerAccount)
                         }
                     }
🤖 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 `@app/components/candidates/Apply.vue` around lines 592 - 604, The fallback
from this.web3.eth.personal.sign to this.web3.eth.sign can produce incompatible
signatures and the original error is swallowed; in the catch block around the
personal.sign call in Apply.vue, log the caught error (e) with context (e.g.,
"personal.sign failed", signerAccount, message) before attempting the eth.sign
fallback so debugging shows why personal.sign failed and what was attempted.

606-621: 💤 Low value

Triple transmission of auth fields is redundant.

The same account, message, and signedMessage are sent via FormData body (lines 607-609), query params (lines 611-615), and custom headers (lines 616-620). The server accepts any of these sources. Pick one authoritative source to reduce payload size and potential for mismatched values.

🤖 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 `@app/components/candidates/Apply.vue` around lines 606 - 621, The code
currently sends account/message/signedMessage three times (in formData via
formData.append, as query params in axios.post params, and as custom headers),
which is redundant and risky; pick one authoritative transport (recommend
keeping them in headers like 'x-kyc-account'/'x-kyc-message'/'x-kyc-signature'
and remove the duplicate formData.append calls for account/message/signedMessage
and the params object) so only the file is sent in formData (this.KYC.file) and
authentication fields are sent once with axios.post('/api/ipfs/addKYC',
formData, { headers: { ... } }); ensure any server-side handler still reads the
chosen source (headers) and update any client-side references to
signerAccount/message/signedMessage accordingly.

626-630: 💤 Low value

Hardcoded gas value differs from other transactions.

Other transactions in this file use self.chainConfig.gas (line 395-396), but here 3000000 is hardcoded. For consistency and configurability, consider using the chain config 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 `@app/components/candidates/Apply.vue` around lines 626 - 630, The txOptions in
Apply.vue currently hardcodes gas as 3000000 (in the txOptions object where
gasPrice is set via this.web3.utils.toHex(gasPrice) and gasLimit similarly),
causing inconsistency with other transactions; replace the hardcoded 3000000
values for gas and gasLimit with the configured value (self.chainConfig.gas) and
convert it to hex using this.web3.utils.toHex(self.chainConfig.gas) so it
matches the other usages (see where self.chainConfig.gas is used around lines
~395).
apis/ipfs.js (2)

90-107: ⚡ Quick win

Silent error swallowing hinders debugging.

All catch (e) {} blocks discard exception details. Consider adding structured debug logging to aid troubleshooting signature recovery failures in production.

Proposed fix
     // 2. Recover Signer (Ledger / hardware wallets may use alternate v bytes)
     let recovered = await recoverPersonalSignAddress(message, signedMessage)
     if (!recovered) {
         const candidateMessages = [message]
         try {
             candidateMessages.push(web3.utils.utf8ToHex(message))
-        } catch (e) {}
+        } catch (e) {
+            console.debug('utf8ToHex encoding failed', e.message)
+        }
         try {
             candidateMessages.push(web3.utils.sha3(message))
-        } catch (e) {}
+        } catch (e) {
+            console.debug('sha3 encoding failed', e.message)
+        }

         for (const candidateMessage of candidateMessages) {
             try {
                 recovered = (await web3.eth.accounts.recover(candidateMessage, signedMessage) || '').toLowerCase()
                 if (recovered) break
-            } catch (e) {}
+            } catch (e) {
+                console.debug('recover attempt failed', e.message)
+            }
         }
     }
🤖 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 `@apis/ipfs.js` around lines 90 - 107, The empty catch blocks in the recovery
logic (around recoverPersonalSignAddress, web3.utils.utf8ToHex, web3.utils.sha3,
and web3.eth.accounts.recover) swallow errors; update each catch to log
structured debug info including the operation and the failing candidateMessage
(but do not log raw signedMessage) using the project's logger (e.g.,
processLogger.error) with the caught error object as metadata, and fall back to
console.error if the logger is unavailable so signature recovery failures are
visible in logs for diagnostics.

83-88: 💤 Low value

15-minute signature window is generous for replay protection.

The expiry increased from 5 to 15 minutes. While this accommodates hardware wallet signing delays, it widens the replay attack window. Ensure this tradeoff is acceptable for your threat model.

🤖 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 `@apis/ipfs.js` around lines 83 - 88, The signature expiry window
(fifteenMinutes) used in the addKYC check is too large for replay protection;
change it to a safer default and make it configurable: replace the hard-coded
fifteenMinutes = 15 * 60 * 1000 with a configurable value (e.g.,
process.env.SIGNATURE_WINDOW_MS parsed to integer with a default of 5 * 60 *
1000), keep the existing isNaN(signedTime) / Math.abs(currentTime - signedTime)
check but reference the new variable (e.g., signatureWindowMs) so operators can
increase only if they accept the replay risk; optionally add a short log note
when a non-default value is used.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@apis/ipfs.js`:
- Around line 23-29: In normalizeValue the Array.isArray branch uses value[0]
which becomes undefined for empty arrays and yields the string "undefined";
change the array handling in normalizeValue (function normalizeValue) to check
for an empty array and return '' (or set value = ''), otherwise assign value =
value[0]; also ensure that after selecting the first element you treat
null/undefined by returning '' before calling String(...).trim().replace(...) so
empty arrays and null/undefined first elements do not become the string
"undefined".

In `@app/components/candidates/Apply.vue`:
- Around line 570-585: The account resolution order is inconsistent between
hardware and standard wallets (in Apply.vue) causing different accounts to be
chosen; standardize the lookup order used to set signerAccount so both branches
call getAccount() in the same relative position. Update the logic around
NetworkProvider/provider/store.get('network') and the signerAccount assignment
(the branches that call this.toRpcAddress and await this.getAccount()) to use a
single, consistent resolution sequence (e.g., await this.getAccount() ||
this.account || store.get('address') || '') for both hardware
('ledger'|'trezor') and non-hardware paths so toRpcAddress and toLowerCase
operate on the same resolved value.
- Around line 631-648: The code force-sets this.KYC.status = true after calling
getKYCStatus, which can mask failed transactions; instead, use the actual
on-chain result or transaction receipt to update status: after calling
sendHardwareWalletTransaction or sendContractTransaction capture the transaction
receipt or error, call getKYCStatus(this.account) and use its returned value to
set this.KYC.status (do not unconditionally assign true), and if receipts are
unavailable consider marking a pending state and optionally polling getKYCStatus
until it becomes true; update references in sendHardwareWalletTransaction,
sendContractTransaction and getKYCStatus to surface the receipt/result so the UI
reflects real on-chain state.

In `@helpers/ledgerWeb3Provider.js`:
- Line 158: The file mixes CommonJS requires with an ES module export; replace
the ES6 export with a CommonJS export so exports are consistent with the rest of
the helpers: export the createLedgerWeb3Provider and toRpcAddress symbols via
module.exports (e.g., set module.exports = { createLedgerWeb3Provider,
toRpcAddress }) and remove the `export { ... }` statement so the module uses
only CommonJS style.

In `@helpers/personalSign.js`:
- Around line 74-87: The ecrecover call in recoverPersonalSignAddress is passing
recoveryId (0/1) as v but ethereumjs-util expects legacy v (27/28), so change
the code to pass recoveryId + 27 (ensure it's a Number) when calling
ethUtil.ecrecover(msgHash, v, r, s); locate the loop using recoveryId, msgHash,
sigBuf and replace the v argument with (Number(recoveryId) + 27) and keep r as
sigBuf.slice(0,32) and s as sigBuf.slice(32,64), preserving the try/catch and
return behavior.

In `@helpers/walletError.js`:
- Around line 62-68: toWalletError currently mutates the original Error.message
which can double-format or lose context; instead, create and return a new Error
instance that uses formatWalletError(error) for its message while preserving
relevant properties (stack, name, and any custom fields) from the original
error; update the toWalletError function to construct a cloned Error (copy
stack/name/custom props) and set the formatted message rather than modifying the
passed-in Error; reference formatWalletError and toWalletError when making the
change.

---

Outside diff comments:
In `@app/app.js`:
- Around line 1138-1155: The switch case for 'ledger' leaks block-scoped
variables (signature, r, s, v) because they are declared with const/let without
an enclosing block; wrap the entire case 'ledger' body in braces (i.e., case
'ledger': { ... break } ) so ensureLedgerEth,
Vue.prototype.appEth.signPersonalMessage, toHexBuffer and ethUtils.toRpcSig
remain scoped to that case and do not bleed into other cases.
- Around line 1156-1162: The current assignment to result uses operator
precedence incorrectly so '0x' + sig.payload.signature || '' yields
'0xundefined' if signature is undefined; update the trezor branch (the sig
variable returned by TrezorConnect.ethereumSignMessage and the result
assignment) to explicitly check for a valid signature before prefixing '0x' —
e.g., test sig && sig.payload && sig.payload.signature (or use a ternary) and
only then set result = '0x' + sig.payload.signature, otherwise set result = ''.

---

Nitpick comments:
In `@apis/ipfs.js`:
- Around line 90-107: The empty catch blocks in the recovery logic (around
recoverPersonalSignAddress, web3.utils.utf8ToHex, web3.utils.sha3, and
web3.eth.accounts.recover) swallow errors; update each catch to log structured
debug info including the operation and the failing candidateMessage (but do not
log raw signedMessage) using the project's logger (e.g., processLogger.error)
with the caught error object as metadata, and fall back to console.error if the
logger is unavailable so signature recovery failures are visible in logs for
diagnostics.
- Around line 83-88: The signature expiry window (fifteenMinutes) used in the
addKYC check is too large for replay protection; change it to a safer default
and make it configurable: replace the hard-coded fifteenMinutes = 15 * 60 * 1000
with a configurable value (e.g., process.env.SIGNATURE_WINDOW_MS parsed to
integer with a default of 5 * 60 * 1000), keep the existing isNaN(signedTime) /
Math.abs(currentTime - signedTime) check but reference the new variable (e.g.,
signatureWindowMs) so operators can increase only if they accept the replay
risk; optionally add a short log note when a non-default value is used.

In `@app/components/candidates/Apply.vue`:
- Around line 592-604: The fallback from this.web3.eth.personal.sign to
this.web3.eth.sign can produce incompatible signatures and the original error is
swallowed; in the catch block around the personal.sign call in Apply.vue, log
the caught error (e) with context (e.g., "personal.sign failed", signerAccount,
message) before attempting the eth.sign fallback so debugging shows why
personal.sign failed and what was attempted.
- Around line 606-621: The code currently sends account/message/signedMessage
three times (in formData via formData.append, as query params in axios.post
params, and as custom headers), which is redundant and risky; pick one
authoritative transport (recommend keeping them in headers like
'x-kyc-account'/'x-kyc-message'/'x-kyc-signature' and remove the duplicate
formData.append calls for account/message/signedMessage and the params object)
so only the file is sent in formData (this.KYC.file) and authentication fields
are sent once with axios.post('/api/ipfs/addKYC', formData, { headers: { ... }
}); ensure any server-side handler still reads the chosen source (headers) and
update any client-side references to signerAccount/message/signedMessage
accordingly.
- Around line 626-630: The txOptions in Apply.vue currently hardcodes gas as
3000000 (in the txOptions object where gasPrice is set via
this.web3.utils.toHex(gasPrice) and gasLimit similarly), causing inconsistency
with other transactions; replace the hardcoded 3000000 values for gas and
gasLimit with the configured value (self.chainConfig.gas) and convert it to hex
using this.web3.utils.toHex(self.chainConfig.gas) so it matches the other usages
(see where self.chainConfig.gas is used around lines ~395).

In `@config/local.json`:
- Around line 9-11: The CORS origins array under the "cors" key in
config/local.json is currently on a single line; reformat it so each origin is
on its own line for readability and easier diffs—locate the "cors" JSON array
and split entries onto separate lines with proper JSON commas and indentation
(one origin per line inside the array) while preserving the existing origin
strings and valid JSON syntax.

In `@helpers/ledgerWeb3Provider.js`:
- Around line 145-148: The promise catch currently calls
callback(toWalletError(error)) which violates Node.js (err, result) convention;
update the catch handler in the run().then(...).catch(...) chain to call
callback(toWalletError(error), null) (or undefined) so the second argument is
explicitly provided, referencing the existing run() promise chain, the callback
invocation, and the toWalletError conversion to locate the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 416d03e7-9c79-4fc9-b47b-732aed1e9c2c

📥 Commits

Reviewing files that changed from the base of the PR and between 89c856f and adf01bc.

📒 Files selected for processing (21)
  • apis/candidates.js
  • apis/ipfs.js
  • app/app.js
  • app/components/Setting.vue
  • app/components/candidates/Apply.vue
  • build/app.0cda748de7cad1273504.js
  • build/app.d6ee356cd701e09ed0fb.js
  • build/index.html
  • build/node-vendor.27b07d58b842f5b9d966.js
  • build/node-vendor.27b07d58b842f5b9d966.js.LICENSE.txt
  • build/node-vendor.d4d835193df6f3eb9196.js
  • build/runtime.04647efee1a41c5e77ac.js
  • build/runtime.f6886b9416edacf69bd3.js
  • build/style.67d0ad01ba802b0f47f8.js
  • build/style.9b2a3757cae03d13690f.js
  • build/vendor.a4b84a6e0d4780dac5e3.js
  • build/vendor.d9e3b7255f6258bc8ca1.js
  • config/local.json
  • helpers/ledgerWeb3Provider.js
  • helpers/personalSign.js
  • helpers/walletError.js
💤 Files with no reviewable changes (2)
  • build/runtime.04647efee1a41c5e77ac.js
  • build/vendor.d9e3b7255f6258bc8ca1.js

Comment thread apis/ipfs.js
Comment on lines 23 to 29
function normalizeValue (value) {
if (value === undefined || value === null) return ''
if (Array.isArray(value)) {
value = value[0]
}
return String(value).trim().replace(/^["']|["']$/g, '')
}
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 | ⚡ Quick win

Empty array case returns "undefined" string.

When value is an empty array, value[0] is undefined, and String(undefined) produces the literal string "undefined" which will pass through normalization.

Proposed fix
 function normalizeValue (value) {
     if (value === undefined || value === null) return ''
     if (Array.isArray(value)) {
+        if (value.length === 0) return ''
         value = value[0]
     }
+    if (value === undefined || value === null) return ''
     return String(value).trim().replace(/^["']|["']$/g, '')
 }
📝 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
function normalizeValue (value) {
if (value === undefined || value === null) return ''
if (Array.isArray(value)) {
value = value[0]
}
return String(value).trim().replace(/^["']|["']$/g, '')
}
function normalizeValue (value) {
if (value === undefined || value === null) return ''
if (Array.isArray(value)) {
if (value.length === 0) return ''
value = value[0]
}
if (value === undefined || value === null) return ''
return String(value).trim().replace(/^["']|["']$/g, '')
}
🤖 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 `@apis/ipfs.js` around lines 23 - 29, In normalizeValue the Array.isArray
branch uses value[0] which becomes undefined for empty arrays and yields the
string "undefined"; change the array handling in normalizeValue (function
normalizeValue) to check for an empty array and return '' (or set value = ''),
otherwise assign value = value[0]; also ensure that after selecting the first
element you treat null/undefined by returning '' before calling
String(...).trim().replace(...) so empty arrays and null/undefined first
elements do not become the string "undefined".

Comment on lines +570 to +585
const network = this.NetworkProvider ||
this.provider ||
store.get('network')
let signerAccount
if (network === 'ledger' || network === 'trezor') {
signerAccount = this.toRpcAddress(
await this.getAccount() ||
this.account ||
store.get('address') ||
''
).toLowerCase()
} else {
signerAccount = this.toRpcAddress(
this.account || store.get('address') || (await this.getAccount()) || ''
).toLowerCase()
}
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 | ⚡ Quick win

Inconsistent account resolution order between hardware and standard wallets.

For hardware wallets, getAccount() is called first before falling back to this.account. For standard wallets, the order is reversed. This asymmetry could lead to different accounts being used for the same user session.

Proposed fix: Use consistent resolution order
                     const network = this.NetworkProvider ||
                         this.provider ||
                         store.get('network')
                     let signerAccount
-                    if (network === 'ledger' || network === 'trezor') {
-                        signerAccount = this.toRpcAddress(
-                            await this.getAccount() ||
-                            this.account ||
-                            store.get('address') ||
-                            ''
-                        ).toLowerCase()
-                    } else {
-                        signerAccount = this.toRpcAddress(
-                            this.account || store.get('address') || (await this.getAccount()) || ''
-                        ).toLowerCase()
-                    }
+                    signerAccount = this.toRpcAddress(
+                        this.account ||
+                        store.get('address') ||
+                        (await this.getAccount()) ||
+                        ''
+                    ).toLowerCase()
📝 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 network = this.NetworkProvider ||
this.provider ||
store.get('network')
let signerAccount
if (network === 'ledger' || network === 'trezor') {
signerAccount = this.toRpcAddress(
await this.getAccount() ||
this.account ||
store.get('address') ||
''
).toLowerCase()
} else {
signerAccount = this.toRpcAddress(
this.account || store.get('address') || (await this.getAccount()) || ''
).toLowerCase()
}
const network = this.NetworkProvider ||
this.provider ||
store.get('network')
let signerAccount
signerAccount = this.toRpcAddress(
this.account ||
store.get('address') ||
(await this.getAccount()) ||
''
).toLowerCase()
🤖 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 `@app/components/candidates/Apply.vue` around lines 570 - 585, The account
resolution order is inconsistent between hardware and standard wallets (in
Apply.vue) causing different accounts to be chosen; standardize the lookup order
used to set signerAccount so both branches call getAccount() in the same
relative position. Update the logic around
NetworkProvider/provider/store.get('network') and the signerAccount assignment
(the branches that call this.toRpcAddress and await this.getAccount()) to use a
single, consistent resolution sequence (e.g., await this.getAccount() ||
this.account || store.get('address') || '') for both hardware
('ledger'|'trezor') and non-hardware paths so toRpcAddress and toLowerCase
operate on the same resolved value.

Comment thread app/components/candidates/Apply.vue Outdated
Comment thread helpers/ledgerWeb3Provider.js Outdated
Comment thread helpers/personalSign.js
Comment on lines +74 to +87
for (const recoveryId of recoveryIds) {
try {
const pubKey = ethUtil.ecrecover(
msgHash,
recoveryId,
sigBuf.slice(0, 32),
sigBuf.slice(32, 64)
)
const recovered = ('0x' + ethUtil.pubToAddress(pubKey).toString('hex')).toLowerCase()
if (recovered) {
return recovered
}
} catch (e) {}
}
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 | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

ethereumjs-util ecrecover function signature v parameter

💡 Result:

In ethereumjs-util, the ecrecover function’s v parameter is the signature recovery parameter (the “recovery id”) encoded in Ethereum’s v format, used together with r and s to recover the signer’s public key/address [1][2]. How v is interpreted - The API is ecrecover(msgHash, v, r, s, chainId?) where v is a number-like input for signature recovery [1]. - If v is invalid, ethereumjs-util throws “Invalid signature v value” [1]. - With an optional chainId argument, ethereumjs-util is designed to handle non-legacy v values: it converts the provided v into the underlying recovery value used by secp256k1 (which is effectively 0/1) [3]. Legacy vs EIP-155-style v - In many Ethereum signing utilities, you’ll see v derived as sig.recovery + 27 (i.e., legacy 27/28) [4]. - For EIP-155-style v values, the common relation shown is chain_id = (v − 35) / 2 [4], and ethereumjs-util’s optional chainId parameter is what enables it to interpret those v values correctly [3].

Citations:


Fix ethereumjs-util.ecrecover v argument in helpers/personalSign.js

recoverPersonalSignAddress passes recoveryId (0/1) as the v parameter to ethUtil.ecrecover(msgHash, v, r, s). In ethereumjs-util, ecrecover expects legacy v values (27/28) when no chainId is provided, so 0/1 will be rejected (caught) and the manual recovery won’t work correctly.

Proposed fix
         for (const recoveryId of recoveryIds) {
             try {
                 const pubKey = ethUtil.ecrecover(
                     msgHash,
-                    recoveryId,
+                    recoveryId + 27,
                     sigBuf.slice(0, 32),
                     sigBuf.slice(32, 64)
                 )
📝 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
for (const recoveryId of recoveryIds) {
try {
const pubKey = ethUtil.ecrecover(
msgHash,
recoveryId,
sigBuf.slice(0, 32),
sigBuf.slice(32, 64)
)
const recovered = ('0x' + ethUtil.pubToAddress(pubKey).toString('hex')).toLowerCase()
if (recovered) {
return recovered
}
} catch (e) {}
}
for (const recoveryId of recoveryIds) {
try {
const pubKey = ethUtil.ecrecover(
msgHash,
recoveryId + 27,
sigBuf.slice(0, 32),
sigBuf.slice(32, 64)
)
const recovered = ('0x' + ethUtil.pubToAddress(pubKey).toString('hex')).toLowerCase()
if (recovered) {
return recovered
}
} catch (e) {}
}
🤖 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 `@helpers/personalSign.js` around lines 74 - 87, The ecrecover call in
recoverPersonalSignAddress is passing recoveryId (0/1) as v but ethereumjs-util
expects legacy v (27/28), so change the code to pass recoveryId + 27 (ensure
it's a Number) when calling ethUtil.ecrecover(msgHash, v, r, s); locate the loop
using recoveryId, msgHash, sigBuf and replace the v argument with
(Number(recoveryId) + 27) and keep r as sigBuf.slice(0,32) and s as
sigBuf.slice(32,64), preserving the try/catch and return behavior.

Comment thread helpers/walletError.js
Comment on lines +62 to +68
function toWalletError (error) {
if (error instanceof Error) {
error.message = formatWalletError(error)
return error
}
return new Error(formatWalletError(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 | 🟡 Minor | ⚡ Quick win

Mutating original Error.message can cause unexpected behavior.

The function mutates the message property of the original Error object. If the same error instance is caught and formatted multiple times (e.g., logged then re-thrown), subsequent handlers see the already-formatted message, potentially losing context or double-formatting.

Proposed fix: clone instead of mutate
 function toWalletError (error) {
     if (error instanceof Error) {
-        error.message = formatWalletError(error)
-        return error
+        const formatted = new Error(formatWalletError(error))
+        formatted.stack = error.stack
+        return formatted
     }
     return new Error(formatWalletError(error))
 }
🤖 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 `@helpers/walletError.js` around lines 62 - 68, toWalletError currently mutates
the original Error.message which can double-format or lose context; instead,
create and return a new Error instance that uses formatWalletError(error) for
its message while preserving relevant properties (stack, name, and any custom
fields) from the original error; update the toWalletError function to construct
a cloned Error (copy stack/name/custom props) and set the formatted message
rather than modifying the passed-in Error; reference formatWalletError and
toWalletError when making the change.

rajshah1609
rajshah1609 previously approved these changes May 25, 2026
rajshah1609
rajshah1609 previously approved these changes May 26, 2026
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.

Caution

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

⚠️ Outside diff range comments (2)
app/components/candidates/Apply.vue (2)

610-620: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Stop echoing the signed KYC auth tuple into query params and headers.

These fields are already present in the multipart body, and apis/ipfs.js reads req.body before falling back to headers/query. Sending the same signed payload again via params exposes a replayable auth tuple to URL logs and intermediaries for no functional gain. Keep it in the body only.

Suggested change
-                    const { data } = await axios.post('/api/ipfs/addKYC', formData, {
-                        params: {
-                            account: signerAccount,
-                            message: message,
-                            signedMessage: signedMessage
-                        },
-                        headers: {
-                            'x-kyc-account': signerAccount,
-                            'x-kyc-message': message,
-                            'x-kyc-signature': signedMessage
-                        }
-                    })
+                    const { data } = await axios.post('/api/ipfs/addKYC', formData)
🤖 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 `@app/components/candidates/Apply.vue` around lines 610 - 620, The axios.post
call to '/api/ipfs/addKYC' in Apply.vue is duplicating the signed auth tuple
(signerAccount, message, signedMessage) into params and headers; remove the
params and headers objects that set account/message/signedMessage and the
'x-kyc-*' headers so the signed payload is only sent inside the multipart
formData body (formData already contains signerAccount, message, signedMessage).
Leave the formData and the axios.post call intact otherwise so apis/ipfs.js
continues to read req.body.

637-642: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use signerAccount as the transaction sender in the standard-wallet path.

uploadKYC(data.hash) only carries the hash, so the address written on-chain is determined by from. Here the upload is signed for signerAccount, and Line 645 checks status for that same address, but the transaction is sent from a separately resolved fromAccount. If this.account or store.get('address') is stale after an account switch, the upload is authorized for one address and recorded on-chain for another.

Suggested change
-                        const fromAccount = (this.account || store.get('address') || '').toLowerCase()
-                        const txParams = Object.assign({ from: fromAccount }, txOptions)
+                        const txParams = Object.assign({ from: signerAccount }, txOptions)
                         await this.sendContractTransaction(
                             contract.methods.uploadKYC(data.hash),
                             txParams
                         )
🤖 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 `@app/components/candidates/Apply.vue` around lines 637 - 642, The transaction
is being sent with a potentially stale fromAccount even though the upload is
signed by signerAccount; update the txParams used in sendContractTransaction so
the from field uses signerAccount instead of the resolved fromAccount in the
standard-wallet path. Locate the upload call
(contract.methods.uploadKYC(data.hash)) and the txParams creation (currently
Object.assign({ from: fromAccount }, txOptions)), replace or override that from
value with signerAccount (the same account used to sign and later checked) so
sendContractTransaction and the on-chain write use the same address as
signerAccount.
🤖 Prompt for all review comments with 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.

Outside diff comments:
In `@app/components/candidates/Apply.vue`:
- Around line 610-620: The axios.post call to '/api/ipfs/addKYC' in Apply.vue is
duplicating the signed auth tuple (signerAccount, message, signedMessage) into
params and headers; remove the params and headers objects that set
account/message/signedMessage and the 'x-kyc-*' headers so the signed payload is
only sent inside the multipart formData body (formData already contains
signerAccount, message, signedMessage). Leave the formData and the axios.post
call intact otherwise so apis/ipfs.js continues to read req.body.
- Around line 637-642: The transaction is being sent with a potentially stale
fromAccount even though the upload is signed by signerAccount; update the
txParams used in sendContractTransaction so the from field uses signerAccount
instead of the resolved fromAccount in the standard-wallet path. Locate the
upload call (contract.methods.uploadKYC(data.hash)) and the txParams creation
(currently Object.assign({ from: fromAccount }, txOptions)), replace or override
that from value with signerAccount (the same account used to sign and later
checked) so sendContractTransaction and the on-chain write use the same address
as signerAccount.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6dc87227-e0a1-43a9-81b4-802d3cb420cb

📥 Commits

Reviewing files that changed from the base of the PR and between 91fa156 and 886b7be.

📒 Files selected for processing (1)
  • app/components/candidates/Apply.vue

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
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.

2 participants