Skip to content

Simulate solana txs before send, fix peer calls#659

Merged
cwilvx merged 3 commits intotestnetfrom
fix-solana-and-peer-calls
Feb 3, 2026
Merged

Simulate solana txs before send, fix peer calls#659
cwilvx merged 3 commits intotestnetfrom
fix-solana-and-peer-calls

Conversation

@cwilvx
Copy link
Contributor

@cwilvx cwilvx commented Feb 3, 2026

User description

  • Standardize call methods
  • Use http for local connection
  • fix: xm result parsing

PR Type

Bug fix, Enhancement


Description

  • Standardize peer call method signatures with CallOptions object

    • Replace positional parameters (sleepTime, retries, allowedErrors) with options object
    • Support protocol selection (http/omni) in call options
    • Refactor all call sites to use new signature
  • Simulate Solana transactions before broadcast to prevent failures

    • Add handleSolanaPay function with pre-flight simulation
    • Deserialize and validate transaction before sending
  • Fix XM operation result parsing by removing unnecessary stringify

    • Store raw result objects instead of stringified versions
    • Improve result logging with JSON serialization
  • Improve code formatting and logging consistency

    • Remove unnecessary warning logs in consensus routines
    • Clean up formatting in multiple files

Diagram Walkthrough

flowchart LR
  A["Peer Call Methods"] -->|"Refactor to CallOptions"| B["call/longCall/httpCall"]
  C["Solana Pay Operation"] -->|"Add Pre-flight Check"| D["simulateTransaction"]
  D -->|"If Valid"| E["sendTransaction"]
  F["XM Parser"] -->|"Remove stringify"| G["Store Raw Results"]
  B -->|"Support Protocol Selection"| H["HTTP or OmniProtocol"]
Loading

File Walkthrough

Relevant files
Enhancement
14 files
XMDispatcher.ts
Improve XM execution result logging                                           
+1/-1     
pay.ts
Add Solana transaction pre-flight simulation                         
+62/-15 
Sync.ts
Refactor peer calls to use CallOptions                                     
+46/-28 
broadcastManager.ts
Update longCall invocations with options object                   
+10/-2   
consensusTime.ts
Remove unnecessary warning log message                                     
+1/-3     
PoRBFT.ts
Remove redundant warning logs and comments                             
+0/-4     
mergeMempools.ts
Refactor longCall to use CallOptions object                           
+11/-11 
secretaryManager.ts
Update longCall signatures and improve formatting               
+19/-20 
dtrmanager.ts
Refactor DTR validator calls with CallOptions                       
+15/-15 
manageConsensusRoutines.ts
Change warning log to debug level                                               
+1/-1     
peerAdapter.ts
Update adapter to use CallOptions interface                           
+15/-22 
Peer.ts
Standardize call methods with CallOptions interface           
+177/-219
PeerManager.ts
Update hello peer call with CallOptions                                   
+4/-2     
peerBootstrap.ts
Add retry logic and improve peer bootstrap                             
+53/-22 
Bug fix
1 files
XMParser.ts
Fix XM operation result parsing                                                   
+1/-1     
Configuration changes
1 files
run
Re-enable TLS Notary and Monitoring features                         
+2/-2     

Summary by CodeRabbit

  • New Features

    • TLSNotary and monitoring stack now enabled by default, improving observability and security.
  • Bug Fixes & Improvements

    • Enhanced network resilience with automatic retry logic and timeout handling for remote calls.
    • Improved Solana transaction processing with dedicated handling and simulation validation.
    • Strengthened peer bootstrap reliability with retry mechanism for connection attempts.
    • Added authentication headers to network requests for improved security.
    • Better offline peer detection and error reporting for failed connections.

- lock parameters to 3 items
- introduce options object as 3rd parameter
- add CallOptions interface
- allow call and longCall methods to specify protocol in options object
- refactor call sites: Sync.ts, and friends.
- reroute peer.call to peer.httpCall for local node
- simulate solana tx before broadcast
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 3, 2026

Walkthrough

This PR refactors network call infrastructure by unifying retry/timeout handling into an options-based API, enables TLSNotary and monitoring by default, improves logging consistency across consensus and network routines, enhances Solana transaction handling with simulation, and adds retry logic to peer bootstrap.

Changes

Cohort / File(s) Summary
Configuration Defaults
run.js
Switched TLSNOTARY_DISABLED and MONITORING_DISABLED from true to false, enabling TLSNotary and monitoring stack by default.
Network Call API Refactoring
src/libs/peer/Peer.ts, src/libs/peer/PeerManager.ts, src/libs/communications/broadcastManager.ts, src/libs/consensus/v2/routines/mergeMempools.ts, src/libs/consensus/v2/types/secretaryManager.ts, src/libs/network/dtr/dtrmanager.ts, src/libs/omniprotocol/integration/peerAdapter.ts
Introduced CallOptions interface and refactored longCall invocations from positional parameters (sleepTime, retries, allowedCodes) to options objects; Peer.ts additionally enhanced httpCall with authentication header construction, timeout handling, and peer offline marking; call method now supports protocol selection (http or omni).
Logging and Format Improvements
src/features/multichain/XMDispatcher.ts, src/libs/consensus/routines/consensusTime.ts, src/libs/consensus/v2/PoRBFT.ts, src/libs/network/manageConsensusRoutines.ts
Condensed debug output formatting; removed warning logs for internal state checks (consensus loop already running, in consensus loop); changed one warning to debug level in consensus message handling.
Multichain Execution
src/features/multichain/routines/XMParser.ts, src/features/multichain/routines/executors/pay.ts
XMParser now stores raw result objects instead of stringified values; pay.ts introduces dedicated handleSolanaPay function with transaction simulation before sending; refactored XRPL handling for clarity with multi-line conditional checks.
Blockchain Synchronization
src/libs/blockchain/routines/Sync.ts
Replaced httpCall with longCall across getRemoteBlock, downloadBlock, batchDownloadBlocks, and askTxsForBlock; added unified retry configuration (protocol: "http", sleepTime: 1000, retries: 3).
Peer Bootstrap Enhancement
src/libs/peer/routines/peerBootstrap.ts
Added anchorLoop label and retry mechanism (max 3 retries) for sayHelloToPeer; improved identity handling and error logging; changed control flow to retry on peer list absence instead of immediate failure.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

Review effort 4/5

Poem

🐰 Through networks vast, with options bright,
Retry loops dance through bootstrap night,
Solana simulates before it flies,
As monitoring awakens—no more goodbyes!
The rabbit refactors with grace and might! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main changes: Solana transaction simulation before sending and standardization of peer call methods.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-solana-and-peer-calls

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.

@qodo-code-review
Copy link
Contributor

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Sensitive data in logs

Description: The new HTTP RPC path logs full connection targets and, on errors, logs REQUEST PAYLOAD:
with JSON.stringify(request), which can expose sensitive or security-relevant data (e.g.,
signed payloads, auth-related fields, transaction contents) to logs and log aggregation
systems.
Peer.ts [284-442]

Referred Code
log.info(
    "[RPC Call] [" +
        request.method +
        "] [" +
        new Date(Date.now()).toISOString() +
        "] Making RPC call to: " +
        this.connection.string,
)

// Get some informations
const method = request.method
const currentTimestampReadable = new Date(Date.now()).toISOString()
// Prepare a request with our identity
let pubkey = ""
let signature = ""

if (isAuthenticated) {
    const ourPublicKey = (
        await ucrypto.getIdentity(getSharedState.signingAlgorithm)
    ).publicKey
    const hexPublicKey = uint8ArrayToHex(ourPublicKey as Uint8Array)


 ... (clipped 138 lines)
Potential DoS via payload

Description: handleSolanaPay deserializes and simulates a transaction from
operation.task.signedPayloads[0] without explicit bounds/size checks, so a crafted
oversized payload could cause excessive CPU/memory usage during
VersionedTransaction.deserialize/simulateTransaction (potential DoS) if untrusted inputs
can reach this path.
pay.ts [164-193]

Referred Code
async function handleSolanaPay(rpcUrl: string, operation: IOperation) {
    try {
        // INFO: payload is an object of Uint8Array values
        const payload = Uint8Array.from(
            Object.values(operation.task.signedPayloads[0]),
        )
        const solana = await multichain.SOLANA.create(rpcUrl)

        const simulateResult = await solana.provider.simulateTransaction(
            VersionedTransaction.deserialize(payload),
            {
                commitment: "confirmed",
            },
        )

        if (simulateResult.value.err) {
            return {
                result: "error",
                error: simulateResult.value.err,
            }
        }


 ... (clipped 9 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Missing payload checks: handleSolanaPay constructs a payload from operation.task.signedPayloads[0] without
validating that signedPayloads exists and is non-empty, which can throw at runtime and
bypass graceful handling.

Referred Code
async function handleSolanaPay(rpcUrl: string, operation: IOperation) {
    try {
        // INFO: payload is an object of Uint8Array values
        const payload = Uint8Array.from(
            Object.values(operation.task.signedPayloads[0]),
        )
        const solana = await multichain.SOLANA.create(rpcUrl)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status:
Raw error returned: The HTTP RPC path returns the raw error object in the RPC response (response: error),
which can expose internal implementation details to upstream callers.

Referred Code
} else {
    log.error(
        "[RPC Call] [" +
            method +
            "] [" +
            currentTimestampReadable +
            "] Error making RPC call:" +
            error,
    )
    log.error("CONNECTION URL: " + connectionUrl)
    log.error("REQUEST PAYLOAD: " + JSON.stringify(request))

    return {
        result: 500,
        response: error,
        require_reply: false,
        extra: null,
    }

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Logs full payload: The new error path logs REQUEST PAYLOAD via JSON.stringify(request) which can include
sensitive transaction/auth data and risks leaking it into application logs.

Referred Code
log.error(
    "[RPC Call] [" +
        method +
        "] [" +
        currentTimestampReadable +
        "] Error making RPC call:" +
        error,
)
log.error("CONNECTION URL: " + connectionUrl)
log.error("REQUEST PAYLOAD: " + JSON.stringify(request))

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Unvalidated external input: handleSolanaPay deserializes and simulates a transaction derived from
operation.task.signedPayloads without validating presence, type, or bounds, creating risk
of crashes or malformed input processing.

Referred Code
async function handleSolanaPay(rpcUrl: string, operation: IOperation) {
    try {
        // INFO: payload is an object of Uint8Array values
        const payload = Uint8Array.from(
            Object.values(operation.task.signedPayloads[0]),
        )
        const solana = await multichain.SOLANA.create(rpcUrl)

        const simulateResult = await solana.provider.simulateTransaction(
            VersionedTransaction.deserialize(payload),
            {
                commitment: "confirmed",
            },
        )

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing actor context: New RPC call logs include timestamps and method names but do not clearly record the
actor/user identity performing the action, making it harder to reconstruct who initiated
critical actions from logs alone.

Referred Code
log.info(
    "[RPC Call] [" +
        request.method +
        "] [" +
        new Date(Date.now()).toISOString() +
        "] Making RPC call to: " +
        this.connection.string,
)

// Get some informations
const method = request.method
const currentTimestampReadable = new Date(Date.now()).toISOString()
// Prepare a request with our identity
let pubkey = ""
let signature = ""

if (isAuthenticated) {
    const ourPublicKey = (
        await ucrypto.getIdentity(getSharedState.signingAlgorithm)
    ).publicKey
    const hexPublicKey = uint8ArrayToHex(ourPublicKey as Uint8Array)


 ... (clipped 9 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@sonarqubecloud
Copy link

sonarqubecloud bot commented Feb 3, 2026

@qodo-code-review
Copy link
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Make peer bootstrap more resilient

In peerBootstrap, remove process.exit(1) when failing to connect to an anchor
peer. Instead, log the error and continue to the next peer in the list to make
the startup process more robust.

src/libs/peer/routines/peerBootstrap.ts [23-99]

 export default async function peerBootstrap(
     localList: Peer[],
 ): Promise<Peer[]> {
     log.info("[BOOTSTRAP] Loading peers...")
 
     // Validity check
     anchorLoop: for (let i = 0; i < localList.length; i++) {
         log.debug("[BOOTSTRAP] Checking peer " + localList[i])
         // ANCHOR Extract peer info from the string
         const currentPeer: Peer = localList[i] // The url of the peer
         // If there is a : in the url, we assume it's a address + port
         const currentPeerUrl: string = currentPeer.connection.string
         const currentPublicKey: string = currentPeer.identity
         log.debug(
             "[BOOTSTRAP] Testing " +
                 currentPeerUrl +
                 " with id " +
                 currentPublicKey,
         )
         // ANCHOR Connection test and hello_peer routine
         const blankPeer = new Peer(currentPeerUrl, currentPublicKey)
         // Adding identity if any
         log.debug("[BOOTSTRAP] Testing " + currentPeerUrl + " identity")
         // After this, the peer object will have an identity and thus will be verified
         const verifiedPeer = await getPeerIdentity(blankPeer, currentPublicKey)
         if (!verifiedPeer) {
             log.warning(
                 "[BOOTSTRAP] [FAILED] Failed to get peer identity: see above",
             )
             peerManager.addOfflinePeer(blankPeer)
             peerManager.removeOnlinePeer(blankPeer.identity)
             continue
         }
 
         log.debug("[BOOTSTRAP] Overriding connection string: " + currentPeerUrl)
         log.debug("[BOOTSTRAP] Verified peer: " + JSON.stringify(verifiedPeer))
 
         try {
             verifiedPeer.connection.string = currentPeerUrl // Adding this step
         } catch (error) {
             log.error("[BOOTSTRAP] Error setting connection string: " + error)
             log.critical("Error setting connection string: " + error)
             continue
         }
         log.info("[BOOTSTRAP] OK: Valid peer " + currentPeerUrl)
 
         log.debug(
             "[BOOTSTRAP] Current peer object: " + JSON.stringify(verifiedPeer),
         )
 
         let maxRetries = 3
         while (maxRetries > 0) {
             await PeerManager.sayHelloToPeer(verifiedPeer, true)
 
             // INFO: Confirmed we paired with anchor node
             if (
                 peerManager
                     .getPeers()
                     .find(p => p.identity === verifiedPeer.identity)
             ) {
                 continue anchorLoop
             }
 
             log.warn("[BOOTSTRAP] Failed to pair with anchor node, retrying...")
             maxRetries--
             await sleep(1000)
         }
 
         log.error(
             "[BOOTSTRAP] Failed to pair with anchor peer: " +
                 verifiedPeer.identity +
                 " @ " +
                 verifiedPeer.connection.string +
-                ". Exiting ...",
+                ". Continuing to next anchor...",
         )
-        process.exit(1)
     }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 9

__

Why: This suggestion correctly identifies a critical flaw in the bootstrap logic where failure to connect to a single anchor peer causes the entire process to exit. The proposed change significantly improves the node's startup resilience.

High
Validate and reuse VersionedTransaction

Refactor handleSolanaPay to validate the payload, deserialize it into a
VersionedTransaction once, and reuse this object for both simulateTransaction
and sendTransaction for better robustness and consistency.

src/features/multichain/routines/executors/pay.ts [164-193]

 async function handleSolanaPay(rpcUrl: string, operation: IOperation) {
     try {
-        // INFO: payload is an object of Uint8Array values
-        const payload = Uint8Array.from(
-            Object.values(operation.task.signedPayloads[0]),
-        )
+        const signed = operation.task.signedPayloads[0]
+        // Validate signed payload is Uint8Array
+        if (!validateIfUint8Array(signed)) {
+            return { result: "error", error: "Invalid Solana payload" }
+        }
+        // Build VersionedTransaction once
+        const txBytes = signed as Uint8Array
+        const tx = VersionedTransaction.deserialize(txBytes)
         const solana = await multichain.SOLANA.create(rpcUrl)
 
-        const simulateResult = await solana.provider.simulateTransaction(
-            VersionedTransaction.deserialize(payload),
-            {
-                commitment: "confirmed",
-            },
-        )
-
-        if (simulateResult.value.err) {
-            return {
-                result: "error",
-                error: simulateResult.value.err,
-            }
+        // Simulate
+        const sim = await solana.provider.simulateTransaction(tx, { commitment: "confirmed" })
+        if (sim.value.err) {
+            return { result: "error", error: JSON.stringify(sim.value.err) }
         }
-
-        return await solana.sendTransaction(payload)
+        // Send with same tx object
+        const sendRes = await solana.provider.sendTransaction(tx, { preflightCommitment: "confirmed" })
+        return sendRes
     } catch (error) {
-        return {
-            result: "error",
-            error: error.toString(),
-        }
+        return { result: "error", error: error.toString() }
     }
 }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion improves the robustness and clarity of handleSolanaPay by adding payload validation, reusing the deserialized transaction, and using a more direct provider method, which is a good practice.

Medium
Stringify Solana simulation error object

In handleSolanaPay, serialize the simulateResult.value.err object to a string
using JSON.stringify to ensure the full error details are preserved in the
return value.

src/features/multichain/routines/executors/pay.ts [179-184]

 if (simulateResult.value.err) {
     return {
         result: "error",
-        error: simulateResult.value.err,
+        error: JSON.stringify(simulateResult.value.err),
     }
 }
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies that simulateResult.value.err can be an object, and stringifying it prevents loss of error information, improving debuggability.

Low
General
Rename retry variables for clarity

In the longCall function, rename the retries and sleepTime variables to
maxRetries and delayMs respectively to improve code clarity and avoid potential
confusion.

src/libs/peer/Peer.ts [162-186]

 async longCall(
     request: RPCRequest,
     isAuthenticated = true,
     options?: CallOptions,
 ): Promise<RPCResponse> {
-    const retries = options?.retries ?? 3
-    const sleepTime = options?.sleepTime ?? 1000
+    const maxRetries = options?.retries ?? 3
+    const delayMs = options?.sleepTime ?? 1000
     const allowedCodes = options?.allowedCodes ?? []
 
-    let tries = 0
+    let attempt = 0
     let response: RPCResponse | null = null
-    while (tries < retries) {
+    while (attempt < maxRetries) {
         response = await this.call(request, isAuthenticated, options)
         if (
             response.result === 200 ||
             allowedCodes.includes(response.result)
         ) {
             return response
         }
-        tries++
-        // Sleep for sleepTime milliseconds before next retry
-        if (tries < retries) {
-            await sleep(sleepTime)
+        attempt++
+        if (attempt < maxRetries) {
+            await sleep(delayMs)
         }
     }
     ...
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 2

__

Why: The suggestion offers a minor improvement to code readability by renaming variables for clarity. While correct, it has a low impact on functionality or correctness.

Low
  • More

@cwilvx cwilvx merged commit ce4dfc4 into testnet Feb 3, 2026
7 of 8 checks passed
@cwilvx cwilvx deleted the fix-solana-and-peer-calls branch February 3, 2026 07:17
Copy link
Contributor

@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: 1

Caution

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

⚠️ Outside diff range comments (3)
src/features/multichain/routines/XMParser.ts (1)

67-91: ⚠️ Potential issue | 🟠 Major

Align execute() return type with new result shape.
You now store raw objects in results[name], but the return type still promises result: string. This breaks the API contract and risks JSON serialization errors (e.g., BigInt). Update the type and log using the existing stringify helper (or revert to stringifying the stored result).

✅ One possible fix (update type + safe logging)
-    static async execute(fullscript: XMScript): Promise<{
-        [operationId: string]: {
-            result: string
-            error?: string
-        }
-    }> {
+    static async execute(fullscript: XMScript): Promise<{
+        [operationId: string]: {
+            result: unknown
+            error?: string
+        }
+    }> {
         const results = {}
         let name: string, operation: IOperation
@@
-                results[name] = result
-                log.debug("[RESULT]: " + results[name])
+                results[name] = result
+                log.debug("[RESULT]: " + stringify(results[name]))
src/features/multichain/routines/executors/pay.ts (1)

78-193: ⚠️ Potential issue | 🟡 Minor

Normalize Solana signed payload before deserialize.

The Solana handler uses Object.values() directly, but other chains use validateIfUint8Array for consistency. If the payload arrives as a hex string, already-typed array, or with out-of-order numeric keys, Object.values() can corrupt or misorder the bytes. Use the existing validateIfUint8Array helper (imported at line 8) to handle all payload shapes robustly.

Notably, handleSolanaContractWrite in contract_write.ts already delegates to genericJsonRpcPay, which uses validateIfUint8Array for Solana—this handler should follow the same pattern.

🛠️ Suggested change
-        const payload = Uint8Array.from(
-            Object.values(operation.task.signedPayloads[0]),
-        )
+        const payload = validateIfUint8Array(
+            operation.task.signedPayloads[0],
+        )
src/libs/network/dtr/dtrmanager.ts (1)

329-343: ⚠️ Potential issue | 🔴 Critical

Bug: Wrong field name in tryRelayTransaction params - will not trigger handler.

The handler in manageNodeCall.ts (line 61) switches on content.message, but tryRelayTransaction uses type: "RELAY_TX" instead of message: "RELAY_TX". This prevents the handler's RELAY_TX case from ever being triggered. Change line 334 from type: "RELAY_TX" to message: "RELAY_TX" to match the correct field name expected by the handler.

🤖 Fix all issues with AI agents
In `@src/libs/peer/Peer.ts`:
- Around line 279-442: In httpCall's catch generic branch (inside the async
method httpCall in Peer.ts) the 500 response returns the raw error object;
change this to return a serializable string by extracting a safe message (e.g.,
if axios.isAxiosError(error) use error.message or error.response?.data,
otherwise use String(error) or error.message) and put that string into the
response field (and optionally include a small serialized extra like { message:
..., code: error.code || null } ); update the return in the generic error branch
so response is always a string and extra contains only serializable primitives.
🧹 Nitpick comments (1)
src/libs/consensus/v2/routines/mergeMempools.ts (1)

6-31: Consider Promise.allSettled to avoid fail-fast merges.
If any peer call rejects, Promise.all aborts and skips partial successes. A settled aggregation keeps the merge resilient.

♻️ Suggested change
-    const responses = await Promise.all(promises) // ! Add error handling
+    const settled = await Promise.allSettled(promises)
+    const responses = settled
+        .filter(
+            (r): r is PromiseFulfilledResult<RPCResponse> =>
+                r.status === "fulfilled",
+        )
+        .map(r => r.value)
+    for (const r of settled) {
+        if (r.status === "rejected") {
+            log.error("[mergeMempools] Peer call rejected: " + r.reason)
+        }
+    }

Comment on lines +279 to 442
// Single HTTP call (no retry logic - use longCall for retries)
async httpCall(
request: RPCRequest,
isAuthenticated = true,
sleepTime = 1000,
retries = 3,
allowedErrors: number[] = [],
): Promise<RPCResponse> {
let tries = 0
let lastResponse: RPCResponse | null = null
log.info(
"[RPC Call] [" +
request.method +
"] [" +
new Date(Date.now()).toISOString() +
"] Making RPC call to: " +
this.connection.string,
)

while (tries < retries) {
log.info(
"[RPC Call] [" +
request.method +
"] [" +
new Date(Date.now()).toISOString() +
"] Making RPC call to: " +
this.connection.string +
(tries > 0 ? ` (attempt ${tries + 1}/${retries})` : ""),
// Get some informations
const method = request.method
const currentTimestampReadable = new Date(Date.now()).toISOString()
// Prepare a request with our identity
let pubkey = ""
let signature = ""

if (isAuthenticated) {
const ourPublicKey = (
await ucrypto.getIdentity(getSharedState.signingAlgorithm)
).publicKey
const hexPublicKey = uint8ArrayToHex(ourPublicKey as Uint8Array)
const bufferSignature = await ucrypto.sign(
getSharedState.signingAlgorithm,
new TextEncoder().encode(hexPublicKey),
)

// Get some informations
const method = request.method
const currentTimestampReadable = new Date(Date.now()).toISOString()
// Prepare a request with our identity
let pubkey = ""
let signature = ""

if (isAuthenticated) {
const ourPublicKey = (
await ucrypto.getIdentity(getSharedState.signingAlgorithm)
).publicKey
const hexPublicKey = uint8ArrayToHex(ourPublicKey as Uint8Array)
const bufferSignature = await ucrypto.sign(
getSharedState.signingAlgorithm,
new TextEncoder().encode(hexPublicKey),
)

pubkey = getSharedState.signingAlgorithm + ":" + hexPublicKey
signature = uint8ArrayToHex(bufferSignature.signature)
}
pubkey = getSharedState.signingAlgorithm + ":" + hexPublicKey
signature = uint8ArrayToHex(bufferSignature.signature)
}

// REVIEW Using the connection string as the url with the new format
let connectionUrl = this.connection.string
// REVIEW Using the connection string as the url with the new format
let connectionUrl = this.connection.string

// INFO: If the peer is the local node, we use the internal connection string
if (this.isLocalNode) {
connectionUrl = getSharedState.connectionString
}
// INFO: If the peer is the local node, we use the internal connection string
if (this.isLocalNode) {
connectionUrl = getSharedState.connectionString
}

// Make the request
let timeoutId: NodeJS.Timeout | undefined
try {
// Create AbortController for connection timeout (covers TCP handshake + HTTP request)
const abortController = new AbortController()
timeoutId = setTimeout(() => {
abortController.abort()
}, 3000)

const response = await axios.post<RPCResponse>(
connectionUrl,
request,
{
headers: {
"Content-Type": "application/json",
identity: pubkey,
signature: signature,
},
timeout: 3000,
signal: abortController.signal,
// Make the request
let timeoutId: NodeJS.Timeout | undefined
try {
// Create AbortController for connection timeout (covers TCP handshake + HTTP request)
const abortController = new AbortController()
timeoutId = setTimeout(() => {
abortController.abort()
}, 3000)

const response = await axios.post<RPCResponse>(
connectionUrl,
request,
{
headers: {
"Content-Type": "application/json",
identity: pubkey,
signature: signature,
},
timeout: 3000,
signal: abortController.signal,
},
)

clearTimeout(timeoutId)
log.info(
"[RPC Call] [" +
method +
"] [" +
currentTimestampReadable +
"] Response received ",
)
if (response.data.result == 200) {
log.info(
"[RPC Call] [" +
method +
"] [" +
currentTimestampReadable +
"] Response OK: " +
response.data.result,
)
}

return response.data
} catch (error) {
// Clear timeout if request completed (error or success)
if (timeoutId) {
clearTimeout(timeoutId)
log.info(
}

// Handle abort/timeout errors
if (
axios.isAxiosError(error) &&
(error.code === "ECONNABORTED" ||
error.message === "canceled" ||
error.name === "AbortError" ||
error.code === "ETIMEDOUT")
) {
log.warn(
"[RPC Call] [" +
method +
"] [" +
currentTimestampReadable +
"] Response received ",
"] Request timeout/aborted to: " +
connectionUrl,
)
// log.info(JSON.stringify(response.data, null, 2))
if (response.data.result !== 200) {
log.warning(
"[RPC Call] [" +
method +
"] [" +
currentTimestampReadable +
"] Response not OK: " +
response.data.response +
" - " +
response.data.result,
)
} else {
log.info(
"[RPC Call] [" +
method +
"] [" +
currentTimestampReadable +
"] Response OK: " +
response.data.result,
)
}

lastResponse = response.data
this.markPeerOffline()

if (
response.data.result === 200 ||
allowedErrors.includes(response.data.result)
) {
return response.data
}
} catch (error) {
// Clear timeout if request completed (error or success)
if (timeoutId) {
clearTimeout(timeoutId)
return {
result: 504,
response: "Request timeout",
require_reply: false,
extra: {
code: error.code || "ETIMEDOUT",
url: connectionUrl,
},
}
}
// Handle ECONNREFUSED error
else if (
axios.isAxiosError(error) &&
error.code === "ECONNREFUSED"
) {
log.warn(
"[RPC Call] [" +
method +
"] [" +
currentTimestampReadable +
"] Connection refused to: " +
connectionUrl,
)

// Handle abort/timeout errors
if (
axios.isAxiosError(error) &&
(error.code === "ECONNABORTED" ||
error.message === "canceled" ||
error.name === "AbortError" ||
error.code === "ETIMEDOUT")
) {
log.warn(
"[RPC Call] [" +
method +
"] [" +
currentTimestampReadable +
"] Request timeout/aborted to: " +
connectionUrl,
)

this.markPeerOffline()

lastResponse = {
result: 504,
response: "Request timeout",
require_reply: false,
extra: {
code: error.code || "ETIMEDOUT",
url: connectionUrl,
},
}

if (allowedErrors.includes(504)) {
return lastResponse
}
this.markPeerOffline()

return {
result: 503,
response: "Connection refused",
require_reply: false,
extra: {
code: error.code,
url: connectionUrl,
},
}
// Handle ECONNREFUSED error
else if (
axios.isAxiosError(error) &&
error.code === "ECONNREFUSED"
) {
log.warn(
"[RPC Call] [" +
method +
"] [" +
currentTimestampReadable +
"] Connection refused to: " +
connectionUrl,
)

this.markPeerOffline()

lastResponse = {
result: 503,
response: "Connection refused",
require_reply: false,
extra: {
code: error.code,
url: connectionUrl,
},
}

if (allowedErrors.includes(503)) {
return lastResponse
}
} else {
log.error(
"[RPC Call] [" +
method +
"] [" +
currentTimestampReadable +
"] Error making RPC call:" +
error,
)
log.error("CONNECTION URL: " + connectionUrl)
log.error("REQUEST PAYLOAD: " + JSON.stringify(request))

lastResponse = {
result: 500,
response: error,
require_reply: false,
extra: null,
}

if (allowedErrors.includes(500)) {
return lastResponse
}
} else {
log.error(
"[RPC Call] [" +
method +
"] [" +
currentTimestampReadable +
"] Error making RPC call:" +
error,
)
log.error("CONNECTION URL: " + connectionUrl)
log.error("REQUEST PAYLOAD: " + JSON.stringify(request))

return {
result: 500,
response: error,
require_reply: false,
extra: null,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Return a serializable error string on generic failures.
The 500 branch returns a raw error object, which can be non-serializable and inconsistent with other responses. Prefer a string/message.

🛠️ Suggested change
-                return {
-                    result: 500,
-                    response: error,
-                    require_reply: false,
-                    extra: null,
-                }
+                return {
+                    result: 500,
+                    response:
+                        error instanceof Error ? error.message : String(error),
+                    require_reply: false,
+                    extra: null,
+                }
🤖 Prompt for AI Agents
In `@src/libs/peer/Peer.ts` around lines 279 - 442, In httpCall's catch generic
branch (inside the async method httpCall in Peer.ts) the 500 response returns
the raw error object; change this to return a serializable string by extracting
a safe message (e.g., if axios.isAxiosError(error) use error.message or
error.response?.data, otherwise use String(error) or error.message) and put that
string into the response field (and optionally include a small serialized extra
like { message: ..., code: error.code || null } ); update the return in the
generic error branch so response is always a string and extra contains only
serializable primitives.

@coderabbitai coderabbitai bot mentioned this pull request Feb 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant