Skip to content

refactor(billing): improves batch signing client#2138

Merged
ygrishajev merged 4 commits intomainfrom
feature/billing
Nov 3, 2025
Merged

refactor(billing): improves batch signing client#2138
ygrishajev merged 4 commits intomainfrom
feature/billing

Conversation

@ygrishajev
Copy link
Contributor

@ygrishajev ygrishajev commented Oct 30, 2025

Summary by CodeRabbit

  • New Features

    • Batched sign-and-broadcast API with lifecycle controls (dispose, pending-state)
    • New client creation via HTTP endpoint
    • Async memoization utility exposed
  • Bug Fixes

    • Improved retry/recovery for sequence-mismatch and transient errors
    • Better cleanup to prevent orphaned signing clients; added explicit dispose helper
  • Improvements

    • Enhanced batching/state logging and shutdown cleanup (server now runs disposal hook)
    • Functional-test env: sampling adjusted and stdout log format set to pretty

@ygrishajev ygrishajev requested a review from a team as a code owner October 30, 2025 16:45
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 30, 2025

Walkthrough

Replaces single-tx execution with a DataLoader-driven batched signAndBroadcast pipeline (semaphore-serialized batch processing, retry executors, memoized address/chainId, lifecycle dispose), swaps connectWithSigner for createWithEndpoint, updates providers/consumers/tests, and adds async memoization utility and tests.

Changes

Cohort / File(s) Summary
Configuration
apps/api/env/.env.functional.test
Sets AMPLITUDE_SAMPLING=0 and adds STD_OUT_LOG_FORMAT=pretty.
Batch Signing Core
apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts
Introduces signAndBroadcast with DataLoader batching, SignAndBroadcastOptions/SignAndBroadcastBatchOptions, CreateWithSignerFn, sign/broadcast batch pipelines (signBatch, broadcastBatch), semaphore-serialized blocking batch, retry executors (signAndBroadcastExecutor, getTxExecutor), memoized getAddress/getChainId, logging/tracing events, dispose() and hasPendingTransactions; removes legacy executeTx and related account/sequence caching.
Sync Signing Client Factory
apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts
Replaces connectWithSigner/createWithSigner with static createWithEndpoint(endpoint, signer, options); adds connected(); switches to HTTP/Comet38Client initialization and adjusts imports.
Provider Registrations & Disposal
apps/api/src/billing/providers/signing-client.provider.ts
Rebinds factories to SyncSigningStargateClient.createWithEndpoint and exports disposeSigningClients to concurrently dispose created BatchSigningClientService instances.
Consumers / Service Updates
apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts, apps/api/src/billing/services/managed-signer/managed-signer.service.ts
Switches callers from ExecuteTxOptions/executeTx to SignAndBroadcastOptions/signAndBroadcast, updates factory binding to createWithEndpoint, and adds client cleanup (dispose + cache removal when idle).
Batch Signing Tests
apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.spec.ts
Rewrites tests to a batch-focused suite with TransactionTestData, modular setup, scenarios for full/partial success and retry semantics, and new mocks/utilities.
Caching Utility & Tests
apps/api/src/caching/helpers.ts, apps/api/src/caching/helpers.spec.ts
Adds and exports memoizeAsync (caches in-flight Promise, evicts on rejection) and tests for concurrency, rejection handling, per-argument separation, complex args, and decorator integration.
Shutdown / Disposal Flow
apps/api/src/console.ts, apps/api/src/background-jobs-app.ts, apps/api/src/rest-app.ts, apps/api/src/core/services/start-server/start-server.ts
Adds disposeSigningClients() to server shutdown via an optional beforeEnd hook and wires disposal into bootstrap/shutdown flows to ensure signing clients are cleaned up before container/resource teardown.
Minor Simplification
apps/api/src/billing/services/balances/balances.service.ts
Simplifies calculateDeploymentEscrowBalance by returning the reduce result directly (removes intermediate variable).

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant BatchSvc as BatchSigningClientService
    participant Loader as DataLoader
    participant Signer as signBatch
    participant Broadcaster as broadcastBatch
    participant ChainErr as ChainErrorService

    Note over Caller,BatchSvc: Batched signAndBroadcast flow

    Caller->>BatchSvc: signAndBroadcast(messages, options)
    activate BatchSvc
    BatchSvc->>Loader: load({ messages, options })
    deactivate BatchSvc

    par Accumulate requests
        Loader->>Loader: collect and batch requests until flush
    end

    Loader->>BatchSvc: flush batch
    activate BatchSvc
    BatchSvc->>Signer: signBatch(inputs)  %% sequential per-input signing
    activate Signer
    Signer-->>BatchSvc: signedTxs
    deactivate Signer

    BatchSvc->>Broadcaster: broadcastBatch(signedTxs)  %% sequential broadcast, tx-cache checks
    activate Broadcaster
    Broadcaster-->>BatchSvc: tx results / errors
    deactivate Broadcaster

    alt All success
        BatchSvc-->>Caller: resolve IndexedTx[] (all settled)
    else Partial/Failure
        BatchSvc->>ChainErr: handle/retry/log
        BatchSvc-->>Caller: partial results (Promise.allSettled)
    end
    deactivate BatchSvc
Loading
sequenceDiagram
    participant App
    participant OldFactory as connectWithSigner (old)
    participant NewFactory as createWithEndpoint (new)

    Note over App,NewFactory: Factory method change for creating signing clients

    rect rgb(240,240,240)
        App->>OldFactory: connectWithSigner(...)  -- removed
    end

    rect rgb(225,245,225)
        App->>NewFactory: createWithEndpoint(endpoint, signer, options)
        NewFactory->>NewFactory: build HttpClient + Comet38Client
        NewFactory-->>App: SyncSigningStargateClient
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Areas to focus review on:

  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts: sequence-number handling across signBatch, semaphore correctness, DataLoader boundaries, retry/backoff semantics, and tracing/log events.
  • tests in apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.spec.ts: realism of mocks and flakiness risk.
  • lifecycle/disposal: disposeSigningClients export, dispose() semantics, and dedupe-signing-client cleanup to avoid races/leaks.
  • apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts: HTTP/Comet initialization and connected() semantics.

Possibly related PRs

Suggested reviewers

  • baktun14

Poem

🐰 I hopped through the queue and signed each batch bright,
Shared promises snug in memoized light.
Retries kept me steady when sequences misplayed,
A tidy dispose tucked clients to shade.
Hop—new endpoint factory made startup feel light. 🥕

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "refactor(billing): improves batch signing client" is directly related to the primary focus of the changeset, which involves a comprehensive refactor of the BatchSigningClientService. The batch signing client receives significant improvements including new batching capabilities via DataLoader, retry logic with exponential backoff, new public methods like signAndBroadcast and dispose(), improved lifecycle management, and better fee estimation. While the PR includes secondary changes to related services, environment configuration, and a new caching utility, these are all driven by and dependent on the main batch signing client refactor. The title clearly identifies both the scope (billing module) and the primary subject (batch signing client), making it clear to reviewers scanning the history.
✨ 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 feature/billing

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

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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4126c7e and b0b6cf2.

📒 Files selected for processing (8)
  • apps/api/env/.env.functional.test (1 hunks)
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.spec.ts (1 hunks)
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (1 hunks)
  • apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts (1 hunks)
  • apps/api/src/billing/providers/signing-client.provider.ts (3 hunks)
  • apps/api/src/billing/services/balances/balances.service.ts (1 hunks)
  • apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts (3 hunks)
  • apps/api/src/billing/services/managed-signer/managed-signer.service.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Never use type any or cast to type any. Always define the proper TypeScript types.

Files:

  • apps/api/src/billing/services/balances/balances.service.ts
  • apps/api/src/billing/services/managed-signer/managed-signer.service.ts
  • apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts
  • apps/api/src/billing/providers/signing-client.provider.ts
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.spec.ts
  • apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{js,ts,tsx}: Never use deprecated methods from libraries.
Don't add unnecessary comments to the code

Files:

  • apps/api/src/billing/services/balances/balances.service.ts
  • apps/api/src/billing/services/managed-signer/managed-signer.service.ts
  • apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts
  • apps/api/src/billing/providers/signing-client.provider.ts
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.spec.ts
  • apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts
**/*.spec.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/no-jest-mock.mdc)

Don't use jest.mock() to mock dependencies in test files. Instead, use jest-mock-extended to create mocks and pass mocks as dependencies to the service under test.

**/*.spec.{ts,tsx}: Use setup function instead of beforeEach in test files
setup function must be at the bottom of the root describe block in test files
setup function creates an object under test and returns it
setup function should accept a single parameter with inline type definition
Don't use shared state in setup function
Don't specify return type of setup function

Files:

  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.spec.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: baktun14
PR: akash-network/console#1725
File: apps/api/src/utils/constants.ts:5-5
Timestamp: 2025-07-24T17:00:52.361Z
Learning: In the Akash Network Console project, when cross-cutting concerns or broader refactoring issues are identified during PR review, the preferred approach is to create a separate GitHub issue to track the work rather than expanding the scope of the current PR. This maintains focus and allows for proper planning of architectural improvements.
🧬 Code graph analysis (5)
apps/api/src/billing/services/balances/balances.service.ts (1)
packages/http-sdk/src/deployment/deployment-http.service.ts (1)
  • DeploymentInfo (103-103)
apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts (2)
apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (1)
  • SignAndBroadcastOptions (23-33)
apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts (1)
  • SyncSigningStargateClient (11-25)
apps/api/src/billing/providers/signing-client.provider.ts (1)
apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts (1)
  • SyncSigningStargateClient (11-25)
apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.spec.ts (5)
apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (1)
  • BatchSigningClientService (61-384)
apps/api/test/seeders/akash-address.seeder.ts (1)
  • createAkashAddress (4-12)
apps/api/src/billing/lib/wallet/wallet.ts (1)
  • Wallet (7-65)
apps/api/test/mocks/config-service.mock.ts (1)
  • mockConfigService (16-25)
apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts (1)
  • SyncSigningStargateClient (11-25)
apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (3)
apps/api/src/billing/lib/wallet/wallet.ts (1)
  • Wallet (7-65)
apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts (1)
  • SyncSigningStargateClient (11-25)
apps/api/src/core/services/tracing/tracing.service.ts (1)
  • withSpan (165-182)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
🔇 Additional comments (1)
apps/api/src/billing/services/balances/balances.service.ts (1)

87-92: LGTM! Clean refactor that improves readability.

Directly returning the reduce result eliminates an unnecessary intermediate variable, making the code more concise without changing behavior.

@codecov
Copy link

codecov bot commented Oct 30, 2025

Codecov Report

❌ Patch coverage is 86.72566% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 46.44%. Comparing base (557321a) to head (5935d9a).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...tch-signing-client/batch-signing-client.service.ts 87.50% 10 Missing ⚠️
apps/api/src/background-jobs-app.ts 0.00% 2 Missing ⚠️
...i/src/billing/providers/signing-client.provider.ts 33.33% 2 Missing ⚠️
apps/api/src/rest-app.ts 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2138      +/-   ##
==========================================
- Coverage   46.83%   46.44%   -0.40%     
==========================================
  Files        1014     1004      -10     
  Lines       28748    28372     -376     
  Branches     7460     7396      -64     
==========================================
- Hits        13465    13176     -289     
+ Misses      14887    14801      -86     
+ Partials      396      395       -1     
Flag Coverage Δ *Carryforward flag
api 81.99% <86.72%> (-0.05%) ⬇️
deploy-web 25.24% <ø> (ø) Carriedforward from 557321a
log-collector ?
notifications 88.11% <ø> (ø) Carriedforward from 557321a
provider-console 81.48% <ø> (ø) Carriedforward from 557321a
provider-proxy 85.28% <ø> (ø) Carriedforward from 557321a

*This pull request uses carry forward flags. Click here to find out more.

Files with missing lines Coverage Δ
...ng-stargate-client/sync-signing-stargate-client.ts 100.00% <100.00%> (ø)
.../src/billing/services/balances/balances.service.ts 96.29% <100.00%> (-0.07%) ⬇️
...pe-signing-client/dedupe-signing-client.service.ts 100.00% <100.00%> (ø)
.../services/managed-signer/managed-signer.service.ts 98.71% <100.00%> (ø)
apps/api/src/caching/helpers.ts 100.00% <100.00%> (ø)
...api/src/core/services/start-server/start-server.ts 96.77% <100.00%> (+0.10%) ⬆️
apps/api/src/rest-app.ts 93.33% <50.00%> (-0.99%) ⬇️
apps/api/src/background-jobs-app.ts 0.00% <0.00%> (ø)
...i/src/billing/providers/signing-client.provider.ts 80.00% <33.33%> (-8.24%) ⬇️
...tch-signing-client/batch-signing-client.service.ts 90.65% <87.50%> (+0.45%) ⬆️

... and 10 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

stalniy
stalniy previously approved these changes Oct 31, 2025
stalniy
stalniy previously approved these changes Oct 31, 2025
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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b0b6cf2 and 934bc09.

📒 Files selected for processing (7)
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.spec.ts (1 hunks)
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (1 hunks)
  • apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts (1 hunks)
  • apps/api/src/billing/providers/signing-client.provider.ts (3 hunks)
  • apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts (3 hunks)
  • apps/api/src/caching/helpers.spec.ts (2 hunks)
  • apps/api/src/caching/helpers.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/api/src/billing/providers/signing-client.provider.ts
  • apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Never use type any or cast to type any. Always define the proper TypeScript types.

Files:

  • apps/api/src/caching/helpers.ts
  • apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts
  • apps/api/src/caching/helpers.spec.ts
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.spec.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{js,ts,tsx}: Never use deprecated methods from libraries.
Don't add unnecessary comments to the code

Files:

  • apps/api/src/caching/helpers.ts
  • apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts
  • apps/api/src/caching/helpers.spec.ts
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.spec.ts
**/*.spec.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/no-jest-mock.mdc)

Don't use jest.mock() to mock dependencies in test files. Instead, use jest-mock-extended to create mocks and pass mocks as dependencies to the service under test.

**/*.spec.{ts,tsx}: Use setup function instead of beforeEach in test files
setup function must be at the bottom of the root describe block in test files
setup function creates an object under test and returns it
setup function should accept a single parameter with inline type definition
Don't use shared state in setup function
Don't specify return type of setup function

Files:

  • apps/api/src/caching/helpers.spec.ts
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.spec.ts
🧠 Learnings (3)
📓 Common learnings
Learnt from: baktun14
Repo: akash-network/console PR: 1725
File: apps/api/src/utils/constants.ts:5-5
Timestamp: 2025-07-24T17:00:52.361Z
Learning: In the Akash Network Console project, when cross-cutting concerns or broader refactoring issues are identified during PR review, the preferred approach is to create a separate GitHub issue to track the work rather than expanding the scope of the current PR. This maintains focus and allows for proper planning of architectural improvements.
📚 Learning: 2025-10-31T11:26:42.123Z
Learnt from: stalniy
Repo: akash-network/console PR: 2138
File: apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts:379-382
Timestamp: 2025-10-31T11:26:42.123Z
Learning: In TypeScript/JavaScript, the pattern of checking a cached value and then performing an async operation to fetch it without proper synchronization is race condition unsafe:
```typescript
private async getAddress() {
  if (!this.address) {
    this.address = await this.wallet.getFirstAddress();
  }
  return this.address;
}
```
Multiple concurrent calls can all pass the `if (!this.address)` check before any of them sets the value, leading to duplicate async operations. This should be flagged as a race condition. Proper synchronization (mutex, atomic promise caching, or guaranteed single-threaded execution) is required.

Applied to files:

  • apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts
  • apps/api/src/caching/helpers.spec.ts
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts
📚 Learning: 2025-07-24T17:00:52.361Z
Learnt from: baktun14
Repo: akash-network/console PR: 1725
File: apps/api/src/utils/constants.ts:5-5
Timestamp: 2025-07-24T17:00:52.361Z
Learning: In the Akash Network Console project, when cross-cutting concerns or broader refactoring issues are identified during PR review, the preferred approach is to create a separate GitHub issue to track the work rather than expanding the scope of the current PR. This maintains focus and allows for proper planning of architectural improvements.

Applied to files:

  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts
🧬 Code graph analysis (4)
apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts (2)
apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (1)
  • SignAndBroadcastOptions (24-34)
apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts (1)
  • SyncSigningStargateClient (11-25)
apps/api/src/caching/helpers.spec.ts (1)
apps/api/src/caching/helpers.ts (1)
  • memoizeAsync (113-133)
apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (5)
apps/api/src/billing/lib/wallet/wallet.ts (1)
  • Wallet (7-65)
apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts (1)
  • SyncSigningStargateClient (11-25)
apps/api/src/caching/helpers.ts (1)
  • memoizeAsync (113-133)
packages/logging/src/services/logger/logger.service.ts (1)
  • error (107-109)
apps/api/src/core/services/tracing/tracing.service.ts (1)
  • withSpan (165-182)
apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.spec.ts (5)
apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (1)
  • BatchSigningClientService (62-376)
apps/api/test/seeders/akash-address.seeder.ts (1)
  • createAkashAddress (4-12)
apps/api/src/billing/lib/wallet/wallet.ts (1)
  • Wallet (7-65)
apps/api/test/mocks/config-service.mock.ts (1)
  • mockConfigService (16-25)
apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts (1)
  • SyncSigningStargateClient (11-25)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
🔇 Additional comments (5)
apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts (1)

30-41: LGTM! The await fix resolves the critical disposal issue.

The addition of await on line 34 ensures that client.signAndBroadcast completes before the finally block executes, preventing premature disposal of the client. This addresses the race condition that was flagged in the previous review.

apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.spec.ts (1)

29-192: LGTM! Test suite follows coding guidelines and covers key scenarios.

The test suite properly:

  • Uses the setup function at the bottom of the describe block as per guidelines
  • Covers successful batching, error handling, and retry scenarios
  • Avoids shared state and follows the inline type definition pattern
  • Uses jest-mock-extended for mocking as required
apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (3)

119-129: LGTM! memoizeAsync resolves the race condition.

Using memoizeAsync for both getChainId and getAddress properly handles concurrent calls by caching the promise atomically. Multiple concurrent invocations will all receive the same promise, preventing duplicate async operations. This addresses the race condition concern from the previous review.

Based on learnings.


180-212: Well-structured batch signing and broadcasting flow.

The signAndBroadcast method properly:

  • Enqueues requests through the DataLoader for batching
  • Uses retry logic with exponential backoff for sequence mismatch errors
  • Converts chain errors to application errors via the error service
  • Retrieves and validates the final transaction result

The overall refactor introduces a robust batching mechanism with proper retry semantics.


322-328: Code is correct; review comment is factually incorrect.

CosmJS's broadcastTxSync returns a string containing the transaction hash, not a BroadcastTxSyncResponse object. The code correctly pushes this string directly to the results array on line 324, matching the behavior of broadcastTx which returns DeliverTxResponse.transactionHash (also a string) on line 327. No type inconsistency exists, and no fix is needed.

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

♻️ Duplicate comments (1)
apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (1)

323-329: Return hash string from broadcastTxSync, not the full response object.

Line 325 pushes the entire BroadcastTxSyncResponse object, but the return type is Result<string, unknown>[] expecting a transaction hash string. Downstream code (line 200) passes this to getTx(), which will fail when trying to query for a stringified object.

Extract and convert the hash to hex before returning:

       try {
         if (index < signResults.length - 1) {
           const response = await this.client.broadcastTxSync(txBytes);
-          results.push(Ok(response));
+          const txHash = toHex(response.hash);
+          results.push(Ok(txHash));
         } else {
           const lastDelivery = await this.client.broadcastTx(txBytes);
           results.push(Ok(lastDelivery.transactionHash));
         }
🧹 Nitpick comments (1)
apps/api/env/.env.functional.test (1)

40-42: Minor: Consider reordering for alphabetical consistency.

Static analysis flags that CONSOLE_WEB_PAYMENT_LINK (line 41) should precede FEATURE_FLAGS_ENABLE_ALL (line 40) for alphabetical ordering. While environment files don't enforce strict ordering semantically, aligning with this convention improves maintainability.

-FEATURE_FLAGS_ENABLE_ALL=true
 CONSOLE_WEB_PAYMENT_LINK=http://localhost:3000/payment
+FEATURE_FLAGS_ENABLE_ALL=true
 STD_OUT_LOG_FORMAT=pretty
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 934bc09 and f0d2600.

📒 Files selected for processing (3)
  • apps/api/env/.env.functional.test (1 hunks)
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (1 hunks)
  • apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Never use type any or cast to type any. Always define the proper TypeScript types.

Files:

  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts
  • apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{js,ts,tsx}: Never use deprecated methods from libraries.
Don't add unnecessary comments to the code

Files:

  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts
  • apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts
🧠 Learnings (3)
📓 Common learnings
Learnt from: baktun14
Repo: akash-network/console PR: 1725
File: apps/api/src/utils/constants.ts:5-5
Timestamp: 2025-07-24T17:00:52.361Z
Learning: In the Akash Network Console project, when cross-cutting concerns or broader refactoring issues are identified during PR review, the preferred approach is to create a separate GitHub issue to track the work rather than expanding the scope of the current PR. This maintains focus and allows for proper planning of architectural improvements.
📚 Learning: 2025-10-31T11:26:42.123Z
Learnt from: stalniy
Repo: akash-network/console PR: 2138
File: apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts:379-382
Timestamp: 2025-10-31T11:26:42.123Z
Learning: In TypeScript/JavaScript, the pattern of checking a cached value and then performing an async operation to fetch it without proper synchronization is race condition unsafe:
```typescript
private async getAddress() {
  if (!this.address) {
    this.address = await this.wallet.getFirstAddress();
  }
  return this.address;
}
```
Multiple concurrent calls can all pass the `if (!this.address)` check before any of them sets the value, leading to duplicate async operations. This should be flagged as a race condition. Proper synchronization (mutex, atomic promise caching, or guaranteed single-threaded execution) is required.

Applied to files:

  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts
  • apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts
📚 Learning: 2025-07-24T17:00:52.361Z
Learnt from: baktun14
Repo: akash-network/console PR: 1725
File: apps/api/src/utils/constants.ts:5-5
Timestamp: 2025-07-24T17:00:52.361Z
Learning: In the Akash Network Console project, when cross-cutting concerns or broader refactoring issues are identified during PR review, the preferred approach is to create a separate GitHub issue to track the work rather than expanding the scope of the current PR. This maintains focus and allows for proper planning of architectural improvements.

Applied to files:

  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts
  • apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts
🧬 Code graph analysis (1)
apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (5)
apps/api/src/billing/lib/wallet/wallet.ts (1)
  • Wallet (7-65)
apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts (1)
  • SyncSigningStargateClient (20-55)
apps/api/src/caching/helpers.ts (1)
  • memoizeAsync (113-133)
packages/logging/src/services/logger/logger.service.ts (1)
  • error (107-109)
apps/api/src/core/services/tracing/tracing.service.ts (1)
  • withSpan (165-182)
🪛 dotenv-linter (4.0.0)
apps/api/env/.env.functional.test

[warning] 41-41: [UnorderedKey] The CONSOLE_WEB_PAYMENT_LINK key should go before the FEATURE_FLAGS_ENABLE_ALL key

(UnorderedKey)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
🔇 Additional comments (15)
apps/api/env/.env.functional.test (2)

37-37: Disabling telemetry sampling for functional tests is correct.

Setting AMPLITUDE_SAMPLING=0 prevents test execution telemetry from polluting analytics data, which is a best practice for test environments.


42-42: Pretty-printed logging enhances test readability.

The addition of STD_OUT_LOG_FORMAT=pretty improves log output readability during functional test execution and aligns with the PR's enhanced logging configuration.

apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts (2)

29-33: LGTM!

The createWithEndpoint factory method correctly implements the new HTTP-based client creation flow. The synchronous instantiation pattern aligns with the confirmed behavior of Comet38Client.create and provides a clean, simplified interface for creating signing clients.


52-54: LGTM!

The connected() method provides a useful health check mechanism by invoking the status() RPC call. This allows callers to verify connectivity before performing operations, which is valuable for the batching workflow.

apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (11)

21-53: LGTM!

The new interfaces and type definitions are well-structured and properly documented. The CreateWithSignerFn type correctly expects a synchronous return, aligning with the createWithEndpoint implementation.


82-87: LGTM!

The DataLoader configuration is correct. Disabling cache is appropriate for transaction operations, and the batching interval provides a configurable window for collecting multiple requests.


95-98: LGTM!

The retry strategy for sequence mismatches is well-designed with appropriate backoff parameters. The string matching on "account sequence mismatch" is a practical approach given that chain errors are typically string-based.


106-109: LGTM!

The transaction retrieval retry logic appropriately accounts for blockchain confirmation latency with a longer max delay (7 seconds) compared to signing operations.


119-129: LGTM!

The memoized helpers correctly use memoizeAsync to cache both the chain ID and wallet address. This implementation properly addresses the race condition identified in previous reviews by caching the promise itself rather than checking and setting values separately. Based on learnings.


155-166: LGTM!

The constructor correctly uses the synchronous factory function to initialize the client, aligning with the createWithEndpoint implementation pattern.


180-212: LGTM!

The signAndBroadcast method correctly orchestrates the batching workflow with retry logic, proper error handling via chainErrorService, and transaction retrieval. The logging provides good observability into the signing lifecycle.


229-236: LGTM!

The semaphore-based serialization ensures that batches are processed sequentially, preventing sequence number conflicts. The try-finally block guarantees proper cleanup.


263-294: LGTM!

The batch signing implementation correctly manages sequence numbers by incrementing only after successful signing, preventing sequence gaps. The per-input error handling provides good isolation, allowing the batch to continue even if individual transactions fail.


353-355: LGTM!

The getTx method correctly uses the retry executor to handle cases where transactions are not immediately available after broadcasting.


369-376: LGTM!

The fee estimation logic correctly simulates transactions and applies a safety multiplier to prevent out-of-gas errors. The optional granter support integrates cleanly into the fee structure.

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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f0d2600 and e1280e2.

📒 Files selected for processing (3)
  • apps/api/src/billing/providers/signing-client.provider.ts (3 hunks)
  • apps/api/src/console.ts (1 hunks)
  • apps/api/src/core/services/start-server/start-server.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Never use type any or cast to type any. Always define the proper TypeScript types.

Files:

  • apps/api/src/console.ts
  • apps/api/src/billing/providers/signing-client.provider.ts
  • apps/api/src/core/services/start-server/start-server.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{js,ts,tsx}: Never use deprecated methods from libraries.
Don't add unnecessary comments to the code

Files:

  • apps/api/src/console.ts
  • apps/api/src/billing/providers/signing-client.provider.ts
  • apps/api/src/core/services/start-server/start-server.ts
🧠 Learnings (3)
📓 Common learnings
Learnt from: baktun14
Repo: akash-network/console PR: 1725
File: apps/api/src/utils/constants.ts:5-5
Timestamp: 2025-07-24T17:00:52.361Z
Learning: In the Akash Network Console project, when cross-cutting concerns or broader refactoring issues are identified during PR review, the preferred approach is to create a separate GitHub issue to track the work rather than expanding the scope of the current PR. This maintains focus and allows for proper planning of architectural improvements.
📚 Learning: 2025-08-21T04:03:23.490Z
Learnt from: stalniy
Repo: akash-network/console PR: 1827
File: apps/api/src/console.ts:146-154
Timestamp: 2025-08-21T04:03:23.490Z
Learning: The tsyringe container in the Akash Network console project has been extended with a custom dispose() method that iterates over all registered instances and calls dispose() on each one that implements it, enabling proper resource cleanup during shutdown.

Applied to files:

  • apps/api/src/console.ts
  • apps/api/src/core/services/start-server/start-server.ts
📚 Learning: 2025-08-21T04:03:23.490Z
Learnt from: stalniy
Repo: akash-network/console PR: 1827
File: apps/api/src/console.ts:146-154
Timestamp: 2025-08-21T04:03:23.490Z
Learning: In the Akash Network console project, services like JobQueueService implement dispose() methods for resource cleanup, and the tsyringe container has been extended with a dispose() method that iterates over all registered instances and calls their dispose() methods, enabling proper shutdown orchestration.

Applied to files:

  • apps/api/src/billing/providers/signing-client.provider.ts
  • apps/api/src/core/services/start-server/start-server.ts

Comment on lines +52 to +60
export const disposeSigningClients = async () => {
const clients = await Promise.all([
container.resolve<BatchSigningClientService>(MANAGED_MASTER_SIGNING_CLIENT),
container.resolve<BatchSigningClientService>(UAKT_TOP_UP_MASTER_SIGNING_CLIENT),
container.resolve<BatchSigningClientService>(USDC_TOP_UP_MASTER_SIGNING_CLIENT)
]);

await Promise.all(clients.map(client => client.dispose()));
};
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 | 🟠 Major

Dispose the real signing client instances rather than new ones

Each registration above still relies on container.register(..., { useFactory }) with the default Lifecycle.Transient. In tsyringe, that means every container.resolve(<token>) call produces a brand-new BatchSigningClientService. The disposeSigningClients() helper therefore constructs three fresh services, immediately disposes those, and never touches the live instances your consumers are holding—so the actual signing clients stay open.

Please either promote these registrations to Lifecycle.Singleton (so resolves share the same instance and container.dispose() works) or otherwise track the concrete instances when they are created, and dispose that set here.

A minimal fix:

-import { container, inject } from "tsyringe";
+import { container, inject, Lifecycle } from "tsyringe";
...
 container.register(MANAGED_MASTER_SIGNING_CLIENT, {
   useFactory: c =>
     new BatchSigningClientService(
       c.resolve(BillingConfigService),
       c.resolve(MANAGED_MASTER_WALLET),
       c.resolve(TYPE_REGISTRY),
       SyncSigningStargateClient.createWithEndpoint.bind(SyncSigningStargateClient),
       c.resolve(ChainErrorService),
       MANAGED_MASTER_SIGNING_CLIENT
     )
-});
+  ,
+  lifecycle: Lifecycle.Singleton
+});

Repeat for the other two registrations so disposeSigningClients() hits the actual cached instances.

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +126 to 130
const { disposeSigningClients } = require("./billing/providers/signing-client.provider");

await disposeSigningClients();
await Promise.all([closeConnections(), chainDb.close(), container.dispose()]);
});
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 | 🟠 Major

Protect the rest of shutdown when signing client disposal fails

Here too, any rejection from disposeSigningClients() aborts the subsequent Promise.all(...), leaving database connections and the container alive. Please swallow/log those errors but still run the remaining cleanup.

Consider:

-  await disposeSigningClients();
-  await Promise.all([closeConnections(), chainDb.close(), container.dispose()]);
+  try {
+    await disposeSigningClients();
+  } catch (error) {
+    logger.error({ event: "SIGNING_CLIENT_DISPOSE_ERROR", error });
+  }
+
+  await Promise.all([closeConnections(), chainDb.close(), container.dispose()]);
📝 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 { disposeSigningClients } = require("./billing/providers/signing-client.provider");
await disposeSigningClients();
await Promise.all([closeConnections(), chainDb.close(), container.dispose()]);
});
const { disposeSigningClients } = require("./billing/providers/signing-client.provider");
try {
await disposeSigningClients();
} catch (error) {
logger.error({ event: "SIGNING_CLIENT_DISPOSE_ERROR", error });
}
await Promise.all([closeConnections(), chainDb.close(), container.dispose()]);
});
🤖 Prompt for AI Agents
In apps/api/src/console.ts around lines 126 to 130, calling await
disposeSigningClients() can reject and abort the following Promise.all cleanup;
wrap the disposeSigningClients() call in a try/catch, log the caught error
(using the existing logger or console.error) and do not rethrow so the code
proceeds to await Promise.all([closeConnections(), chainDb.close(),
container.dispose()]) to ensure DBs and container are always cleaned up.

Comment on lines 32 to 36
return disposeSigningClients()
.then(() => container.dispose())
.catch(error => {
logger.error({ event: "CONTAINER_DISPOSE_ERROR", error });
});
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 | 🟠 Major

Ensure container disposal still runs when signing client cleanup fails

If disposeSigningClients() rejects, the current .then(() => container.dispose()) never executes, so the container (and everything registered on it) remains undisposed. That’s a major reliability regression: a transient issue in one signing client would leak DB pools, queue workers, etc., and block shutdown. Please run container disposal in a finally-style block so it always executes, and log signing-client failures separately.

Apply this diff:

-  const disposeContainerOnce = once(() => {
-    logger.info({ event: "DISPOSING_CONTAINER" });
-    return disposeSigningClients()
-      .then(() => container.dispose())
-      .catch(error => {
-        logger.error({ event: "CONTAINER_DISPOSE_ERROR", error });
-      });
-  });
+  const disposeContainerOnce = once(async () => {
+    logger.info({ event: "DISPOSING_CONTAINER" });
+
+    try {
+      await disposeSigningClients();
+    } catch (error) {
+      logger.error({ event: "SIGNING_CLIENT_DISPOSE_ERROR", error });
+    }
+
+    try {
+      await container.dispose();
+    } catch (error) {
+      logger.error({ event: "CONTAINER_DISPOSE_ERROR", error });
+    }
+  });
🤖 Prompt for AI Agents
In apps/api/src/core/services/start-server/start-server.ts around lines 32 to
36, the current chain stops container.dispose() if disposeSigningClients()
rejects; change it to always run container.dispose() in a finally-style step:
call disposeSigningClients(), catch and log its error separately, then in a
finally handler run container.dispose() (and catch/log any error from
container.dispose() as well) so container disposal is guaranteed even when
signing-client cleanup fails.

* @throws If the connection cannot be established.
*/
async connected() {
await this.forceGetCometClient().status();
Copy link
Contributor

Choose a reason for hiding this comment

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

question(non-blocking): is this race condition safe?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

what race conditions do you mean?

Copy link
Contributor

Choose a reason for hiding this comment

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

if this method is called by many callers, everybody will try to get status. So, instead of one there will be many calls to this.forceGetCometClient().status()

stalniy
stalniy previously approved these changes Nov 1, 2025
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: 3

Caution

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

⚠️ Outside diff range comments (1)
apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts (1)

44-62: Race condition: concurrent getClient calls can create duplicate clients.

Multiple concurrent calls with the same key can all pass the !this.clientsByAddress.has(key) check before any of them sets the value, resulting in multiple client instances being created. The later set calls overwrite earlier ones, potentially leaking resources and violating the deduplication contract.

Based on learnings.

Apply proper synchronization, such as:

  • A mutex/lock around the check-and-create block
  • Atomic promise caching (store the creation promise, not just the result)
  • A semaphore or similar construct to serialize getClient per key

Example with promise caching:

+private readonly clientCreationPromises: Map<string, Promise<CachedClient>> = new Map();
+
 private getClient(mnemonic: string, addressIndex?: number): CachedClient {
   const key = `${crypto.createHash("sha256").update(mnemonic).digest("hex")}/${addressIndex ?? 0}`;
   
-  if (!this.clientsByAddress.has(key)) {
-    this.logger.debug({ event: "DEDUPE_SIGNING_CLIENT_CREATE", key });
-    this.clientsByAddress.set(key, {
-      key: key,
-      client: new BatchSigningClientService(
-        this.config,
-        new Wallet(mnemonic, addressIndex),
-        this.registry,
-        SyncSigningStargateClient.createWithEndpoint.bind(SyncSigningStargateClient),
-        this.chainErrorService
-      )
-    });
-  }
+  if (!this.clientsByAddress.has(key)) {
+    if (!this.clientCreationPromises.has(key)) {
+      const promise = this.createClient(key, mnemonic, addressIndex);
+      this.clientCreationPromises.set(key, promise);
+      promise.finally(() => this.clientCreationPromises.delete(key));
+    }
+    await this.clientCreationPromises.get(key);
+  }
   
   return this.clientsByAddress.get(key)!;
 }
+
+private async createClient(key: string, mnemonic: string, addressIndex?: number): Promise<CachedClient> {
+  this.logger.debug({ event: "DEDUPE_SIGNING_CLIENT_CREATE", key });
+  const cached = {
+    key: key,
+    client: new BatchSigningClientService(
+      this.config,
+      new Wallet(mnemonic, addressIndex),
+      this.registry,
+      SyncSigningStargateClient.createWithEndpoint.bind(SyncSigningStargateClient),
+      this.chainErrorService
+    )
+  };
+  this.clientsByAddress.set(key, cached);
+  return cached;
+}

Note: This requires making getClient async and updating its callers.

♻️ Duplicate comments (1)
apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (1)

320-329: Return a hash string for broadcastTxSync, not the whole response.

Right now we push Ok(response) where response is a BroadcastTxSyncResponse. When signAndBroadcast later calls getTx(result.val), it stringifies to "[object Object]", every lookup fails, and we throw “Failed to sign and broadcast transaction.” Convert the sync response into the actual hash (and treat non‑zero codes as errors) before wrapping it.

-          const response = await this.client.broadcastTxSync(txBytes);
-          results.push(Ok(response));
+          const response = await this.client.broadcastTxSync(txBytes);
+          if (response.code && response.code !== 0) {
+            results.push(Err(new Error(response.log ?? "broadcastTxSync failed")));
+            continue;
+          }
+          const txHash = toHex(response.hash);
+          results.push(Ok(txHash));
🧹 Nitpick comments (1)
apps/api/env/.env.functional.test (1)

40-41: Fix key ordering to comply with static analysis.

The static analysis tool flagged that CONSOLE_WEB_PAYMENT_LINK (line 41) should appear before FEATURE_FLAGS_ENABLE_ALL (line 40) in alphabetical order.

Apply this diff to fix the ordering:

-FEATURE_FLAGS_ENABLE_ALL=true
-CONSOLE_WEB_PAYMENT_LINK=http://localhost:3000/payment
+CONSOLE_WEB_PAYMENT_LINK=http://localhost:3000/payment
+FEATURE_FLAGS_ENABLE_ALL=true
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e1280e2 and 5935d9a.

📒 Files selected for processing (14)
  • apps/api/env/.env.functional.test (1 hunks)
  • apps/api/src/background-jobs-app.ts (1 hunks)
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.spec.ts (1 hunks)
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (1 hunks)
  • apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts (2 hunks)
  • apps/api/src/billing/providers/signing-client.provider.ts (3 hunks)
  • apps/api/src/billing/services/balances/balances.service.ts (1 hunks)
  • apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts (3 hunks)
  • apps/api/src/billing/services/managed-signer/managed-signer.service.ts (1 hunks)
  • apps/api/src/caching/helpers.spec.ts (2 hunks)
  • apps/api/src/caching/helpers.ts (1 hunks)
  • apps/api/src/console.ts (1 hunks)
  • apps/api/src/core/services/start-server/start-server.ts (2 hunks)
  • apps/api/src/rest-app.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • apps/api/src/core/services/start-server/start-server.ts
  • apps/api/src/caching/helpers.ts
  • apps/api/src/console.ts
  • apps/api/src/billing/services/balances/balances.service.ts
  • apps/api/src/rest-app.ts
  • apps/api/src/billing/providers/signing-client.provider.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Never use type any or cast to type any. Always define the proper TypeScript types.

Files:

  • apps/api/src/background-jobs-app.ts
  • apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts
  • apps/api/src/billing/services/managed-signer/managed-signer.service.ts
  • apps/api/src/caching/helpers.spec.ts
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.spec.ts
  • apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

**/*.{js,ts,tsx}: Never use deprecated methods from libraries.
Don't add unnecessary comments to the code

Files:

  • apps/api/src/background-jobs-app.ts
  • apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts
  • apps/api/src/billing/services/managed-signer/managed-signer.service.ts
  • apps/api/src/caching/helpers.spec.ts
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.spec.ts
  • apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts
**/*.spec.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/no-jest-mock.mdc)

Don't use jest.mock() to mock dependencies in test files. Instead, use jest-mock-extended to create mocks and pass mocks as dependencies to the service under test.

**/*.spec.{ts,tsx}: Use setup function instead of beforeEach in test files
setup function must be at the bottom of the root describe block in test files
setup function creates an object under test and returns it
setup function should accept a single parameter with inline type definition
Don't use shared state in setup function
Don't specify return type of setup function

Files:

  • apps/api/src/caching/helpers.spec.ts
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.spec.ts
🧠 Learnings (4)
📓 Common learnings
Learnt from: baktun14
Repo: akash-network/console PR: 1725
File: apps/api/src/utils/constants.ts:5-5
Timestamp: 2025-07-24T17:00:52.361Z
Learning: In the Akash Network Console project, when cross-cutting concerns or broader refactoring issues are identified during PR review, the preferred approach is to create a separate GitHub issue to track the work rather than expanding the scope of the current PR. This maintains focus and allows for proper planning of architectural improvements.
📚 Learning: 2025-08-21T04:03:23.490Z
Learnt from: stalniy
Repo: akash-network/console PR: 1827
File: apps/api/src/console.ts:146-154
Timestamp: 2025-08-21T04:03:23.490Z
Learning: In the Akash Network console project, services like JobQueueService implement dispose() methods for resource cleanup, and the tsyringe container has been extended with a dispose() method that iterates over all registered instances and calls their dispose() methods, enabling proper shutdown orchestration.

Applied to files:

  • apps/api/src/background-jobs-app.ts
📚 Learning: 2025-10-31T11:26:42.123Z
Learnt from: stalniy
Repo: akash-network/console PR: 2138
File: apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts:379-382
Timestamp: 2025-10-31T11:26:42.123Z
Learning: In TypeScript/JavaScript, the pattern of checking a cached value and then performing an async operation to fetch it without proper synchronization is race condition unsafe:
```typescript
private async getAddress() {
  if (!this.address) {
    this.address = await this.wallet.getFirstAddress();
  }
  return this.address;
}
```
Multiple concurrent calls can all pass the `if (!this.address)` check before any of them sets the value, leading to duplicate async operations. This should be flagged as a race condition. Proper synchronization (mutex, atomic promise caching, or guaranteed single-threaded execution) is required.

Applied to files:

  • apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts
  • apps/api/src/caching/helpers.spec.ts
  • apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts
📚 Learning: 2025-07-24T17:00:52.361Z
Learnt from: baktun14
Repo: akash-network/console PR: 1725
File: apps/api/src/utils/constants.ts:5-5
Timestamp: 2025-07-24T17:00:52.361Z
Learning: In the Akash Network Console project, when cross-cutting concerns or broader refactoring issues are identified during PR review, the preferred approach is to create a separate GitHub issue to track the work rather than expanding the scope of the current PR. This maintains focus and allows for proper planning of architectural improvements.

Applied to files:

  • apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts
🧬 Code graph analysis (5)
apps/api/src/background-jobs-app.ts (3)
apps/api/src/rest-app.ts (1)
  • bootstrap (201-207)
apps/api/src/core/services/start-server/start-server.ts (1)
  • startServer (18-66)
apps/api/src/billing/providers/signing-client.provider.ts (1)
  • disposeSigningClients (52-60)
apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (5)
apps/api/src/billing/lib/wallet/wallet.ts (1)
  • Wallet (7-65)
apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts (1)
  • SyncSigningStargateClient (20-55)
apps/api/src/caching/helpers.ts (1)
  • memoizeAsync (113-133)
packages/logging/src/services/logger/logger.service.ts (2)
  • LoggerService (39-183)
  • error (142-144)
apps/api/src/core/services/tracing/tracing.service.ts (1)
  • withSpan (165-182)
apps/api/src/caching/helpers.spec.ts (1)
apps/api/src/caching/helpers.ts (1)
  • memoizeAsync (113-133)
apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.spec.ts (4)
apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (1)
  • BatchSigningClientService (62-377)
apps/api/test/seeders/akash-address.seeder.ts (1)
  • createAkashAddress (4-12)
apps/api/src/billing/lib/wallet/wallet.ts (1)
  • Wallet (7-65)
apps/api/test/mocks/config-service.mock.ts (1)
  • mockConfigService (16-25)
apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts (2)
apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (1)
  • SignAndBroadcastOptions (24-34)
apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts (1)
  • SyncSigningStargateClient (20-55)
🪛 dotenv-linter (4.0.0)
apps/api/env/.env.functional.test

[warning] 41-41: [UnorderedKey] The CONSOLE_WEB_PAYMENT_LINK key should go before the FEATURE_FLAGS_ENABLE_ALL key

(UnorderedKey)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
🔇 Additional comments (6)
apps/api/env/.env.functional.test (2)

37-37: Amplitude sampling disabled for functional tests.

Disabling amplitude sampling (set to 0) is appropriate for a functional test environment, as it prevents telemetry events from being generated during test execution. This change looks good.


42-42: New log format setting improves test readability.

Adding STD_OUT_LOG_FORMAT=pretty enhances the readability of functional test logs, which supports better debugging and easier interpretation of test output.

apps/api/src/background-jobs-app.ts (1)

6-6: LGTM! Import follows established patterns.

The import is necessary for resource cleanup and matches the pattern used in rest-app.ts.

apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts (3)

6-6: LGTM: Import updated to match new API.

The import change from ExecuteTxOptions to SignAndBroadcastOptions correctly aligns with the refactored batch signing client interface.


34-34: Good fix: await prevents premature disposal.

Adding await ensures the broadcast completes before the finally block runs, correctly addressing the previous review comment.


55-55: LGTM: Client factory binding updated to new API.

The change from connectWithSigner to createWithEndpoint correctly aligns with the refactored SyncSigningStargateClient interface, as shown in the relevant code snippets.


app.route("/", healthzRouter);
const server = await startServer(app, LoggerService.forContext("BACKGROUND_JOBS"), process, { port });
const server = await startServer(app, LoggerService.forContext("BACKGROUND_JOBS"), process, { port, beforeEnd: () => disposeSigningClients() });
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 | 🔴 Critical

Critical: async disposal callback is not awaited, risking incomplete cleanup.

The disposeSigningClients function is async and performs resource cleanup, but the startServer implementation in apps/api/src/core/services/start-server/start-server.ts (line 33) calls options?.beforeEnd?.() without awaiting it. This means the server may shut down before the signing clients are fully disposed, potentially causing resource leaks or orphaned connections.

The startServer implementation should be updated to await the beforeEnd callback:

await options?.beforeEnd?.();

This issue affects both this file and rest-app.ts which uses the same pattern. Based on learnings, proper disposal is critical for resource cleanup in this project.

🤖 Prompt for AI Agents
In apps/api/src/background-jobs-app.ts around line 14, the async disposal
callback disposeSigningClients is passed to startServer but startServer
currently invokes options?.beforeEnd?.() without awaiting it; update the
start-server implementation
(apps/api/src/core/services/start-server/start-server.ts, around line 33) to
await the callback (i.e., await options?.beforeEnd?.()) so disposal completes
before shutdown, and apply the same change for the identical pattern used in
rest-app.ts to ensure all async cleanup is awaited.

Comment on lines 36 to 40
if (!client.hasPendingTransactions && this.clientsByAddress.has(key)) {
this.logger.debug({ event: "DEDUPE_SIGNING_CLIENT_CLEAN_UP", key });
this.clientsByAddress.delete(key);
client.dispose();
}
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 | 🔴 Critical

🧩 Analysis chain

Potential race condition in concurrent cleanup.

If executeManagedTx is called concurrently with the same key, multiple executions share the same client instance. When one finishes and enters the finally block, it could delete from the map and dispose the client while another concurrent call is about to use the same client reference. The check !client.hasPendingTransactions may not provide sufficient synchronization against this race.

Consider:

  1. Call A finishes transaction, checks hasPendingTransactions (false), proceeds to delete/dispose
  2. Call B starts with same key, calls getClient, receives the same client reference
  3. Call A disposes the client
  4. Call B attempts signAndBroadcast on the disposed client

Proper synchronization (mutex, promise coordination, or reference counting) would prevent disposing a client while it's being acquired or used by concurrent callers.

Run the following script to check if there are concurrent usage patterns that could trigger this race:


🏁 Script executed:

#!/bin/bash
# Search for concurrent or batched calls to executeManagedTx
rg -nP -C5 'executeManagedTx' --type=ts -g '!*.spec.ts' -g '!*.test.ts'

Length of output: 11803


I'll examine the full implementation of the dedupe-signing-client service and search for concurrent usage patterns.



---

🏁 Script executed:

```shell
#!/bin/bash
# Get the full dedupe-signing-client.service.ts file to see the complete finally block
cat apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts

Length of output: 2483


🏁 Script executed:

#!/bin/bash
# Search for Promise.all or batch patterns with executeManagedTx
rg -nP 'Promise\.all.*executeManagedTx|map.*executeManagedTx' --type=ts -g '!*.spec.ts' -g '!*.test.ts' -C3

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if there are any concurrent/parallel patterns when calling executeManagedTx
rg -nP 'Promise\.(all|race|any)|forkJoin|combineLatest' --type=ts apps/api/src/deployment/services -C3 | head -50

Length of output: 6303


Let me examine the BatchSigningClientService to understand the hasPendingTransactions and dispose behavior:


🏁 Script executed:

#!/bin/bash
# View the BatchSigningClientService implementation
cat apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts

Length of output: 13991


🏁 Script executed:

#!/bin/bash
# Search for how executeManagedTx is actually called - check if concurrent calls with same key are possible
rg -nP 'executeManagedTx.*walletIndex|walletId' --type=ts apps/api/src/billing/services apps/api/src/deployment/services -C3 | head -80

Length of output: 9820


Race condition confirmed: multiple concurrent executeManagedTx calls share the same client instance, and concurrent finally blocks race on disposal.

When top-up-managed-deployments.service.ts calls Promise.allSettled on multiple owners with the same walletId, concurrent executeManagedTx calls with identical mnemonic and walletIndex retrieve the same cached client from clientsByAddress. Both calls' signAndBroadcast are batched by the DataLoader, and when the batch completes, their finally blocks execute concurrently. Both check !client.hasPendingTransactions (true, batch done) and clientsByAddress.has(key) (true), causing a race where one deletes and disposes while the other still holds a reference or is about to use it.

Proper synchronization (mutex, reference counting, or preventing concurrent acquisition per key) is required before disposing.

Comment on lines +349 to +356
const fn = jest.fn().mockResolvedValue("cached");
const memoized = memoizeAsync(fn);

await memoized();
const result = memoized();

expect(result).resolves.toBe("cached");
expect(fn).toHaveBeenCalledTimes(1);
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 | 🟠 Major

Await the .resolves assertion so Jest actually checks it.

Without await, the promise returned by expect(result).resolves may resolve after the test finishes, giving false positives or unhandled rejections. Await the assertion (or return it) to keep the test reliable.

-      expect(result).resolves.toBe("cached");
+      await expect(result).resolves.toBe("cached");
📝 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 fn = jest.fn().mockResolvedValue("cached");
const memoized = memoizeAsync(fn);
await memoized();
const result = memoized();
expect(result).resolves.toBe("cached");
expect(fn).toHaveBeenCalledTimes(1);
const fn = jest.fn().mockResolvedValue("cached");
const memoized = memoizeAsync(fn);
await memoized();
const result = memoized();
await expect(result).resolves.toBe("cached");
expect(fn).toHaveBeenCalledTimes(1);
🤖 Prompt for AI Agents
In apps/api/src/caching/helpers.spec.ts around lines 349 to 356, the test uses
expect(result).resolves.toBe("cached") but does not await or return that
assertion; update the test to await the resolves assertion (e.g., await
expect(result).resolves.toBe("cached")) so Jest waits for the promise resolution
and the assertion is actually executed, keeping the test reliable and preventing
false positives.

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

Comments