Skip to content

refactor: extracts tx signer into a dedicated service#2579

Merged
ygrishajev merged 8 commits intomainfrom
feature/tx-signer
Jan 30, 2026
Merged

refactor: extracts tx signer into a dedicated service#2579
ygrishajev merged 8 commits intomainfrom
feature/tx-signer

Conversation

@ygrishajev
Copy link
Contributor

@ygrishajev ygrishajev commented Jan 28, 2026

Summary by CodeRabbit

  • New Features

    • Adds tx-signer service with HTTP endpoints for funding/derived signing and a /healthz check
    • Optional external signing integration (feature-flag gated)
  • Infrastructure

    • Docker Compose, Helm values, and deployment workflows for tx-signer
    • New CI release/build/deploy workflows and build config for tx-signer
    • Runtime env files and sample configs for multiple environments
  • Tests

    • New unit and functional tests covering signing flows and server lifecycle

✏️ Tip: You can customize this high-level summary in your review settings.

@ygrishajev ygrishajev requested a review from a team as a code owner January 28, 2026 17:27
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 28, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Adds a new tx-signer microservice (HTTP API, DI, batching, wallet, tests), integrates it into CI/CD, Docker, Helm and the API via a feature-flagged ExternalSignerHttpSdkService that the TxManagerService delegates to when enabled.

Changes

Cohort / File(s) Summary
Workflows & Release
​.github/workflows/reusable-build-image.yml, ​.github/workflows/reusable-deploy-k8s.yml, ​.github/workflows/tx-signer-release.yml
Add tx-signer tag handling, deployment input option, and a reusable release/build/deploy workflow for tx-signer.
Helm values
.helm/tx-signer-*.yaml
Add deploymentEnv, chain and nodeOptions.enabledOTEL entries for staging/prod mainnet and sandbox values.
API feature flags & config
apps/api/env/.env.functional.test, apps/api/src/billing/config/env.config.ts, apps/api/src/core/config/env.config.ts, apps/api/src/core/services/feature-flags/*
Introduce TX_SIGNER_BASE_URL and FEATURE_FLAGS_EXTERNAL_TX_SIGNER_DISABLED; add FeatureFlags.EXTERNAL_TX_SIGNER and runtime guard.
API external signer service
apps/api/src/billing/services/external-signer-http-sdk/*.ts
New ExternalSignerHttpSdkService that encodes messages, posts to tx-signer endpoints, parses responses, and maps/logs errors.
API TxManager integration
apps/api/src/billing/services/tx-manager/*.ts, *.spec.ts
Inject featureFlags and ExternalSignerHttpSdkService; add feature-flag gated delegation paths and tests updated to mock external signer.
tx-signer project scaffold
apps/tx-signer/* (package.json, tsconfig*, tsup.config.ts, jest.config.ts`, env files)
Add new app scaffold: package, build/test configs, env files for multiple environments and sample env.
Providers & DI
apps/tx-signer/src/providers/*
Add RAW_APP_CONFIG, APP_CONFIG, TYPE_REGISTRY, logging provider and barrel exports for DI wiring.
Core services
apps/tx-signer/src/services/*
Add startServer/shutdownServer, HonoErrorHandlerService, OpenApiHonoHandler, tracing helpers, ConfigService and AppConfigService with tests.
Wallet, signing client & batching
apps/tx-signer/src/lib/*, apps/tx-signer/src/caching/*
Add Wallet abstraction, BatchSigningClientService (batching, retries, recovery), signing-stargate client factory, memoizeAsync helper and accompanying tests.
Controllers, routes & schemas
apps/tx-signer/src/controllers/*, apps/tx-signer/src/routes/*, apps/tx-signer/src/http-schemas/*, apps/tx-signer/src/index.ts
Add Health and TX controllers, routes for /v1/tx/funding and /v1/tx/derived, Zod request/response schemas, and Hono app entrypoint.
Tests & seeders
apps/tx-signer/src/**/*.spec.ts, apps/tx-signer/test/**
Add extensive unit and functional tests for signing flows, batch retries, wallet, factory, and test seeders.
Docker & compose
packages/docker/docker-compose.*.yml, packages/docker/.env.sandbox.docker-compose-dev
Add tx-signer service to build/dev/prod compose files, port mapping 3091:3000 and TX_SIGNER_BASE_URL env.
Misc & release
apps/tx-signer/.release-it.js, .gitignore
Add minimal release-it delegation and a .gitignore entry.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API as API Service
    participant Flags as FeatureFlagsService
    participant TxMgr as TxManagerService
    participant ExternalSdk as ExternalSignerHttpSdk
    participant TxSigner as tx-signer Service

    Client->>API: Send sign request
    API->>Flags: isEnabled(EXTERNAL_TX_SIGNER)?
    Flags-->>API: true/false

    alt external signer enabled
        API->>TxMgr: signAndBroadcastWithFundingWallet(messages)
        TxMgr->>ExternalSdk: signAndBroadcastWithFundingWallet(messages)
        ExternalSdk->>TxSigner: POST /v1/tx/funding (encoded messages)
        TxSigner->>TxSigner: decode → sign → broadcast
        TxSigner-->>ExternalSdk: IndexedTx
        ExternalSdk-->>TxMgr: IndexedTx
        TxMgr-->>API: IndexedTx
    else feature disabled
        API->>TxMgr: signAndBroadcastWithFundingWallet(messages)
        TxMgr->>TxMgr: internal batch signing flow
        TxMgr-->>API: IndexedTx
    end

    API-->>Client: return result
Loading
sequenceDiagram
    participant Client
    participant TxSigner as tx-signer Service
    participant Wallet as Wallet Factory
    participant Batch as BatchSigningClient
    participant Stargate as SigningStargateClient
    participant Chain as Blockchain

    Client->>TxSigner: POST /v1/tx/derived (derivationIndex, messages)
    TxSigner->>TxSigner: decode messages
    TxSigner->>Wallet: create(derivationIndex)
    Wallet-->>TxSigner: Wallet
    TxSigner->>Batch: signAndBroadcast(messages, options)
    Batch->>Stargate: getAccount / sign / estimateGas / broadcast
    Stargate->>Chain: RPC calls
    Chain-->>Batch: responses (account, gas, tx hash)
    Batch->>Stargate: getTx (with retry)
    Chain-->>Batch: IndexedTx
    Batch-->>TxSigner: IndexedTx
    TxSigner-->>Client: { code, hash, rawLog }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • baktun14
  • stalniy

Poem

🐰 I hopped along the repo trail,
New tx-signer sings a signing tale,
Wallets hum and batches play,
OTEL lights the logging way,
Hops of joy for every mail! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'refactor: extracts tx signer into a dedicated service' clearly and concisely summarizes the primary change—extracting the transaction signer functionality into a separate, dedicated service architecture. This is the main objective evident throughout the changeset.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/tx-signer

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

@codecov
Copy link

codecov bot commented Jan 28, 2026

Codecov Report

❌ Patch coverage is 78.78788% with 98 lines in your changes missing coverage. Please review.
✅ Project coverage is 50.59%. Comparing base (415fdc4) to head (5acf322).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...s/hono-error-handler/hono-error-handler.service.ts 34.04% 31 Missing ⚠️
...tch-signing-client/batch-signing-client.service.ts 87.80% 15 Missing ⚠️
apps/tx-signer/src/lib/wallet/wallet.ts 57.14% 12 Missing ⚠️
...gner/src/services/tx-manager/tx-manager.service.ts 81.81% 10 Missing ⚠️
...x-signer/src/services/start-server/start-server.ts 71.42% 8 Missing ⚠️
...er/src/services/shutdown-server/shutdown-server.ts 70.58% 5 Missing ⚠️
.../tx-signer/src/services/tracing/tracing.service.ts 73.33% 4 Missing ⚠️
...gner/src/controllers/healthz/healthz.controller.ts 50.00% 2 Missing ⚠️
apps/tx-signer/src/providers/logging.provider.ts 75.00% 2 Missing ⚠️
...pps/tx-signer/src/routes/healthz/healthz.router.ts 80.00% 2 Missing ⚠️
... and 5 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2579      +/-   ##
==========================================
+ Coverage   50.44%   50.59%   +0.14%     
==========================================
  Files        1029     1043      +14     
  Lines       29245    29356     +111     
  Branches     6610     6660      +50     
==========================================
+ Hits        14753    14852      +99     
- Misses      14084    14117      +33     
+ Partials      408      387      -21     
Flag Coverage Δ *Carryforward flag
api 78.45% <ø> (-0.01%) ⬇️ Carriedforward from d7f85b3
deploy-web 32.24% <100.00%> (+<0.01%) ⬆️
log-collector ?
notifications 87.94% <ø> (ø) Carriedforward from d7f85b3
provider-console 81.48% <ø> (ø)
provider-proxy 84.35% <ø> (ø)
tx-signer 78.74% <78.74%> (?)

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

Files with missing lines Coverage Δ
apps/api/src/billing/config/env.config.ts 100.00% <ø> (ø)
.../billing/services/tx-manager/tx-manager.service.ts 98.24% <ø> (-0.04%) ⬇️
apps/api/src/core/config/env.config.ts 75.00% <ø> (ø)
...re/services/feature-flags/feature-flags.service.ts 93.33% <ø> (ø)
...i/src/core/services/feature-flags/feature-flags.ts 100.00% <ø> (ø)
apps/deploy-web/src/queries/useTemplateQuery.tsx 97.33% <100.00%> (+0.03%) ⬆️
apps/tx-signer/src/caching/helpers/helpers.ts 100.00% <100.00%> (ø)
apps/tx-signer/src/config/env.config.ts 100.00% <100.00%> (ø)
apps/tx-signer/src/controllers/tx/tx.controller.ts 100.00% <100.00%> (ø)
apps/tx-signer/src/http-schemas/healthz.schema.ts 100.00% <100.00%> (ø)
... and 20 more

... and 13 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.

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

🤖 Fix all issues with AI agents
In @.github/workflows/reusable-build-image.yml:
- Around line 108-110: The workflow uses registry variables like
TX_SIGNER_REGISTRY when writing to GITHUB_ENV (see the elif branch that sets
"registry=${{ vars.TX_SIGNER_REGISTRY }}" and "app=tx-signer"), but there is no
validation that these repository variables are defined; add an early validation
step in the workflow (before any build.sh invocation) that checks each required
vars.TX_SIGNER_REGISTRY, vars.API_REGISTRY, vars.WEB_REGISTRY,
vars.PROVIDER_PROXY_REGISTRY, vars.PROVIDER_CONSOLE_REGISTRY,
vars.STATS_WEB_REGISTRY, vars.NOTIFICATIONS_REGISTRY, and vars.INDEXER_REGISTRY
and fails the job with a clear error message if any are empty so the workflow
fails fast with a helpful message rather than passing an empty registry into
build.sh.

In @.github/workflows/tx-signer-release.yml:
- Around line 10-83: The workflow is missing explicit least-privilege
permissions for the release and beta deploy jobs causing CodeQL warnings; update
the jobs named "release", "deploy-beta-mainnet", and "deploy-beta-sandbox" to
declare explicit permissions instead of relying on defaults (mirror the minimal
scopes used in "build" and "deploy-prod-*" jobs), e.g., grant only the exact
rights needed (contents: write for release if creating releases, packages: write
if publishing images) and add actions: read for all deploy-* jobs to support gh
run list in the reusable deploy workflow; ensure you reference the job names
release, deploy-beta-mainnet, and deploy-beta-sandbox when adding these
permissions blocks.

In `@apps/api/src/billing/config/env.config.ts`:
- Line 40: Update the TX_SIGNER_BASE_URL env schema to be optional: change its
zod definition from TX_SIGNER_BASE_URL: z.string() to TX_SIGNER_BASE_URL:
z.string().optional() so deployments don't fail when the EXTERNAL_TX_SIGNER
feature is disabled; locate the constant/field named TX_SIGNER_BASE_URL in
env.config.ts and adjust its zod validator accordingly so code that reads
FEATURE_FLAGS_EXTERNAL_TX_SIGNER_DISABLED can run without requiring that URL.

In
`@apps/api/src/billing/services/external-signer-http-sdk/external-signer-http-sdk.service.ts`:
- Around line 71-91: The post method in ExternalSignerHttpSdkService lacks a
request timeout; fetch a timeout value from
billingConfigService.get("TX_SIGNER_HTTP_TIMEOUT_MS") (falling back to a
sensible default like 30000 ms), parse it to a number, and pass it to axios.post
via the timeout option so the request to the URL constructed with
TX_SIGNER_BASE_URL is bounded; ensure the timeout value is used in the axios
config object and preserved in error logging (TX_SIGNER_REQUEST_FAILED) so
timeouts surface correctly.

In `@apps/tx-signer/env/.env`:
- Line 1: The .env is missing required values validated by envSchema
(FUNDING_WALLET_MNEMONIC, OLD_MASTER_WALLET_MNEMONIC,
DERIVATION_WALLET_MNEMONIC, RPC_NODE_ENDPOINT); add a non-committed sample
and/or defaults: create a .env.local.sample containing placeholder mnemonic
variables and short instructions to copy into .env.local for local dev, and
ensure RPC_NODE_ENDPOINT has a sensible default in .env (or add
environment-specific files like .env.sandbox/.env.testnet) so tx-signer can
start; reference the envSchema and the exact variable names above when
implementing the sample and updating the documentation.

In `@apps/tx-signer/package.json`:
- Line 7: The package.json has mismatched entries: the "main" field
("server.js") and the "prod" script point at dist/server.js while tsup builds
./src/index.ts to dist/index.js; update package.json so the "main" field is
"dist/index.js" and the "prod" script runs node dist/index.js (or alternatively
change tsup's entry/output to produce dist/server.js) — edit the "main" property
and the "prod" script to consistently reference dist/index.js to match the tsup
entry ./src/index.ts.

In `@apps/tx-signer/src/lib/batch-signing-client/batch-signing-client.service.ts`:
- Around line 58-60: The hasPendingTransactions getter is wrong because
this.semaphore.nrWaiting() only reports waiting callers and misses active
acquisitions; do not use tryAcquire() (it would consume a permit). Add an
explicit counter (e.g., private activeBatches = 0) and update
hasPendingTransactions to return this.activeBatches > 0 ||
this.semaphore.nrWaiting() > 0; increment activeBatches immediately after
successfully acquiring the semaphore and decrement it in the finally/cleanup
block right after releasing the semaphore (update the methods that call
semaphore.acquire()/semaphore.release() accordingly) so the counter always
reflects in-flight batches.

In `@apps/tx-signer/src/lib/create-route/create-route.ts`:
- Around line 1-10: The createRoute wrapper currently only removes "security"
via a TypeScript cast, so at runtime createOpenApiRoute still receives
routeConfig.security; change createRoute to destructure routeConfig and exclude
security before forwarding (e.g., const { security, ...payload } = routeConfig)
and then call createOpenApiRoute(payload) so the security property is actually
omitted at runtime; update references to routeConfig, createRoute, and
createOpenApiRoute accordingly.

In
`@apps/tx-signer/src/lib/signing-stargate-client-factory/signing-stargate-client.factory.spec.ts`:
- Around line 10-34: The factory and test must be updated to handle async
creation: change createSigningStargateClientFactory's returned factory to be
async and update its type signature to return Promise<SigningStargateClient>
(where it calls SigningStargateClient.createWithSigner) and ensure the
implementation awaits the call to SigningStargateClient.createWithSigner; then
update the test (the factory variable in
signing-stargate-client.factory.spec.ts) to call await on factory(endpoint,
signer) (mark the test async) and adjust expectations to work with the resolved
mock Promise (ensure mockFactory is mocked to return a Promise or the test
awaits the returned Promise before asserting result equals mockClient).

In `@apps/tx-signer/src/lib/wallet/wallet.ts`:
- Around line 53-56: The getFirstAddress method currently assumes getAccounts()
returns a non-empty array which will throw when accessing accounts[0].address;
update getFirstAddress to defensively check the result of this.getAccounts()
(call to getAccounts) and if the array is empty either throw a clear error
(e.g., "No accounts available") or handle the empty case explicitly before
returning accounts[0].address so callers won't hit a runtime exception.

In `@apps/tx-signer/src/providers/type-registry.provider.ts`:
- Around line 19-40: Add a shared type guard (e.g., function isGeneratedType(x):
x is { $type: string } that returns Boolean(x) && typeof x === "object" &&
"$type" in x) and use it to filter inputs before .map() for
defaultRegistryTypes, akashTypes, and newAkashTypes so the 'in' operator never
runs on primitives; update defaultRegistryTypes' .filter(x => x && "$type" in x)
to .filter(isGeneratedType), insert .filter(isGeneratedType) before mapping
akashTypes (which currently lacks any filter), and replace newAkashTypes'
.filter(x => "$type" in x) with .filter(isGeneratedType) to prevent undefined
entries and runtime errors.

In `@apps/tx-signer/src/services/config/config.service.ts`:
- Around line 3-8: The generic default for C is causing inferred schema
properties to collapse to never; update both the ConfigServiceOptions interface
and the ConfigService class to use an empty object type as the default for C
(replace Record<string, never> with {}), so that the private readonly config: C
& z.infer<E> and methods like get() preserve the inferred z.infer<E> types when
C is omitted.

In `@apps/tx-signer/src/services/shutdown-server/shutdown-server.spec.ts`:
- Around line 7-17: Refactor the test to use a local setup helper that creates
and returns fresh mocks and the function under test instead of inlining them:
create a setup() function at the bottom of the describe that constructs the
mocked ServerType (with listening: false), a mocked Logger, and returns {
server, logger }; then update the test to call const { server, logger } =
setup() and use those values, and ensure no shared state or beforeEach is used;
reference the shutdownServer function and the ServerType and Logger mocks when
implementing the helper.

In `@apps/tx-signer/src/services/start-server/start-server.ts`:
- Around line 49-52: The process exit handler uses the "exit" event which won't
wait for async cleanup in shutdown() (which calls shutdownServer()); change the
listener from processEvents.on("exit", ...) to processEvents.on("beforeExit",
async (code) => { await shutdown(`BEFOREEXIT:${code}`); }) (or equivalent) so
the async shutdown() is awaited and cleanup completes; keep the existing
SIGINT/SIGTERM handlers that call shutdown("SIGINT"/"SIGTERM") and ensure you
reference the shutdown and shutdownServer functions in the updated handler.

In `@apps/tx-signer/test/functional/sign-and-broadcast-tx.spec.ts`:
- Around line 42-61: Tests register a mocked TxManagerService directly into the
DI container which can leak across tests; call container.clearInstances() at the
start of this test (or move the registration into a shared setup function) to
ensure a clean container before registering the mock. Specifically, before
registering the mock TxManagerService and calling
container.registerInstance(TxManagerService, txManagerService), invoke
container.clearInstances() (or refactor the mock/registration into the test
suite's setup helper) so signAndBroadcastWithDerivedWallet and other mocks don't
pollute subsequent tests.
🧹 Nitpick comments (30)
apps/tx-signer/src/caching/helpers/helpers.spec.ts (1)

3-26: Consider adding a test for argument-based memoization.

The current tests verify memoization for no-argument calls and rejection handling. Consider adding a test that verifies different arguments result in separate cache entries (i.e., memoized('a') and memoized('b') should call fn twice, while repeated calls with 'a' should use the cache).

💡 Suggested test case
+  it("caches results per unique arguments", async () => {
+    const fn = jest.fn().mockImplementation((arg: string) => Promise.resolve(arg));
+    const memoized = memoizeAsync(fn);
+
+    const result1 = await memoized("a");
+    const result2 = await memoized("b");
+    const result3 = await memoized("a");
+
+    expect(result1).toBe("a");
+    expect(result2).toBe("b");
+    expect(result3).toBe("a");
+    expect(fn).toHaveBeenCalledTimes(2);
+  });
apps/deploy-web/public/sdl-schema.yaml (2)

60-81: Description mentions enforcement not implemented in schema.

The description states "Non-RAM classes (beta1, beta2, beta3, default) require persistent=true", but the schema only enforces that RAM class must NOT have persistent=true. The requirement for non-RAM classes to have persistent=true isn't validated here.

This may be intentional given the note about semantic validations at runtime, but consider updating the description to clarify what this schema definition actually validates versus what's deferred to runtime.

📝 Suggested description clarification
   storageAttributesValidation:
     description: |
       Storage attributes validation:
-      1. RAM class must not be persistent
-      2. Non-RAM classes (beta1, beta2, beta3, default) require persistent=true
+      Validates that RAM class must not have persistent=true.
+      Note: The requirement for non-RAM classes to have persistent=true
+      is enforced at runtime by the Go parser.

184-222: GPU bidirectional validation not fully enforced in schema.

The description mentions bidirectional validation: "units > 0 requires attributes, and attributes require units > 0". However, the schema only requires units but doesn't enforce:

  1. If units > 0, then attributes must be present
  2. If attributes is present, then units must be > 0

If this is intentionally deferred to runtime validation (consistent with the top-level note), consider clarifying in the description. Otherwise, conditional schemas (if/then) could enforce this.

apps/tx-signer/env/.env.sample (1)

1-13: Sample file may be missing some environment variables.

Comparing with .env.functional.test and .env.unit.test, this sample appears to be missing:

  • NETWORK
  • REST_API_NODE_URL
  • LOG_LEVEL
  • DEPLOYMENT_ENV
  • STD_OUT_LOG_FORMAT

Consider adding these placeholders to help developers understand the full configuration surface.

apps/tx-signer/src/lib/create-route/create-route.spec.ts (1)

1-17: Consider covering security stripping to prevent regressions.
A small test asserting the returned route omits security would lock in the intended behavior once the runtime strip is applied.

✅ Example add-on test
 describe(createRoute.name, () => {
   it("returns a route config with path", () => {
     const route = createRoute({
       method: "get",
       path: "/test",
       responses: {
           description: "ok"
         }
       }
     });

     expect(route.path).toBe("/test");
   });
+
+  it("omits security in the returned route config", () => {
+    const route = createRoute({
+      method: "get",
+      path: "/secure",
+      security: [{ bearerAuth: [] }],
+      responses: {
+        200: { description: "ok" }
+      }
+    });
+
+    expect("security" in route).toBe(false);
+  });
 });
packages/docker/docker-compose.prod-with-db.yml (1)

3-7: Add healthcheck to tx-signer service and change depends_on condition to service_healthy.

tx-signer exposes a /healthz endpoint but docker-compose.prod-with-db.yml doesn't define a healthcheck for it. Currently using service_started only confirms the container is up, not that the signer is ready. Add a healthcheck configuration (similar to the db service in the same file) and change the condition to service_healthy to ensure the API doesn't race against an unready signer.

apps/tx-signer/src/services/start-server/start-server.spec.ts (1)

8-18: Consider using a setup function pattern per coding guidelines.

The test should use a setup function at the bottom of the describe block instead of inline object creation. This improves reusability and follows project conventions.

♻️ Suggested refactor with setup function
 describe(startServer.name, () => {
   it("starts and returns a server", async () => {
-    const app = new Hono();
-    const logger = mock<LoggerService>();
-    const emitter = new EventEmitter();
-
-    const server = await startServer(app, logger, emitter, { port: 0 });
+    const { server } = await setup();
     expect(server).toBeDefined();
 
     await new Promise<void>(resolve => server?.close(() => resolve()));
   });
+
+  async function setup() {
+    const app = new Hono();
+    const logger = mock<LoggerService>();
+    const emitter = new EventEmitter();
+    const server = await startServer(app, logger, emitter, { port: 0 });
+    return { app, logger, emitter, server };
+  }
 });

As per coding guidelines: "Use setup function instead of beforeEach in test files."

apps/tx-signer/src/http-schemas/tx-signer/tx-signer.schema.ts (1)

14-26: Consider adding integer and non-negative validation for derivationIndex.

The derivationIndex is used for HD wallet path derivation and should be a non-negative integer. Using z.number() allows floats and negative values.

♻️ Suggested validation improvement
 export const SignAndBroadcastDerivedRequestInputSchema = z.object({
   data: z.object({
-    derivationIndex: z.number(),
+    derivationIndex: z.number().int().nonnegative(),
     messages: z.array(EncodedMessageSchema).min(1),
     options: z
       .object({
apps/tx-signer/src/http-schemas/tx-signer/tx-signer.schema.spec.ts (2)

3-3: Use a named reference instead of a hardcoded string in describe block.

Per coding guidelines, use a reference like SignAndBroadcastFundingRequestInputSchema.description or create a module-level constant for the describe name to enable automated refactoring tools.


3-17: Consider expanding test coverage for schema validation.

The test only covers the happy path for SignAndBroadcastFundingRequestInputSchema. Consider adding tests for:

  • Invalid payloads (missing fields, empty messages array)
  • SignAndBroadcastDerivedRequestInputSchema validation
  • SignAndBroadcastResponseOutputSchema validation
apps/tx-signer/src/services/hono-error-handler/hono-error-handler.service.ts (2)

85-88: Minor: HTTP 502 maps to "service_unavailable" which is technically "Bad Gateway".

Both 502 (Bad Gateway) and 503 (Service Unavailable) return the same code "service_unavailable". Consider using "bad_gateway" for 502 if semantic precision is desired.

♻️ Optional: Differentiate 502 and 503
       case 502:
-        return "service_unavailable";
+        return "bad_gateway";
       case 503:
         return "service_unavailable";

71-91: Missing case for HTTP 500 in getErrorCode.

The switch handles 400, 401, 403, 404, 409, 429, 502, 503 but not 500. A 500 error would fall through to "unknown_error" rather than a more appropriate "internal_server_error" code.

♻️ Add 500 case
       case 429:
         return "rate_limited";
+      case 500:
+        return "internal_server_error";
       case 502:
-        return "service_unavailable";
+        return "bad_gateway";
apps/tx-signer/src/lib/wallet/wallet.spec.ts (1)

3-9: Test structure follows guidelines, but coverage is minimal.

The test correctly uses Wallet.name in the describe block and validates the core address prefix behavior. Consider expanding coverage to test:

  • Mnemonic-based wallet creation (deterministic addresses)
  • Index-based HD path derivation
  • signDirect and signAmino methods
apps/tx-signer/src/lib/batch-signing-client/batch-signing-client.service.ts (2)

132-162: Signing batch continues after first failure, potentially wasting gas simulations.

When a signing error occurs (line 157), subsequent transactions in the batch continue to be processed. However, if the error is due to a fundamental issue (e.g., account not found, insufficient funds), continuing to sign may waste RPC calls for gas estimation.

This is acceptable if partial batch success is the intended behavior, but consider whether early termination on certain error types would be more efficient.


164-200: Sequential broadcast loop is correct but could be simplified.

The while loop with manual index management works but could be more idiomatic using a for-of loop with index tracking or entries().

Optional simplification
  private async broadcastBatch(signResults: Result<TxRaw, unknown>[]): Promise<Result<string, unknown>[]> {
    const results: Result<string, unknown>[] = [];
-   let index = 0;
-
-   while (index < signResults.length) {
-     const signResult = signResults[index];
+   for (const [index, signResult] of signResults.entries()) {
      if (!signResult.ok) {
        results.push(signResult);
-       index++;
        continue;
      }
      // ... rest of the logic
-     index++;
    }
    return results;
  }
apps/tx-signer/src/lib/batch-signing-client/batch-signing-client.service.spec.ts (7)

24-36: Test descriptions should not start with "should".

Per coding guidelines, test descriptions should use present simple, 3rd person singular without prepending "should".

Proposed fix
-  it("should batch and execute multiple transactions successfully", async () => {
+  it("batches and executes multiple transactions successfully", async () => {

As per coding guidelines: Use present simple, 3rd person singular for test descriptions without prepending 'should'.


38-57: Test description should follow guidelines.

Proposed fix
-  it("should handle errors in batch without affecting other transactions", async () => {
+  it("handles errors in batch without affecting other transactions", async () => {

As per coding guidelines: Use present simple, 3rd person singular for test descriptions without prepending 'should'.


59-81: Test description should follow guidelines.

Proposed fix
-  it("should retry failed transaction within a batch on sequence mismatch error and eventually succeed", async () => {
+  it("retries failed transaction within a batch on sequence mismatch error and eventually succeeds", async () => {

As per coding guidelines: Use present simple, 3rd person singular for test descriptions without prepending 'should'.


83-97: Test description should follow guidelines.

Proposed fix
-  it("should recover transaction when getTx fails with network error but tx exists on chain", async () => {
+  it("recovers transaction when getTx fails with network error but tx exists on chain", async () => {

As per coding guidelines: Use present simple, 3rd person singular for test descriptions without prepending 'should'.


99-113: Test description should follow guidelines.

Proposed fix
-  it("should recover transaction when getTx fails with socket error", async () => {
+  it("recovers transaction when getTx fails with socket error", async () => {

As per coding guidelines: Use present simple, 3rd person singular for test descriptions without prepending 'should'.


115-131: Test description should follow guidelines.

Proposed fix
-  it("should recover transaction when getTx fails with cosmjs fetch failed error with cause", async () => {
+  it("recovers transaction when getTx fails with cosmjs fetch failed error with cause", async () => {

As per coding guidelines: Use present simple, 3rd person singular for test descriptions without prepending 'should'.


133-144: Test description should follow guidelines.

Proposed fix
-  it("should not attempt recovery for non-network errors", async () => {
+  it("does not attempt recovery for non-network errors", async () => {

As per coding guidelines: Use present simple, 3rd person singular for test descriptions without prepending 'should'.

apps/tx-signer/src/services/tx-manager/tx-manager.service.ts (1)

104-115: Client cleanup logic may have a race condition.

The check !client.hasPendingTransactions happens after signAndBroadcast completes. If another request arrives between the broadcast completion and the cleanup check, and that request uses the same derived wallet, the client might be deleted while the new request is using it.

However, since #getClient will recreate the client if missing, this is safe but may cause unnecessary client recreation. This is likely acceptable given the low probability and self-healing nature.

apps/tx-signer/src/services/hono-error-handler/hono-error-handler.service.spec.ts (1)

6-15: Use a setup function instead of direct instantiation.

Per coding guidelines, tests should use a setup function at the bottom of the describe block instead of inline instantiation.

♻️ Suggested refactor
 describe(HonoErrorHandlerService.name, () => {
   it("returns 500 response for unknown errors", async () => {
-    const service = new HonoErrorHandlerService();
-    const context = mock<AppContext>({
-      json: ((body: unknown, init: ResponseInit) => new Response(JSON.stringify(body), init)) as AppContext["json"]
-    });
+    const { service, context } = setup();
 
     const response = await service.handle(new Error("boom"), context);
     expect(response.status).toBe(500);
   });
+
+  function setup() {
+    const context = mock<AppContext>({
+      json: ((body: unknown, init: ResponseInit) => new Response(JSON.stringify(body), init)) as AppContext["json"]
+    });
+    const service = new HonoErrorHandlerService();
+    return { service, context };
+  }
 });

As per coding guidelines: "Use setup function instead of beforeEach in test files. The setup function must be at the bottom of the root describe block."

apps/tx-signer/src/routes/tx/tx.router.spec.ts (1)

7-7: Use txRouter.constructor.name or a reference instead of hardcoded string.

Per coding guidelines, use a dynamic reference for describe suite descriptions to enable automated refactoring tools.

However, since txRouter is an instance of OpenApiHonoHandler, using txRouter.constructor.name would yield "OpenApiHonoHandler" which may not be descriptive. Consider keeping the current approach or using a constant.

apps/api/src/core/services/feature-flags/feature-flags.service.ts (1)

30-32: Config-driven disable takes precedence over ENABLE_ALL.

The EXTERNAL_TX_SIGNER_DISABLED check runs before ENABLE_ALL, so this flag can be disabled even when all feature flags are globally enabled. If this is intentional (e.g., for safety in test environments), consider adding a brief inline comment to clarify the precedence.

apps/tx-signer/src/controllers/tx-manager/tx-manager.controller.spec.ts (1)

7-32: Consider using the setup function pattern per coding guidelines.

Per guidelines, tests should use a setup function at the bottom of the describe block. This keeps instantiation consistent and simplifies adding more test cases (e.g., for signWithDerivedWallet).

♻️ Suggested refactor
 describe(TxManagerController.name, () => {
   it("decodes messages and signs with funding wallet", async () => {
-    const decodedMessage = { typeUrl: "/test.MsgTest", value: { foo: "bar" } };
-    const registry = mock<Registry>({
-      decode: jest.fn().mockReturnValue(decodedMessage.value)
-    });
-    const txManagerService = mock<TxManagerService>({
-      signAndBroadcastWithFundingWallet: jest.fn().mockResolvedValue({ code: 0, hash: "tx", rawLog: "" })
-    });
-
-    const controller = new TxManagerController(registry, txManagerService);
+    const decodedMessage = { typeUrl: "/test.MsgTest", value: { foo: "bar" } };
+    const { controller, registry, txManagerService } = setup({
+      decodeResult: decodedMessage.value,
+      signResult: { code: 0, hash: "tx", rawLog: "" }
+    });
+
     const result = await controller.signWithFundingWallet({
       data: {
         messages: [
           {
             typeUrl: decodedMessage.typeUrl,
             value: Buffer.from([1, 2, 3]).toString("base64")
           }
         ]
       }
     });

     expect(registry.decode).toHaveBeenCalledWith({ typeUrl: decodedMessage.typeUrl, value: expect.any(Uint8Array) });
     expect(txManagerService.signAndBroadcastWithFundingWallet).toHaveBeenCalledWith([decodedMessage]);
     expect(result.data.code).toBe(0);
   });
+
+  function setup({ decodeResult, signResult }: { decodeResult?: unknown; signResult?: { code: number; hash: string; rawLog: string } }) {
+    const registry = mock<Registry>({
+      decode: jest.fn().mockReturnValue(decodeResult)
+    });
+    const txManagerService = mock<TxManagerService>({
+      signAndBroadcastWithFundingWallet: jest.fn().mockResolvedValue(signResult)
+    });
+    const controller = new TxManagerController(registry, txManagerService);
+    return { controller, registry, txManagerService };
+  }
 });

As per coding guidelines: "Use setup function instead of beforeEach in test files. The setup function must be at the bottom of the root describe block."

apps/tx-signer/src/routes/healthz/healthz.router.ts (1)

28-31: Consider resolving the controller once if it's stateless.

The controller is resolved from the DI container on every request. If HealthzController is stateless (registered as singleton), resolving once at module load could avoid repeated lookups:

const controller = container.resolve(HealthzController);
healthzRouter.openapi(healthzRoute, async c => {
  const payload = await controller.getStatus();
  return c.json(payload, 200);
});

However, the current approach is acceptable if the controller needs request-scoped dependencies or if consistency with other routers is preferred.

apps/tx-signer/src/routes/tx/tx.router.ts (1)

14-43: Consider documenting error responses in the OpenAPI spec.

The route definitions only specify the 200 success response. For a complete OpenAPI spec, consider adding common error responses (e.g., 400 for validation errors, 500 for server errors) if they're not handled by a global error schema.

apps/tx-signer/package.json (1)

52-52: Consider pinning zod to a more specific version range.

The "3.*" version range is quite loose and could lead to unexpected breaking changes. Consider using a more specific range like "^3.23.0" or aligning with other packages in the monorepo.

@ygrishajev ygrishajev force-pushed the feature/tx-signer branch 4 times, most recently from 6f15578 to 13f0e97 Compare January 29, 2026 10:11
stalniy
stalniy previously approved these changes Jan 29, 2026
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: 11

Caution

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

⚠️ Outside diff range comments (1)
packages/docker/.env.sandbox.docker-compose-dev (1)

2-8: Fix dotenv-linter UnorderedKey by moving TX_SIGNER_BASE_URL earlier.

Line 8 triggers the UnorderedKey warning; reordering keeps lint clean.

🔧 Proposed reorder
 AkashSandboxDatabaseCS=postgres://postgres:password@db:5432/console-akash-sandbox
+TX_SIGNER_BASE_URL=http://tx-signer:3000
 UserDatabaseCS=postgres://postgres:password@db:5432/console-users
 NETWORK=sandbox
 POSTGRES_DB_URI=postgres://postgres:password@db:5432/console-users
 DB_HOST=db
 NOTIFICATIONS_API_BASE_URL=http://notifications:3000
-TX_SIGNER_BASE_URL=http://tx-signer:3000
🤖 Fix all issues with AI agents
In `@apps/api/src/core/services/start-server/start-server.ts`:
- Around line 56-59: The current use of processEvents.on("beforeExit", ...) is
inappropriate for process.exit/uncaught errors; update the shutdown wiring by
replacing the beforeExit listener with an "exit" listener or remove it
altogether, and add handlers for "uncaughtException" and "unhandledRejection"
that call shutdown, while keeping the existing processEvents.on("SIGTERM", ...)
and processEvents.on("SIGINT", ...) hooks; target the processEvents.on calls and
the shutdown function to implement this change.

In `@apps/api/src/server.ts`:
- Around line 50-60: Replace the dead child.once("beforeExit", ...) listener
with child.once("exit", ...) so non‑zero child exits are correctly detected and
rejected; update the handler attached to the ChildProcess instance (the code
using child.once and reject with INTERFACE in the error message) to listen for
"exit" (or "close" if you prefer) instead of "beforeExit", and ensure the
disconnect cleanup logic (disconnect function and process.on handlers) remains
unchanged.

In `@apps/tx-signer/env/.env.functional.test`:
- Around line 1-6: Reorder the environment variable keys in the
.env.functional.test file to satisfy dotenv-linter (e.g., sort keys
alphabetically); specifically ensure keys like DEPLOYMENT_ENV, LOG_LEVEL,
NETWORK, REST_API_NODE_URL, RPC_NODE_ENDPOINT, STD_OUT_LOG_FORMAT are placed in
the linter-expected order (alphabetical or project-specified ordering) so the
dotenv-linter no longer flags the file.

In `@apps/tx-signer/env/.env.mainnet`:
- Around line 1-2: The file is missing a trailing newline which trips
dotenv-linter; open the env file containing REST_API_NODE_URL and
RPC_NODE_ENDPOINT and add a single final newline (EOF newline) so the file ends
with a newline character, then save and re-run lint/CI to confirm the warning is
resolved.

In `@apps/tx-signer/env/.env.sample`:
- Around line 1-13: The .env.sample triggers dotenv-linter UnorderedKey warnings
because the environment keys are not sorted; fix this by alphabetizing all keys
(e.g., AVERAGE_GAS_PRICE, DERIVATION_WALLET_MNEMONIC,
DERIVATION_WALLET_MNEMONIC_V1, DERIVATION_WALLET_MNEMONIC_V2,
FUNDING_WALLET_MNEMONIC, FUNDING_WALLET_MNEMONIC_V1, FUNDING_WALLET_MNEMONIC_V2,
GAS_SAFETY_MULTIPLIER, OLD_MASTER_WALLET_MNEMONIC, PORT, RPC_NODE_ENDPOINT,
SERVER_ORIGIN, WALLET_BATCHING_INTERVAL_MS), preserving the same key names and
blank-line/formatting conventions so dotenv-linter no longer reports
UnorderedKey.

In `@apps/tx-signer/env/.env.unit.test`:
- Around line 1-10: This file contains plaintext wallet mnemonics
(OLD_MASTER_WALLET_MNEMONIC, FUNDING_WALLET_MNEMONIC,
DERIVATION_WALLET_MNEMONIC); remove the secrets from env/.env.unit.test and
replace their values with placeholder variables (e.g., "<REPLACE_WITH_SECRET>")
or omit them, commit an .env.unit.test.example with those placeholders, and
update CI/local docs to load the real mnemonics from CI secrets or a local .env
(set via environment variables in your pipeline) so the actual mnemonic values
are never committed.
- Around line 1-10: The dotenv file keys are out of order and causing
dotenv-linter failures; reorder the environment variables (NETWORK,
OLD_MASTER_WALLET_MNEMONIC, FUNDING_WALLET_MNEMONIC, DERIVATION_WALLET_MNEMONIC,
REST_API_NODE_URL, RPC_NODE_ENDPOINT, LOG_LEVEL, STD_OUT_LOG_FORMAT,
SQL_LOG_FORMAT, DEPLOYMENT_ENV) into the order expected by dotenv-linter
(typically alphabetical by key) so the linter passes.

In
`@apps/tx-signer/src/lib/signing-stargate-client-factory/signing-stargate-client.factory.ts`:
- Around line 8-25: The returned factory must be async and await the Comet38
client creation: change CreateSigningStargateClient to return
Promise<SigningStargateClient>, make createSigningStargateClientFactory's inner
function async, call and await ProvidedComet38Client.create(client) (referencing
ProvidedComet38Client.create) and then await/return the result of
factory(cometClient, signer, options) (referencing
SigningStargateClient.createWithSigner) so you pass an actual Comet38Client
instance rather than a Promise.

In `@apps/tx-signer/src/services/config/config.service.ts`:
- Around line 10-14: The constructor currently spreads an optional
options.config into this.config allowing an unsafe {} cast; update the
ConfigService constructor to validate options.config at runtime (in the
constructor of class ConfigService) and either throw a clear error if
options.config is undefined or merge in a safe default before assigning to
this.config (preserving the C & z.infer<E> type). Specifically, check
options.config (from ConfigServiceOptions<E,C>) and handle the undefined case
instead of blindly doing this.config = { ...options.config } as C & z.infer<E>,
so callers won’t get a fake empty object at runtime.

In `@apps/tx-signer/src/services/tx-manager/tx-manager.service.ts`:
- Around line 8-9: Remove the blanket eslint suppression before the import of
LoggerService: delete the "// eslint-disable-next-line" comment preceding
"import { LoggerService } ..." in tx-manager.service.ts so linting rules apply
normally; if a specific rule genuinely needs disabling, replace the blanket
suppression with a targeted rule (e.g., // eslint-disable-next-line <rule-name>)
next to the exact offending line after validating why it's required.

In `@packages/docker/docker-compose.prod-with-db.yml`:
- Around line 6-7: The current dependency uses "condition: service_started" for
the tx-signer service; replace it with "condition: service_healthy" and add a
corresponding HEALTHCHECK to the tx-signer service definition (in
docker-compose.prod.yml where tx-signer is defined) that tests the signer’s API
readiness (e.g., curl or a small probe against the signer’s health endpoint);
alternatively, if you determine "service_started" is sufficient, add a comment
in docker-compose.prod-with-db.yml documenting that decision and why readiness
is not required.
🧹 Nitpick comments (6)
apps/tx-signer/src/services/tracing/tracing.service.ts (1)

7-27: Missing context propagation breaks parent-child span relationships.

The function doesn't set the span as the active context during fn() execution. This means nested spans created within fn() won't automatically be linked as children of this span, breaking distributed tracing hierarchies.

Additionally, for consistency with existing patterns in job-queue.service.ts, consider recording exceptions for non-Error throws as well.

♻️ Proposed fix with context propagation and consistent exception recording
-import { SpanStatusCode, trace } from "@opentelemetry/api";
+import { context, SpanStatusCode, trace } from "@opentelemetry/api";

 export function createSpan(name: string) {
   return trace.getTracer("default").startSpan(name);
 }

 export async function withSpan<T>(spanName: string, fn: () => Promise<T>): Promise<T> {
   const span = createSpan(spanName);
+  const ctx = trace.setSpan(context.active(), span);

   try {
-    const result = await fn();
+    const result = await context.with(ctx, fn);
     span.setStatus({ code: SpanStatusCode.OK });
     return result;
   } catch (error: unknown) {
     const message = error instanceof Error ? error.message : "Unknown error";
     span.setStatus({
       code: SpanStatusCode.ERROR,
       message
     });
-    if (error instanceof Error) {
-      span.recordException(error);
-    }
+    span.recordException(error instanceof Error ? error : new Error(String(error)));
     throw error;
   } finally {
     span.end();
   }
 }
apps/tx-signer/env/.env.staging (1)

1-1: Port 80 binding is supported via container capabilities; verify staging environment intent.

The Dockerfile.node production stage grants cap_net_bind_service to the node executable before switching to the non-root app user, enabling privileged port binding. However, PORT=80 differs from production (PORT: 3000) and the default config (PORT: 3091). Confirm this staging-specific port assignment is intentional.

apps/tx-signer/src/lib/create-route/create-route.spec.ts (1)

3-16: Consider adding test coverage for security stripping behavior.

The test verifies path propagation, but given that createRoute is designed to handle the security property specially, consider adding a test case that verifies security is properly handled (stripped or ignored) when provided in the input config.

apps/tx-signer/src/providers/raw-app-config.provider.ts (1)

3-3: Type includes number but process.env values are always strings.

process.env values are either string or undefined, never number. Consider using Record<string, string | undefined> for type accuracy, or if the broader type is intentional for flexibility, this is fine as-is.

apps/tx-signer/src/http-schemas/tx-signer/tx-signer.schema.ts (1)

14-17: Harden derivationIndex to an integer, non‑negative value.
Fractional/negative indexes are invalid for HD wallet derivation and should be rejected early.

♻️ Proposed change
-    derivationIndex: z.number(),
+    derivationIndex: z.number().int().nonnegative(),
apps/tx-signer/src/lib/batch-signing-client/batch-signing-client.service.ts (1)

176-212: Clarify the broadcast strategy: sync for all but last, async for final.

The broadcast logic uses broadcastTxSync for intermediate transactions and broadcastTx (async/confirmed) for the last one. This is an optimization to reduce latency, but it means intermediate transactions may not be confirmed when the batch returns. Consider adding a brief inline comment to document this intentional design choice for future maintainers.

@ygrishajev ygrishajev force-pushed the feature/tx-signer branch 3 times, most recently from fd07a40 to eb3fff9 Compare January 29, 2026 13:25
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: 6

🤖 Fix all issues with AI agents
In
`@apps/api/src/billing/services/external-signer-http-sdk/external-signer-http-sdk.service.ts`:
- Around line 30-62: The response type SignAndBroadcastResponse currently uses
IndexedTx but the tx-signer returns only the subset { code, hash, rawLog } (per
SignAndBroadcastResponseOutputSchema); update the contract by replacing
SignAndBroadcastResponse.data with a narrow type matching the actual schema
(e.g., an interface with code, hash, rawLog) and update usages in
ExternalSignerHttpSdkService.signAndBroadcastWithFundingWallet and
signAndBroadcastWithDerivedWallet (and the generic post<T> calls) to use the new
type, or alternatively modify the tx-signer output to return a full IndexedTx if
you want callers to receive all fields—ensure the type used in the post<>
generic matches the actual runtime shape.

In `@apps/tx-signer/src/http-schemas/tx-signer/tx-signer.schema.ts`:
- Around line 14-26: The SignAndBroadcastDerivedRequestInputSchema currently
allows any number for derivationIndex; update the derivationIndex validator to
enforce a non-negative integer by replacing z.number() with
z.number().int().nonnegative() (i.e., change the derivationIndex schema inside
SignAndBroadcastDerivedRequestInputSchema to use .int().nonnegative()) so floats
and negative values are rejected.

In
`@apps/tx-signer/src/services/hono-error-handler/hono-error-handler.service.ts`:
- Around line 32-44: The handler currently accesses error.data without type
safety; update the isHttpError block in hono-error-handler.service (the branch
using isHttpError, getErrorCode, getErrorType and c.json) to only include data
when present or use a narrowed type - e.g., define a HttpErrorWithData =
HttpError & { data?: unknown } and cast/narrow error to that before reading
data, or check "if ('data' in error && error.data !== undefined)" and include it
conditionally in the response object passed to c.json so you never serialize an
undefined property.

In `@apps/tx-signer/src/services/tracing/tracing.service.ts`:
- Around line 7-27: withSpan creates a span but doesn't make it the active span,
so child spans inside fn() aren't parented; update withSpan to set the created
span into the active context (using trace.setSpan(context.active(), span)) and
execute fn() inside context.with(...) so context propagation matches
job-queue.service.ts; also ensure exceptions that are not Error objects are
recorded by converting them (e.g., recordException(new Error(String(error))) or
similar) and include that message when calling span.setStatus.

In `@apps/tx-signer/src/services/tx-manager/tx-manager.service.ts`:
- Around line 121-134: The `#getClient` method can create duplicate clients under
concurrency because two callers can pass the has(address) check; fix by caching
the in-flight creation promise in `#clientsByAddress`: after deriving address (via
getDerivedWallet and wallet.getFirstAddress) immediately set
`#clientsByAddress.set`(address, { address, client: <Promise> }) where client is
the Promise returned by invoking batchSigningClientServiceFactory(wallet) (store
the Promise synchronously before awaiting), then await that promise and return
the resolved CachedClient; alternatively use a mutex keyed by address to ensure
only one creation runs (key methods: `#getClient`, `#clientsByAddress`,
batchSigningClientServiceFactory).

In `@apps/tx-signer/test/setup-unit-tests.ts`:
- Around line 3-8: The afterAll teardown currently swallows all errors from
container.dispose(); update the afterAll to avoid a blank catch by either
checking and using an idempotent guard (e.g., if (!container.isDisposed) await
container.dispose()) or by catching only expected/benign errors and rethrowing
others; specifically modify the afterAll block that calls container.dispose() so
it does not use an empty catch, instead detect known disposable-state errors or
rethrow unexpected exceptions from container.dispose() to surface real failures.
🧹 Nitpick comments (6)
packages/docker/docker-compose.prod.yml (1)

10-20: New tx-signer service looks good overall.

The service configuration is consistent with other services in the file. A couple of minor observations:

  1. Quote style inconsistency: The tx-signer and api services now use double quotes for port mappings, while notifications (line 29) and others still use single quotes. Consider aligning for consistency.

  2. Environment configuration: Verify that the tx-signer service doesn't require additional environment variables (e.g., for connecting to the blockchain RPC, wallet configuration, or API endpoints) beyond what's provided in .env.sandbox.docker-compose-dev.

apps/tx-signer/src/services/shutdown-server/shutdown-server.spec.ts (1)

7-17: Consider adding test coverage for remaining code paths.

The test only covers the "not listening" scenario. The shutdownServer function has multiple branches that would benefit from coverage:

  • Server is listening → server.close() is called
  • onShutdown callback is invoked and completes successfully
  • onShutdown callback throws → logs ON_SHUTDOWN_ERROR
  • server.close() fails → logs SERVER_CLOSE_ERROR
apps/tx-signer/src/services/hono-error-handler/hono-error-handler.service.ts (1)

71-92: Consider semantic accuracy for HTTP 502 error code.

HTTP 502 is "Bad Gateway", not "Service Unavailable" (which is 503). Mapping both to "service_unavailable" may cause confusion when debugging. Consider using "bad_gateway" for 502.

♻️ Suggested change for semantic accuracy
       case 429:
         return "rate_limited";
       case 502:
-        return "service_unavailable";
+        return "bad_gateway";
       case 503:
         return "service_unavailable";
apps/tx-signer/src/services/hono-error-handler/hono-error-handler.service.spec.ts (2)

6-15: Consider adding tests for other error branches.

The test only covers the fallback path for unknown errors. The handle method has four distinct branches (HTTPException, HttpError, ZodError, and fallback). Testing only one path leaves significant coverage gaps.

🧪 Suggested additional test cases
import { mock } from "jest-mock-extended";
import { HTTPException } from "hono/http-exception";
import createHttpError from "http-errors";
import { ZodError } from "zod";

import type { AppContext } from "../../types/app-context";
import { HonoErrorHandlerService } from "./hono-error-handler.service";

describe(HonoErrorHandlerService.name, () => {
  describe("handle", () => {
    it("returns 500 response for unknown errors", async () => {
      const { service, context } = setup();

      const response = await service.handle(new Error("boom"), context);

      expect(response.status).toBe(500);
    });

    it("returns appropriate status for HTTPException", async () => {
      const { service, context } = setup();

      const response = await service.handle(new HTTPException(404, { message: "Not found" }), context);

      expect(response.status).toBe(404);
    });

    it("returns 400 for ZodError", async () => {
      const { service, context } = setup();
      const zodError = new ZodError([]);

      const response = await service.handle(zodError, context);

      expect(response.status).toBe(400);
    });

    it("returns appropriate status for HttpError", async () => {
      const { service, context } = setup();

      const response = await service.handle(createHttpError(403, "Forbidden"), context);

      expect(response.status).toBe(403);
    });
  });

  function setup() {
    const service = new HonoErrorHandlerService();
    const context = mock<AppContext>({
      json: ((body: unknown, init: ResponseInit) => new Response(JSON.stringify(body), init)) as AppContext["json"]
    });
    return { service, context };
  }
});

6-15: Consider using a setup function for consistency.

Per coding guidelines, tests should use a setup function at the bottom of the root describe block. While this single-test file works, adopting the pattern now makes it easier to add tests later. As per coding guidelines: "The setup function must be at the bottom of the root describe block, should create an object under test and return it."

apps/tx-signer/src/routes/tx/tx.router.spec.ts (1)

7-7: Use <Subject>.name for the root describe.

Hardcoded suite name makes refactors harder. Consider binding the suite to the subject name (e.g., constructor name) to follow the test conventions.

🔧 Example adjustment
-describe("txRouter", () => {
+describe(txRouter.constructor.name, () => {

As per coding guidelines: Use <Subject>.name in the root describe suite description instead of hardcoded class/service name strings to enable automated refactoring tools to find all references.

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

🤖 Fix all issues with AI agents
In `@apps/api/env/.env.functional.test`:
- Around line 37-45: Reorder the environment keys in
apps/api/env/.env.functional.test to satisfy dotenv-linter (alphabetical/order
rules) — specifically ensure FEATURE_FLAGS_ENABLE_ALL,
FEATURE_FLAGS_EXTERNAL_TX_SIGNER_DISABLED, CONSOLE_WEB_PAYMENT_LINK,
STD_OUT_LOG_FORMAT and the FUNDING_/DERIVATION_WALLET_MNEMONIC_V1/V2 entries
follow the linter's expected ordering and grouping, and add a single trailing
newline at the end of the file to fix the EOF newline warning; update the file
so the mnemonic lines remain together and in the correct order per the linter.
- Around line 42-45: The committed plaintext wallet mnemonics
(FUNDING_WALLET_MNEMONIC_V1, DERIVATION_WALLET_MNEMONIC_V1,
FUNDING_WALLET_MNEMONIC_V2, DERIVATION_WALLET_MNEMONIC_V2) must be replaced with
non-sensitive placeholders in apps/api/env/.env.functional.test and real values
should be loaded from CI/local secrets or an example file; change each value to
a clear placeholder (e.g. "__PLACEHOLDER__" or unset) and add instructions to
load the actual mnemonics from environment secrets or a
.env.functional.test.example, ensuring no real mnemonics remain in the repo.

In `@apps/tx-signer/src/config/env.config.ts`:
- Around line 4-7: The schema currently marks FUNDING_WALLET_MNEMONIC_V1,
FUNDING_WALLET_MNEMONIC_V2, DERIVATION_WALLET_MNEMONIC_V1, and
DERIVATION_WALLET_MNEMONIC_V2 as optional, which lets the Wallet constructor
silently generate ephemeral mnemonics; update validation so at least one of
these env vars is present and fail early: add a runtime check in the
WALLET_RESOURCES initialization (or modify the zod schema) that inspects
FUNDING_WALLET_MNEMONIC_V1|V2 and DERIVATION_WALLET_MNEMONIC_V1|V2 and throws a
clear error if all four are undefined, referencing the environment symbol names
in the error message so startup fails loudly instead of creating ephemeral
wallets.
🧹 Nitpick comments (2)
apps/tx-signer/src/config/env.config.ts (1)

3-16: Schema structure looks good and follows existing patterns.

The configuration aligns well with the patterns established in apps/api/src/billing/config/env.config.ts and apps/api/src/core/config/env.config.ts. The use of coercion for numeric fields is correct since environment variables are strings.

One minor observation: RPC_NODE_ENDPOINT could benefit from .url() validation for stronger input checking, similar to how REST_API_NODE_URL is validated in the core config.

💡 Optional: Add URL validation
-  RPC_NODE_ENDPOINT: z.string(),
+  RPC_NODE_ENDPOINT: z.string().url(),
apps/tx-signer/src/services/tx-manager/tx-manager.service.ts (1)

99-102: Consider accepting WalletOptions for funding address lookups.
This keeps read-only address retrieval consistent with the v1/v2 support already exposed for derived wallets.

♻️ Suggested change
-  async getFundingWalletAddress() {
-    const { masterWallet } = this.#getWalletResources();
+  async getFundingWalletAddress(options?: WalletOptions) {
+    const { masterWallet } = this.#getWalletResources(options);
     return await masterWallet.getFirstAddress();
   }
Based on learnings: In the Akash Console codebase, v1 wallets are only used for read-only purposes such as retrieving addresses.

@akash-network akash-network deleted a comment from coderabbitai bot Jan 29, 2026
@akash-network akash-network deleted a comment from coderabbitai bot Jan 29, 2026
@ygrishajev ygrishajev force-pushed the feature/tx-signer branch 4 times, most recently from 97250d7 to 1433e56 Compare January 29, 2026 14:35
baktun14
baktun14 previously approved these changes Jan 29, 2026
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

🤖 Fix all issues with AI agents
In `@apps/tx-signer/src/services/shutdown-server/shutdown-server.ts`:
- Around line 11-17: The current Promise.resolve(onShutdown?.()) calls
onShutdown synchronously so thrown errors escape the .catch; change it to defer
invocation by using Promise.resolve().then(() => onShutdown?.()) so any
synchronous throw is converted into a rejection and will be handled by the
existing .catch (which logs via appLogger.error with event "ON_SHUTDOWN_ERROR")
before the .finally which calls resolve().
🧹 Nitpick comments (9)
apps/tx-signer/test/seeders/akash-address.seeder.ts (1)

4-12: LGTM! Optional: more idiomatic array initialization.

The implementation correctly generates valid Akash addresses. For a slightly more functional approach, you could use Uint8Array.from():

♻️ Optional refactor
 export const createAkashAddress = (): string => {
-  const addressData = new Uint8Array(20);
-
-  for (let i = 0; i < 20; i++) {
-    addressData[i] = faker.number.int({ min: 0, max: 255 });
-  }
+  const addressData = Uint8Array.from({ length: 20 }, () => faker.number.int({ min: 0, max: 255 }));

   return toBech32("akash", addressData);
 };
apps/api/src/billing/services/tx-manager/tx-manager.service.spec.ts (1)

294-314: Verify feature flag name is asserted in tests.

The mock for featureFlagsService.isEnabled returns a boolean unconditionally. Consider asserting that isEnabled is called with the specific feature flag name (e.g., "EXTERNAL_TX_SIGNER") to ensure the correct flag is being checked.

💡 Optional enhancement to verify flag name
-    const featureFlagsService = mock<FeatureFlagsService>({
-      isEnabled: jest.fn().mockReturnValue(input?.featureFlagsEnabled ?? false)
-    });
+    const featureFlagsService = mock<FeatureFlagsService>({
+      isEnabled: jest.fn().mockImplementation((flag: string) => {
+        if (flag === "EXTERNAL_TX_SIGNER") {
+          return input?.featureFlagsEnabled ?? false;
+        }
+        return false;
+      })
+    });

Then in the tests, add an assertion:

expect(featureFlagsService.isEnabled).toHaveBeenCalledWith("EXTERNAL_TX_SIGNER");
apps/tx-signer/src/services/config/config.service.spec.ts (1)

5-11: Consider adding edge case tests.

The test validates the happy path. Consider adding tests for:

  • Accessing multiple config values
  • Type inference with different schema shapes

This would improve confidence in the generic type handling.

apps/tx-signer/src/http-schemas/tx-signer/tx-signer.schema.spec.ts (2)

3-3: Use schema reference in describe block.

Per coding guidelines, use a reference instead of hardcoded strings in describe blocks to enable automated refactoring tools.

♻️ Suggested fix
-describe("tx-signer schema", () => {
+describe(SignAndBroadcastFundingRequestInputSchema.description ?? "SignAndBroadcastFundingRequestInputSchema", () => {

Or simply:

-describe("tx-signer schema", () => {
+describe("SignAndBroadcastFundingRequestInputSchema", () => {

As per coding guidelines: Use <Subject>.name in the root describe suite description instead of hardcoded class/service name strings.


4-17: Consider adding validation failure tests.

The test only covers the happy path. Consider adding tests for invalid inputs to ensure the schema correctly rejects malformed payloads:

  • Empty messages array (schema has .min(1))
  • Missing typeUrl
  • Missing value
💡 Example additional tests
it("rejects empty messages array", () => {
  const result = SignAndBroadcastFundingRequestInputSchema.safeParse({
    data: { messages: [] }
  });
  expect(result.success).toBe(false);
});

it("rejects message without typeUrl", () => {
  const result = SignAndBroadcastFundingRequestInputSchema.safeParse({
    data: {
      messages: [{ value: Buffer.from([1, 2, 3]).toString("base64") }]
    }
  });
  expect(result.success).toBe(false);
});
apps/tx-signer/src/controllers/tx/tx.controller.spec.ts (1)

7-33: Align this test with the required setup helper pattern.
The test constructs its dependencies inline; please move that into a setup function at the bottom of the root describe to match the project’s test conventions.

♻️ Suggested refactor
 describe(TxController.name, () => {
   it("decodes messages and signs with funding wallet", async () => {
-    const decodedMessage = { typeUrl: "/test.MsgTest", value: { foo: "bar" } };
-    const registry = mock<Registry>({
-      decode: jest.fn().mockReturnValue(decodedMessage.value)
-    });
-    const txManagerService = mock<TxManagerService>({
-      signAndBroadcastWithFundingWallet: jest.fn().mockResolvedValue({ code: 0, hash: "tx", rawLog: "" })
-    });
-
-    const controller = new TxController(registry, txManagerService);
+    const { controller, registry, txManagerService, decodedMessage } = setup({
+      decodedMessage: { typeUrl: "/test.MsgTest", value: { foo: "bar" } }
+    });
     const result = await controller.signWithFundingWallet({
       data: {
         messages: [
           {
             typeUrl: decodedMessage.typeUrl,
             value: Buffer.from([1, 2, 3]).toString("base64")
           }
         ]
       }
     });
 
     expect(registry.decode).toHaveBeenCalledWith({ typeUrl: decodedMessage.typeUrl, value: expect.any(Uint8Array) });
     expect(txManagerService.signAndBroadcastWithFundingWallet).toHaveBeenCalledWith([decodedMessage]);
     expect(result.data.code).toBe(0);
   });
+
+  const setup = ({
+    decodedMessage
+  }: {
+    decodedMessage: { typeUrl: string; value: { foo: string } };
+  }) => {
+    const registry = mock<Registry>();
+    registry.decode.mockReturnValue(decodedMessage.value);
+    const txManagerService = mock<TxManagerService>();
+    txManagerService.signAndBroadcastWithFundingWallet.mockResolvedValue({ code: 0, hash: "tx", rawLog: "" });
+    const controller = new TxController(registry, txManagerService);
+    return { controller, registry, txManagerService, decodedMessage };
+  };
 });

As per coding guidelines: Use setup function instead of beforeEach in test files. The setup function must be at the bottom of the root describe block, should create an object under test and return it, accept a single parameter with inline type definition, avoid shared state, and not have a specified return type.

apps/tx-signer/src/lib/batch-signing-client/batch-signing-client.service.spec.ts (1)

24-36: Test descriptions should not use "should" prefix.

Per coding guidelines, test descriptions should use present simple, 3rd person singular without prepending "should". This applies to all test cases in this file.

♻️ Suggested test description changes
-  it("should batch and execute multiple transactions successfully", async () => {
+  it("batches and executes multiple transactions successfully", async () => {

Similar changes for other tests:

  • "handles errors in batch without affecting other transactions"
  • "retries failed transaction within a batch on sequence mismatch error and eventually succeeds"
  • "recovers transaction when getTx fails with network error but tx exists on chain"
  • "recovers transaction when getTx fails with socket error"
  • "recovers transaction when getTx fails with cosmjs fetch failed error with cause"
  • "does not attempt recovery for non-network errors"
apps/tx-signer/src/lib/batch-signing-client/batch-signing-client.service.ts (1)

49-52: Type narrowing for error in retry handler.

The retry handler accesses res.val.message without type narrowing. While this works at runtime, it relies on implicit type coercion.

♻️ Consider explicit type check
 private readonly signAndBroadcastExecutor = retry(
-  handleWhenResult(res => res instanceof Err && "message" in res.val && res.val.message?.includes("account sequence mismatch")),
+  handleWhenResult(res => res instanceof Err && res.val instanceof Error && res.val.message?.includes("account sequence mismatch")),
   { maxAttempts: 5, backoff: new ExponentialBackoff({ maxDelay: 5_000, initialDelay: 500 }) }
 );
apps/tx-signer/src/controllers/tx/tx.controller.ts (1)

45-55: Consider error handling for message decoding.

The decodeMessages method will throw if:

  • message.value contains invalid base64
  • registry.decode fails for an unknown typeUrl

While these errors will propagate to the caller, consider whether specific error handling or logging would be beneficial for debugging malformed requests.

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

🤖 Fix all issues with AI agents
In
`@apps/api/src/billing/services/external-signer-http-sdk/external-signer-http-sdk.service.ts`:
- Around line 71-74: The URL is constructed outside the try in the post method
which can throw (new URL(path, baseUrl)) and bypass LoggerService error
handling; move the URL construction (the call to
billingConfigService.get("TX_SIGNER_BASE_URL") and the new URL(path,
baseUrl).toString() usage) inside the try block in the post<T>(path: string,
body: unknown) method so any exceptions from invalid base URLs or path are
caught and logged by LoggerService within the existing try/catch.
🧹 Nitpick comments (3)
apps/tx-signer/src/http-schemas/tx.schema.ts (2)

14-26: derivationIndex constraint is correct; consider validating granter is non-empty.

The derivationIndex now properly enforces non-negative integers. One minor consideration: granter currently accepts empty strings, which would be invalid as a Cosmos address.

💡 Optional: Prevent empty granter string
         fee: z.object({
-          granter: z.string()
+          granter: z.string().min(1)
         })

28-36: Response schema looks good; code could optionally be constrained to integer.

The .passthrough() is appropriate for allowing additional response fields. Transaction response codes are always integers (0 for success, non-zero for errors), so adding .int() would make the schema more precise.

💡 Optional: Constrain code to integer
   data: z
     .object({
-      code: z.number(),
+      code: z.number().int(),
       hash: z.string(),
       rawLog: z.string()
     })
apps/api/src/billing/services/external-signer-http-sdk/external-signer-http-sdk.service.ts (1)

75-80: Consider making the HTTP timeout configurable (optional).

A hardcoded timeout works, but making it configurable via BillingConfigService keeps it aligned with other environments and avoids code changes to tune it.

♻️ Possible refactor
-      const response = await axios.post<T>(url, body, {
+      const timeoutMs = Number(this.billingConfigService.get("TX_SIGNER_HTTP_TIMEOUT_MS") ?? 60_000);
+      const response = await axios.post<T>(url, body, {
         headers: {
           "Content-Type": "application/json"
         },
-        timeout: 60_000
+        timeout: Number.isFinite(timeoutMs) ? timeoutMs : 60_000
       });

If adding a new env/config key is out of scope for this PR, consider tracking it as a follow-up issue. Based on learnings: 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.

Comment on lines +71 to +74
private async post<T>(path: string, body: unknown): Promise<T> {
const baseUrl = this.billingConfigService.get("TX_SIGNER_BASE_URL");
const url = new URL(path, baseUrl).toString();
try {
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

🧩 Analysis chain

🏁 Script executed:

cd /tmp && find . -name "external-signer-http-sdk.service.ts" 2>/dev/null | head -5

Repository: akash-network/console

Length of output: 47


🏁 Script executed:

fd "external-signer-http-sdk.service.ts" --type f

Repository: akash-network/console

Length of output: 155


🏁 Script executed:

cat -n apps/api/src/billing/services/external-signer-http-sdk/external-signer-http-sdk.service.ts

Repository: akash-network/console

Length of output: 4329


🏁 Script executed:

rg -n "TX_SIGNER_BASE_URL" -g'*.ts'

Repository: akash-network/console

Length of output: 309


🏁 Script executed:

fd "billing-config.service.ts" --type f

Repository: akash-network/console

Length of output: 135


🏁 Script executed:

cat -n apps/api/src/billing/services/billing-config/billing-config.service.ts

Repository: akash-network/console

Length of output: 787


🏁 Script executed:

cat -n apps/api/src/billing/config/env.config.ts

Repository: akash-network/console

Length of output: 2282


Move URL construction into the try block to ensure errors are logged consistently.

new URL(path, baseUrl) on line 73 can throw for invalid URL formats before reaching the try block, bypassing your standardized LoggerService error handling. Although TX_SIGNER_BASE_URL is validated as a non-empty string at startup (env.config.ts line 40), string validation does not guarantee valid URL format. Move the URL construction into the try block so all failures are logged via LoggerService.

🛠️ Suggested minimal fix
  private async post<T>(path: string, body: unknown): Promise<T> {
    const baseUrl = this.billingConfigService.get("TX_SIGNER_BASE_URL");
+   try {
-   const url = new URL(path, baseUrl).toString();
-   try {
+     const url = new URL(path, baseUrl).toString();
📝 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
private async post<T>(path: string, body: unknown): Promise<T> {
const baseUrl = this.billingConfigService.get("TX_SIGNER_BASE_URL");
const url = new URL(path, baseUrl).toString();
try {
private async post<T>(path: string, body: unknown): Promise<T> {
const baseUrl = this.billingConfigService.get("TX_SIGNER_BASE_URL");
try {
const url = new URL(path, baseUrl).toString();
🤖 Prompt for AI Agents
In
`@apps/api/src/billing/services/external-signer-http-sdk/external-signer-http-sdk.service.ts`
around lines 71 - 74, The URL is constructed outside the try in the post method
which can throw (new URL(path, baseUrl)) and bypass LoggerService error
handling; move the URL construction (the call to
billingConfigService.get("TX_SIGNER_BASE_URL") and the new URL(path,
baseUrl).toString() usage) inside the try block in the post<T>(path: string,
body: unknown) method so any exceptions from invalid base URLs or path are
caught and logged by LoggerService within the existing try/catch.

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

🤖 Fix all issues with AI agents
In `@apps/deploy-web/src/queries/useTemplateQuery.tsx`:
- Around line 155-161: The query currently sets staleTime and gcTime to
Number.POSITIVE_INFINITY and disables all refetch triggers, so clients never see
backend template updates; change the cache policy in useTemplateQuery.tsx by
replacing Number.POSITIVE_INFINITY for staleTime/gcTime with a bounded duration
(e.g., 5–15 minutes) or enable refetch triggers (set refetchOnWindowFocus: true
and/or refetchOnReconnect: true), and/or ensure callers invalidate
QueryKeys.getTemplatesKey() when templates are updated (e.g., after manual
refresh action); update the options spread around staleTime/gcTime/refetch* in
the query config so QueryKeys.getTemplatesKey()/useTemplateQuery will receive
fresh data.

@ygrishajev ygrishajev merged commit 8a74b7e into main Jan 30, 2026
29 of 31 checks passed
@ygrishajev ygrishajev deleted the feature/tx-signer branch January 30, 2026 14:30
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.

3 participants

Comments