Skip to content

feat(billing): uses ws transport for signer implementation#2124

Closed
ygrishajev wants to merge 2 commits intomainfrom
feature/billing
Closed

feat(billing): uses ws transport for signer implementation#2124
ygrishajev wants to merge 2 commits intomainfrom
feature/billing

Conversation

@ygrishajev
Copy link
Contributor

@ygrishajev ygrishajev commented Oct 28, 2025

Summary by CodeRabbit

  • Bug Fixes

    • Improved explicit cleanup of signing client connections to prevent resource leaks and better handle sequence mismatches.
  • Chores

    • Updated RPC endpoint used in functional test environment.
  • Refactor

    • Switched signing client to eager initialization with explicit lifecycle controls (disconnect/dispose) and moved transport to a websocket-based implementation for more reliable connections.

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

coderabbitai bot commented Oct 28, 2025

Walkthrough

Switches a functional test RPC endpoint; replaces SyncSigningStargateClient connect/create API with a new static init that uses WebsocketClient + Comet38Client; BatchSigningClientService now constructs a concrete SyncSigningStargateClient via create/init, adds disconnect()/dispose(), and DedupeSigningClientService calls client.disconnect() during cleanup.

Changes

Cohort / File(s) Summary
Configuration & Environment
apps/api/env/.env.functional.test
RPC node endpoint updated: old URL commented out and RPC_NODE_ENDPOINT set to https://rpc.sandbox.akt.dev/rpc.
Batch Signing Client (lifecycle & API)
apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts
Replaced ConnectWithSignerFn → CreateWithSignerFn (returns concrete SyncSigningStargateClient), constructor now accepts createWithSigner and initializes client via createWithSigner/init; clientAsPromised removed and replaced with client: SyncSigningStargateClient; added disconnect() and dispose(); removed cached-account helper methods and related fields; adjusted all client call sites to use this.client; sequence-mismatch handling simplified (no client reinit).
Sync Signing Stargate Client (transport & API)
apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts
Replaced connectWithSigner/createWithSigner API with static init(endpoint: string, signer, options); removed HttpEndpoint/connectComet usage and now constructs new WebsocketClient(endpoint) + Comet38Client.create(...) before building the signing client.
Dedupe Signing Client Cleanup
apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts
In executeManagedTx cleanup, explicitly await client.disconnect() before removing the client entry; changed binding to use SyncSigningStargateClient.init.bind(...) when constructing BatchSigningClientService.
Provider bindings
apps/api/src/billing/providers/signing-client.provider.ts
Replaced uses of SyncSigningStargateClient.connectWithSigner with SyncSigningStargateClient.init in container factory registrations.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant DedupeService as DedupeSigningClientService
    participant BatchSvc as BatchSigningClientService
    participant SyncClient as SyncSigningStargateClient
    participant Transport as WebsocketClient/Comet38

    Caller->>DedupeService: executeManagedTx(tx...)
    DedupeService->>BatchSvc: request executeTxBatch / simulate
    BatchSvc->>BatchSvc: constructor called with createWithSigner
    BatchSvc->>SyncClient: createWithSigner/init(...)
    SyncClient->>Transport: new WebsocketClient -> Comet38Client.create()
    Transport-->>SyncClient: cometClient
    SyncClient-->>BatchSvc: SyncSigningStargateClient instance
    BatchSvc->>SyncClient: simulate/submit tx(s)
    SyncClient-->>BatchSvc: result / error
    BatchSvc-->>DedupeService: result

    Note over DedupeService,BatchSvc: cleanup path
    DedupeService->>BatchSvc: disconnect() / dispose()
    BatchSvc->>SyncClient: disconnect()
    SyncClient->>Transport: close websocket
    Transport-->>SyncClient: closed
    SyncClient-->>BatchSvc: disconnected
    DedupeService->>DedupeService: remove client entry
    DedupeService-->>Caller: final result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Review SyncSigningStargateClient.init transport construction (WebsocketClient + Comet38Client) for parity with previous connectComet behavior and options handling.
  • Inspect BatchSigningClientService constructor/init, lifecycle (disconnect/dispose), and places where cached account logic was removed to ensure no regressions/race conditions.
  • Validate DedupeSigningClientService cleanup changes (awaiting disconnect) for potential blocking or ordering issues in high-concurrency scenarios.

Possibly related PRs

Suggested reviewers

  • baktun14

Poem

🐇 I nudge the socket, hum the wire,

Init the client, spark the fire.
Disconnect tidy, endpoints hopped,
Comet sails and queues are popped.
A rabbit's hop — the deploy is lighter.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "feat(billing): uses ws transport for signer implementation" is directly related to a key technical change in the pull request. The changes to sync-signing-stargate-client.ts explicitly replace HTTP transport (connectComet) with WebSocket transport (WebsocketClient and Comet38Client.create), which aligns precisely with the title's stated objective. The title is specific, concise, and clearly communicates the primary implementation shift at the transport layer that drives the subsequent refactoring across the billing client services. While the PR includes additional substantial changes such as removing caching mechanisms and refactoring client lifecycle management, the transport switch is the foundational architectural change that enables these modifications.
✨ 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: 0

🧹 Nitpick comments (2)
apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts (1)

36-41: Consider error handling for disconnect() in the finally block.

The disconnect() call in the finally block is correct for resource cleanup. However, if disconnect() throws an exception, it could mask the original error from the try block or cause unexpected behavior.

Apply this diff to add defensive error handling:

     } finally {
       if (!client.hasPendingTransactions && this.clientsByAddress.has(key)) {
         this.logger.debug({ event: "DEDUPE_SIGNING_CLIENT_CLEAN_UP", key });
         this.clientsByAddress.delete(key);
-        await client.disconnect();
+        try {
+          await client.disconnect();
+        } catch (error) {
+          this.logger.error({ event: "DEDUPE_SIGNING_CLIENT_DISCONNECT_ERROR", key, error });
+        }
       }
     }
apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (1)

85-92: Consider adding error handling to the disconnect methods.

The disconnect() method is confirmed to exist on SigningStargateClient (inherited from StargateClient in @cosmjs/stargate), so the implementation is correct. However, since these methods are called during cleanup, wrapping the disconnect call in a try-catch block would prevent errors from propagating and allow graceful degradation:

async disconnect() {
  try {
    const client = await this.clientAsPromised;
    client.disconnect();
  } catch (error) {
    // Log error
  }
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cc8d5a6 and 249971b.

📒 Files selected for processing (4)
  • 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 (1 hunks)
  • apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts (1 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/services/dedupe-signing-client/dedupe-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/services/dedupe-signing-client/dedupe-signing-client.service.ts
  • apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts
🪛 dotenv-linter (4.0.0)
apps/api/env/.env.functional.test

[warning] 6-6: [UnorderedKey] The RPC_NODE_ENDPOINT key should go before the UAKT_TOP_UP_MASTER_WALLET_MNEMONIC 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 (3)
apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts (2)

11-19: Verified: The endpoint parameter type change does not break any existing code.

All call sites already pass string values (RPC endpoint URLs), and no code uses the HttpEndpoint type. The narrowing from string | HttpEndpoint to string is safe.


16-18: Consider potential connection initialization timing issues.

The transport implementation changed from connectComet() (which likely established connection) to constructing WebsocketClient and wrapping it with Comet38Client.create().

The WebsocketClient constructor is synchronous, and the connection might not be established until the first RPC call. This could lead to:

  1. Delayed error detection if the endpoint is unreachable
  2. First RPC call latency including connection establishment
apps/api/env/.env.functional.test (1)

5-6: RPC endpoint configuration verified successfully.

The endpoint https://rpc.sandbox.akt.dev/rpc is accessible and responds correctly to RPC health checks. The CosmJS WebsocketClient library automatically converts https:// URLs to wss:// (WebSocket Secure) protocol internally, so the configuration is correct as-is.

@codecov
Copy link

codecov bot commented Oct 28, 2025

❌ 13 Tests Failed:

Tests completed Failed Passed Skipped
1470 13 1457 0
View the top 3 failed test(s) by shortest run time
 Test execution failure: could be caused by test hooks like 'afterAll'.
Stack Traces | 0s run time
{"message":"","stack":"Error: thrown: \"Connection attempt timed out after 10246 ms\"\n    at _getError (.../jest-circus/build/utils.js:432:18)\n    at Array.map (<anonymous>)\n    at makeRunResult (.../jest-circus/build/utils.js:346:36)\n    at run (.../jest-circus/build/run.js:75:35)\n    at processTicksAndRejections (node:internal/process/task_queues:105:5)\n    at runAndTransformResultsToJestFormat (.../build/legacy-code-todo-rewrite/jestAdapterInit.js:122:21)\n    at jestAdapter (.../build/legacy-code-todo-rewrite/jestAdapter.js:79:19)\n    at runTestInternal (.../jest-runner/build/runTest.js:367:16)\n    at runTest (.../jest-runner/build/runTest.js:444:34)\n    at Object.worker (.../jest-runner/build/testWorker.js:106:12)"}
 Test execution failure: could be caused by test hooks like 'afterAll'.
Stack Traces | 0s run time
{"message":"","stack":"Error: thrown: \"Connection attempt timed out after 14773 ms\"\n    at _getError (.../jest-circus/build/utils.js:432:18)\n    at Array.map (<anonymous>)\n    at makeRunResult (.../jest-circus/build/utils.js:346:36)\n    at run (.../jest-circus/build/run.js:75:35)\n    at processTicksAndRejections (node:internal/process/task_queues:105:5)\n    at runAndTransformResultsToJestFormat (.../build/legacy-code-todo-rewrite/jestAdapterInit.js:122:21)\n    at jestAdapter (.../build/legacy-code-todo-rewrite/jestAdapter.js:79:19)\n    at runTestInternal (.../jest-runner/build/runTest.js:367:16)\n    at runTest (.../jest-runner/build/runTest.js:444:34)\n    at Object.worker (.../jest-runner/build/testWorker.js:106:12)\nError: thrown: \"Connection attempt timed out after 14773 ms\"\n    at _getError (.../jest-circus/build/utils.js:432:18)\n    at Array.map (<anonymous>)\n    at makeRunResult (.../jest-circus/build/utils.js:346:36)\n    at run (.../jest-circus/build/run.js:75:35)\n    at processTicksAndRejections (node:internal/process/task_queues:105:5)\n    at runAndTransformResultsToJestFormat (.../build/legacy-code-todo-rewrite/jestAdapterInit.js:122:21)\n    at jestAdapter (.../build/legacy-code-todo-rewrite/jestAdapter.js:79:19)\n    at runTestInternal (.../jest-runner/build/runTest.js:367:16)\n    at runTest (.../jest-runner/build/runTest.js:444:34)\n    at Object.worker (.../jest-runner/build/testWorker.js:106:12)"}
 Test execution failure: could be caused by test hooks like 'afterAll'.
Stack Traces | 0s run time
{"message":"","stack":"Error: thrown: \"Connection attempt timed out after 18250 ms\"\n    at _getError (.../jest-circus/build/utils.js:432:18)\n    at Array.map (<anonymous>)\n    at makeRunResult (.../jest-circus/build/utils.js:346:36)\n    at run (.../jest-circus/build/run.js:75:35)\n    at processTicksAndRejections (node:internal/process/task_queues:105:5)\n    at runAndTransformResultsToJestFormat (.../build/legacy-code-todo-rewrite/jestAdapterInit.js:122:21)\n    at jestAdapter (.../build/legacy-code-todo-rewrite/jestAdapter.js:79:19)\n    at runTestInternal (.../jest-runner/build/runTest.js:367:16)\n    at runTest (.../jest-runner/build/runTest.js:444:34)\n    at Object.worker (.../jest-runner/build/testWorker.js:106:12)\nError: thrown: \"Connection attempt timed out after 18249 ms\"\n    at _getError (.../jest-circus/build/utils.js:432:18)\n    at Array.map (<anonymous>)\n    at makeRunResult (.../jest-circus/build/utils.js:346:36)\n    at run (.../jest-circus/build/run.js:75:35)\n    at processTicksAndRejections (node:internal/process/task_queues:105:5)\n    at runAndTransformResultsToJestFormat (.../build/legacy-code-todo-rewrite/jestAdapterInit.js:122:21)\n    at jestAdapter (.../build/legacy-code-todo-rewrite/jestAdapter.js:79:19)\n    at runTestInternal (.../jest-runner/build/runTest.js:367:16)\n    at runTest (.../jest-runner/build/runTest.js:444:34)\n    at Object.worker (.../jest-runner/build/testWorker.js:106:12)"}
 Test execution failure: could be caused by test hooks like 'afterAll'.
Stack Traces | 0s run time
{"message":"","stack":"Error: thrown: \"Connection attempt timed out after 11794 ms\"\n    at _getError (.../jest-circus/build/utils.js:432:18)\n    at Array.map (<anonymous>)\n    at makeRunResult (.../jest-circus/build/utils.js:346:36)\n    at run (.../jest-circus/build/run.js:75:35)\n    at processTicksAndRejections (node:internal/process/task_queues:105:5)\n    at runAndTransformResultsToJestFormat (.../build/legacy-code-todo-rewrite/jestAdapterInit.js:122:21)\n    at jestAdapter (.../build/legacy-code-todo-rewrite/jestAdapter.js:79:19)\n    at runTestInternal (.../jest-runner/build/runTest.js:367:16)\n    at runTest (.../jest-runner/build/runTest.js:444:34)\n    at Object.worker (.../jest-runner/build/testWorker.js:106:12)\nError: thrown: \"Connection attempt timed out after 11792 ms\"\n    at _getError (.../jest-circus/build/utils.js:432:18)\n    at Array.map (<anonymous>)\n    at makeRunResult (.../jest-circus/build/utils.js:346:36)\n    at run (.../jest-circus/build/run.js:75:35)\n    at processTicksAndRejections (node:internal/process/task_queues:105:5)\n    at runAndTransformResultsToJestFormat (.../build/legacy-code-todo-rewrite/jestAdapterInit.js:122:21)\n    at jestAdapter (.../build/legacy-code-todo-rewrite/jestAdapter.js:79:19)\n    at runTestInternal (.../jest-runner/build/runTest.js:367:16)\n    at runTest (.../jest-runner/build/runTest.js:444:34)\n    at Object.worker (.../jest-runner/build/testWorker.js:106:12)\nError: thrown: \"Connection attempt timed out after 11792 ms\"\n    at _getError (.../jest-circus/build/utils.js:432:18)\n    at Array.map (<anonymous>)\n    at makeRunResult (.../jest-circus/build/utils.js:346:36)\n    at run (.../jest-circus/build/run.js:75:35)\n    at processTicksAndRejections (node:internal/process/task_queues:105:5)\n    at runAndTransformResultsToJestFormat (.../build/legacy-code-todo-rewrite/jestAdapterInit.js:122:21)\n    at jestAdapter (.../build/legacy-code-todo-rewrite/jestAdapter.js:79:19)\n    at runTestInternal (.../jest-runner/build/runTest.js:367:16)\n    at runTest (.../jest-runner/build/runTest.js:444:34)\n    at Object.worker (.../jest-runner/build/testWorker.js:106:12)\nError: thrown: \"Connection attempt timed out after 11791 ms\"\n    at _getError (.../jest-circus/build/utils.js:432:18)\n    at Array.map (<anonymous>)\n    at makeRunResult (.../jest-circus/build/utils.js:346:36)\n    at run (.../jest-circus/build/run.js:75:35)\n    at processTicksAndRejections (node:internal/process/task_queues:105:5)\n    at runAndTransformResultsToJestFormat (.../build/legacy-code-todo-rewrite/jestAdapterInit.js:122:21)\n    at jestAdapter (.../build/legacy-code-todo-rewrite/jestAdapter.js:79:19)\n    at runTestInternal (.../jest-runner/build/runTest.js:367:16)\n    at runTest (.../jest-runner/build/runTest.js:444:34)\n    at Object.worker (.../jest-runner/build/testWorker.js:106:12)"}
 Test execution failure: could be caused by test hooks like 'afterAll'.
Stack Traces | 0s run time
{"message":"","stack":"Error: thrown: \"Connection attempt timed out after 11511 ms\"\n    at _getError (.../jest-circus/build/utils.js:432:18)\n    at Array.map (<anonymous>)\n    at makeRunResult (.../jest-circus/build/utils.js:346:36)\n    at run (.../jest-circus/build/run.js:75:35)\n    at processTicksAndRejections (node:internal/process/task_queues:105:5)\n    at runAndTransformResultsToJestFormat (.../build/legacy-code-todo-rewrite/jestAdapterInit.js:122:21)\n    at jestAdapter (.../build/legacy-code-todo-rewrite/jestAdapter.js:79:19)\n    at runTestInternal (.../jest-runner/build/runTest.js:367:16)\n    at runTest (.../jest-runner/build/runTest.js:444:34)\n    at Object.worker (.../jest-runner/build/testWorker.js:106:12)"}
Alerts CRUD should perform all CRUD operations against raw alerts
Stack Traces | 0.389s run time
error: null value in column "summary" of relation "alerts" violates not-null constraint
    at .../pg/lib/client.js:545:17
    at processTicksAndRejections (node:internal/process/task_queues:105:5)
    at .../src/node-postgres/session.ts:66:19
    at .../repositories/alert/alert.repository.ts:107:24
    at NodePgSession.transaction (.../src/node-postgres/session.ts:155:19)
    at prepareAlert (.../test/functional/alert-crud.spec.ts:39:19)
    at Object.<anonymous> (.../test/functional/alert-crud.spec.ts:28:104)
Tx Sign POST /v1/tx should create a wallet for a user
Stack Traces | 7.45s run time
Error: expect(received).toBe(expected) // Object.is equality

Expected: 200
Received: 500
    at Object.<anonymous> (.../test/functional/sign-and-broadcast-tx.spec.ts:26:26)
Tx Sign POST /v1/tx should create a deployment for a user
Stack Traces | 9.76s run time
Error: expect(received).toBe(expected) // Object.is equality

Expected: 200
Received: 500
    at Object.<anonymous> (.../test/functional/create-deployment.spec.ts:64:26)
Certificate API POST /v1/certificate/create creates a certificate for the authenticated user
Stack Traces | 10.5s run time
Error: expect(received).toBe(expected) // Object.is equality

Expected: 200
Received: 500
    at Object.<anonymous> (.../test/functional/certificate.spec.ts:87:31)
View the full list of 4 ❄️ flaky test(s)
Managed Wallet API Deployment Flow should execute a full deployment cycle with provider JWT auth

Flake rate in main: 60.61% (Passed 13 times, Failed 20 times)

Stack Traces | 9.58s run time
TypeError: Cannot read properties of undefined (reading 'dseq')
    at createDeployment (.../test/functional/managed-api-deployment-flow.spec.ts:463:23)
    at .../test/functional/managed-api-deployment-flow.spec.ts:50:24
Managed Wallet API Deployment Flow should execute a full deployment cycle with provider mTLS auth

Flake rate in main: 60.61% (Passed 13 times, Failed 20 times)

Stack Traces | 7.35s run time
TypeError: Cannot read properties of undefined (reading 'dseq')
    at createDeployment (.../test/functional/managed-api-deployment-flow.spec.ts:463:23)
    at .../test/functional/managed-api-deployment-flow.spec.ts:50:24
Managed Wallet API Deployment Flow should maintain read-only operations during blockchain node outages

Flake rate in main: 60.61% (Passed 13 times, Failed 20 times)

Stack Traces | 6.66s run time
TypeError: Cannot read properties of undefined (reading 'dseq')
    at createDeployment (.../test/functional/managed-api-deployment-flow.spec.ts:463:23)
    at Object.<anonymous> (.../test/functional/managed-api-deployment-flow.spec.ts:183:24)
ProviderService sendManifest should retry on lease not found error and succeed

Flake rate in main: 6.67% (Passed 14 times, Failed 1 times)

Stack Traces | 6s run time
Error: thrown: "Connection attempt timed out after 20024 ms"
    at _getError (.../jest-circus/build/utils.js:432:18)
    at Array.map (<anonymous>)
    at makeSingleTestResult (.../jest-circus/build/utils.js:385:38)
    at makeTestResults (.../jest-circus/build/utils.js:408:26)
    at makeTestResults (.../jest-circus/build/utils.js:404:29)
    at makeTestResults (.../jest-circus/build/utils.js:404:29)
    at makeRunResult (.../jest-circus/build/utils.js:345:16)
    at run (.../jest-circus/build/run.js:75:35)
    at runAndTransformResultsToJestFormat (.../build/legacy-code-todo-rewrite/jestAdapterInit.js:122:21)
    at jestAdapter (.../build/legacy-code-todo-rewrite/jestAdapter.js:79:19)
    at runTestInternal (.../jest-runner/build/runTest.js:367:16)
    at runTest (.../jest-runner/build/runTest.js:444:34)
    at Object.worker (.../jest-runner/build/testWorker.js:106:12)

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

return this.clientAsPromised;
}

const client = await backOff(
Copy link
Contributor

Choose a reason for hiding this comment

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

issue(blocking): this is race condition vulnerable

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 (1)
apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (1)

92-117: Race condition: concurrent getClient() calls may create multiple clients.

If getClient() is invoked concurrently before the first call completes, both calls will pass the if (this.clientAsPromised) check and each will initialize a separate client. This can lead to multiple WebSocket connections and resource leaks.

While executeTxBatchBlocking() uses the semaphore for protection, getCachedAccountInfo() (line 159) and simulate() (line 318) call getClient() without synchronization.

Apply this diff to coordinate concurrent initialization:

+private clientInitPromise?: Promise<SyncSigningStargateClient>;
+
 private async getClient(): Promise<SyncSigningStargateClient> {
   if (this.clientAsPromised) {
     return this.clientAsPromised;
   }
+
+  if (this.clientInitPromise) {
+    return this.clientInitPromise;
+  }

-  const client = await backOff(
+  this.clientInitPromise = backOff(
     () =>
       this.connectWithSigner(this.config.get("RPC_NODE_ENDPOINT"), this.wallet, {
         registry: this.registry
       }).then(async client => {
         this.chainId = await client.getChainId();
         return client;
       }),
     {
       maxDelay: 10_000,
       startingDelay: 500,
       timeMultiple: 2,
       numOfAttempts: 7,
       jitter: "full"
     }
-  );
+  )
+    .then(client => {
+      this.clientAsPromised = client;
+      this.clientInitPromise = undefined;
+      return client;
+    })
+    .catch(error => {
+      this.clientInitPromise = undefined;
+      throw error;
+    });

-  this.clientAsPromised = client;
-
-  return client;
+  return this.clientInitPromise;
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 249971b and c4cd5ec.

⛔ Files ignored due to path filters (1)
  • packages/net/src/generated/netConfigData.ts is excluded by !**/generated/**
📒 Files selected for processing (1)
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (8 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
**/*.{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
🧬 Code graph analysis (1)
apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (1)
apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts (1)
  • SyncSigningStargateClient (10-36)
⏰ 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). (9)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: test-build
  • GitHub Check: Validate local packages
🔇 Additional comments (6)
apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (6)

41-41: LGTM: Type change supports lazy initialization.

The type change from Promise<SyncSigningStargateClient> to SyncSigningStargateClient | undefined correctly supports the new lazy initialization pattern.


119-124: LGTM: Correct cleanup pattern.

The removeClient() method properly checks for client existence before disconnecting and correctly clears the reference.


159-159: LGTM: Correctly uses lazy initialization.

The update to use this.getClient() is consistent with the lazy initialization pattern.


210-210: LGTM: Correct sequence mismatch handling.

Using removeClient() to reset the client on sequence mismatch is correct. The next operation will lazily re-initialize via getClient().


227-227: LGTM: Correctly uses lazy initialization.

The update to use await this.getClient() is consistent with the lazy initialization pattern.


318-318: LGTM: Correctly uses lazy initialization.

The update to use await this.getClient() is consistent with the lazy initialization pattern.

… call stack

Also uses sync transport client instantiation
@ygrishajev ygrishajev marked this pull request as draft October 28, 2025 18:06
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

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

84-86: init method contributes to race condition.

This private async method is called from the constructor without await, causing the race condition flagged in lines 58-61. See the earlier comment for fix options.

🧹 Nitpick comments (2)
apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts (1)

39-39: Add error handling for disconnect.

If disconnect() throws during cleanup (e.g., WebSocket already closed), the error would propagate from the finally block and potentially mask the original transaction result or error. Consider wrapping the disconnect call in a try-catch.

Apply this diff:

-        await client.disconnect();
+        try {
+          client.disconnect();
+        } catch (error) {
+          this.logger.warn({ event: "DEDUPE_SIGNING_CLIENT_DISCONNECT_ERROR", key, error });
+        }
apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (1)

116-121: Verify account existence error handling is sufficient.

The code throws a generic Error if account info is not found. Consider whether this error message provides enough context for debugging and whether it should be a more specific error type.

Consider enriching the error message:

  if (!accountInfo) {
-   throw new Error("Failed to get account info");
+   throw new Error(`Failed to get account info for address: ${address}`);
  }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c4cd5ec and 578de9c.

⛔ Files ignored due to path filters (1)
  • packages/net/src/generated/netConfigData.ts is excluded by !**/generated/**
📒 Files selected for processing (4)
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (9 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 (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/sync-signing-stargate-client/sync-signing-stargate-client.ts
  • apps/api/src/billing/providers/signing-client.provider.ts
  • apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.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/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts
  • apps/api/src/billing/providers/signing-client.provider.ts
  • apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts
  • apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts
🧬 Code graph analysis (3)
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 (10-28)
apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts (1)
apps/api/src/billing/lib/sync-signing-stargate-client/sync-signing-stargate-client.ts (1)
  • SyncSigningStargateClient (10-28)
apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts (2)
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 (10-28)
⏰ 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). (11)
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: validate / validate-app
  • GitHub Check: test-build
  • GitHub Check: Validate local packages
🔇 Additional comments (8)
apps/api/src/billing/providers/signing-client.provider.ts (3)

31-31: Binding looks correct.

The API change from connectWithSigner to init is properly applied with correct binding.


44-44: Binding looks correct.

The API change is consistently applied across all three client registrations.


18-18: Incorrect timing characterization in review comment.

The concern conflates container factory execution timing. Errors from SyncSigningStargateClient.init() will not surface at "container resolution time" during app initialization. Instead, they surface during the first HTTP request that instantiates a service requiring the signing client (e.g., when TopUpToolsService is injected and used). The factory function in useFactory executes lazily on first container.resolve() call within the service instantiation chain, not at module load.

The underlying point about error handling for synchronous initialization is worth considering, but the error surface timing as stated is incorrect.

Likely an incorrect or invalid review comment.

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

55-55: Binding correctly updated to use new API.

The change from connectWithSigner to init is correct and consistent with the broader refactor.

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

2-5: Import changes are correct.

The imports properly reflect the transport switch from HTTP to WebSocket: replaced HttpEndpoint and connectComet with WebsocketClient and Comet38Client.


11-14: The review comment's core assumptions are incorrect—WebsocketClient uses lazy connection.

WebsocketClient construction does not complete connection immediately; instead, it creates a client handle and connection is established asynchronously. The connected() method returns a Promise that resolves when the WebSocket is ready.

This means:

  • The init() method won't throw synchronously on invalid endpoints—errors occur on first use
  • The disconnect() method properly closes the connection when called
  • No try-catch is needed in init() for connection failures

The review comment incorrectly assumes synchronous connection behavior. The code is sound as written.

Likely an incorrect or invalid review comment.

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

30-30: Type definition correctly updated.

The rename from ConnectWithSignerFn to CreateWithSignerFn and the return type change from Promise<SyncSigningStargateClient> to SyncSigningStargateClient correctly reflects the synchronous initialization pattern.


101-101: Direct client access is correct.

Replacing await this.getClient() calls with direct this.client access is correct and eliminates unnecessary complexity. The removed caching layer was not needed with the new synchronous initialization.

Also applies to: 131-133, 152-152, 155-155, 170-170, 210-210

Comment on lines +58 to +61
this.client = this.createWithSigner(this.config.get("RPC_NODE_ENDPOINT"), this.wallet, {
registry: this.registry
});
this.init();
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

Fix race condition: await the initialization.

The constructor calls this.init() without awaiting it, creating a race condition where this.chainId might be undefined when executeTx is first called. Line 134 uses this.chainId! (non-null assertion), which could fail if the transaction executes before initialization completes.

The constructor cannot be async, so you need an alternative initialization strategy. Consider one of these approaches:

Option 1: Make init synchronous by moving chainId fetch to first use (lazy initialization)

  constructor(
    private readonly config: BillingConfigService,
    private readonly wallet: Wallet,
    private readonly registry: Registry,
    private readonly createWithSigner: CreateWithSignerFn,
    private readonly chainErrorService: ChainErrorService,
    private readonly loggerContext = BatchSigningClientService.name
  ) {
    this.client = this.createWithSigner(this.config.get("RPC_NODE_ENDPOINT"), this.wallet, {
      registry: this.registry
    });
-    this.init();
  }

- private async init() {
-   this.chainId = await this.client.getChainId();
- }
+ private async getChainId(): Promise<string> {
+   if (!this.chainId) {
+     this.chainId = await this.client.getChainId();
+   }
+   return this.chainId;
+ }

Then update line 134:

  await this.client.sign(accountInfo.address, messages, fee, "", {
    accountNumber: accountInfo.accountNumber,
    sequence: accountInfo.sequence + txIndex,
-   chainId: this.chainId!
+   chainId: await this.getChainId()
  })

Option 2: Use a static factory method instead of constructor

+ public static async create(
+   config: BillingConfigService,
+   wallet: Wallet,
+   registry: Registry,
+   createWithSigner: CreateWithSignerFn,
+   chainErrorService: ChainErrorService,
+   loggerContext = BatchSigningClientService.name
+ ): Promise<BatchSigningClientService> {
+   const service = new BatchSigningClientService(config, wallet, registry, createWithSigner, chainErrorService, loggerContext);
+   await service.init();
+   return service;
+ }

- constructor(
+ private constructor(
    ...
  ) {
    this.client = this.createWithSigner(this.config.get("RPC_NODE_ENDPOINT"), this.wallet, {
      registry: this.registry
    });
-   this.init();
  }

Then update all instantiation sites to use await BatchSigningClientService.create(...).

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

Comment on lines +76 to 82
async disconnect() {
this.client.disconnect();
}

private incrementSequence(): void {
if (this.cachedAccountInfo) {
this.cachedAccountInfo.sequence++;
}
async dispose() {
await this.disconnect();
}
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

Remove incorrect await keywords and add null-safety check.

Based on past review comments and CosmJS documentation:

  1. disconnect() returns void, not a Promise - the await keywords are incorrect
  2. The method should check if a client exists before disconnecting

Apply this diff:

  async disconnect() {
-   this.client.disconnect();
+   if (this.client) {
+     this.client.disconnect();
+   }
  }

  async dispose() {
-   await this.disconnect();
+   this.disconnect();
  }

Note: Both methods can be made synchronous since disconnect() is synchronous:

- async disconnect() {
+ disconnect() {
    if (this.client) {
      this.client.disconnect();
    }
  }

- async dispose() {
+ dispose() {
    this.disconnect();
  }
📝 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
async disconnect() {
this.client.disconnect();
}
private incrementSequence(): void {
if (this.cachedAccountInfo) {
this.cachedAccountInfo.sequence++;
}
async dispose() {
await this.disconnect();
}
async disconnect() {
if (this.client) {
this.client.disconnect();
}
}
async dispose() {
this.disconnect();
}
🤖 Prompt for AI Agents
In apps/api/src/billing/lib/batch-signing-client/batch-signing-client.service.ts
around lines 76 to 82, the disconnect()/dispose() implementations incorrectly
await a synchronous disconnect() and don't check for a null/undefined client;
change both methods to synchronous (remove await/async), add a null-safety check
like if (this.client) this.client.disconnect(), and optionally clear this.client
after disconnect to avoid repeated calls.

if (!client.hasPendingTransactions && this.clientsByAddress.has(key)) {
this.logger.debug({ event: "DEDUPE_SIGNING_CLIENT_CLEAN_UP", key });
this.clientsByAddress.delete(key);
await client.disconnect();
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

Remove await from disconnect() call.

According to the past review comments and CosmJS documentation, SigningStargateClient.disconnect() returns void (not a Promise). The await keyword here has no effect but suggests async behavior that doesn't exist.

Apply this diff:

-        await client.disconnect();
+        client.disconnect();
📝 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
await client.disconnect();
client.disconnect();
🤖 Prompt for AI Agents
In
apps/api/src/billing/services/dedupe-signing-client/dedupe-signing-client.service.ts
around line 39, the call `await client.disconnect();` uses await but
SigningStargateClient.disconnect() is synchronous (returns void); remove the
`await` so the call becomes a plain `client.disconnect();`, ensuring the method
signature stays correct and no unnecessary async behavior is implied.

this.cachedAccountInfo = undefined;
this.accountInfoPromise = undefined;
private async init() {
this.chainId = await this.client.getChainId();
Copy link
Contributor

Choose a reason for hiding this comment

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

question(blocking): can this be done lazily? I assume it sends a remote request somewhere and if we do this without .catch it will crash the app when the remote service is down

@ygrishajev ygrishajev closed this Oct 30, 2025
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