Skip to content

Comments

feat: get_channels RPC, GetAppSessions fix, compat SDK & tests#573

Merged
alessio merged 11 commits intomainfrom
feat/compat-layer-and-get-channels
Feb 23, 2026
Merged

feat: get_channels RPC, GetAppSessions fix, compat SDK & tests#573
alessio merged 11 commits intomainfrom
feat/compat-layer-and-get-channels

Conversation

@ihsraham
Copy link
Collaborator

@ihsraham ihsraham commented Feb 22, 2026

Supersedes #572 (closed due to fork-based PR preventing Docker image push in CI).

What this PR does

Three related changes from the v0.5.3 → v1.0.0 migration effort (YNU-788):

  1. channels.v1.get_channels endpoint — New RPC method to list all channels (open, closed, challenged) for a wallet, with status/asset filtering and pagination. The v0.5.3 appstore displayed closed channels; v1.0.0 had no equivalent endpoint.

  2. GetAppSessions PostgreSQL fix — The existing query combined DISTINCT with ORDER BY created_at, which PostgreSQL rejects (SQLSTATE 42P10). Replaced with a subquery approach. This bug was hidden in tests because SQLite (the test DB) is lenient with this pattern.

  3. @erc7824/nitrolite-compat SDK (WIP) — Compatibility layer that wraps the v1.0.0 TS SDK behind the v0.5.3 API surface, reducing migration diffs from ~400+ lines to ~15 lines for existing apps.


Changes by area

Clearnode

File Change
api/channel_v1/get_channels.go New handler — parses request, queries store, builds paginated response
api/channel_v1/get_channels_test.go 6 handler tests (success, filters, pagination, empty, validation, error)
store/database/channel.go GetUserChannels() — GORM query with optional status/asset filters + count
store/database/channel_test.go 6 DB tests (retrieval, status/asset filter, pagination, empty, invalid status)
store/database/app_session.go Fix: DISTINCT → subquery to resolve PostgreSQL ORDER BY conflict
api/channel_v1/interface.go GetUserChannels added to Store interface
api/channel_v1/testing.go Mock implementation
store/database/interface.go GetUserChannels added to DatabaseStore interface
api/rpc_router.go Route registration

TypeScript SDK

File Change
sdk/ts/src/client.ts Client.getChannels(wallet, options?) wrapping the new RPC method

Compat SDK (sdk/compat/) — WIP

Area Files
Core client.ts, auth.ts, signers.ts, rpc.ts, events.ts, config.ts, errors.ts, types.ts, index.ts
Docs README.md, docs/migration-overview.md, docs/migration-onchain.md, docs/migration-offchain.md
Example examples/lifecycle.ts

WIP note: The compat package itself is complete and documented. Integration testing with beatwav and the nitrolite example app is pending.


GetAppSessions fix — detail

Before: GetAppSessions used Distinct("app_sessions_v1.id") + Order("created_at DESC"). PostgreSQL requires ORDER BY columns to appear in the DISTINCT select list, so this failed with:

ERROR: for SELECT DISTINCT, ORDER BY expressions must appear in select list (SQLSTATE 42P10)

All get_app_sessions RPC calls were broken on production PostgreSQL.

After: Deduplication moved to a subquery (WHERE id IN (SELECT DISTINCT id ... JOIN participants WHERE wallet = ?)), keeping the main query free to ORDER BY without conflict. Existing tests pass (SQLite). Verified manually against local PostgreSQL clearnode.


get_channels API

const result = await client.getChannels('0x...', {
  status: 'closed',                      // optional
  asset: 'usdc',                         // optional
  pagination: { limit: 50, offset: 0 }   // optional
});
// result.channels: Channel[]
// result.metadata: { page, perPage, totalCount, pageCount }

Test coverage

Area Tests Status
get_channels handler 6 (success, status filter, pagination, empty, missing wallet, store error)
GetUserChannels DB 6 (multi-channel, status filter, asset filter, pagination, empty, invalid status)
Existing channel_v1 suite All passing
Existing store/database suite All passing

Summary by CodeRabbit

  • New Features

    • Channel listing: status/asset/channel-type filters, offset pagination with page metadata (page, perPage, totalCount, pageCount), sensible defaults and limits.
    • SDK: client getChannels method and Nitrolite Compat SDK (browser-friendly client, signers, RPC compatibility layer, event poller, asset helpers, convenience APIs).
  • Documentation

    • Compat README, migration guides (overview, on-chain, off-chain), and examples.
  • Tests

    • Unit tests for channel retrieval, filters, pagination, and DB behavior.
  • Chores

    • Compat package setup and .gitignore updates.

Add full-stack support for listing user channels with optional filtering
and pagination, enabling apps to retrieve all channels including closed ones.

Clearnode (Go):
- Add GetUserChannels to database interface with status/asset filters and pagination
- Implement GetUserChannels query using GORM with dynamic WHERE clauses
- Add RPC handler for channels.v1.get_channels with request validation
- Register the new method in rpc_router
- Add mock implementation for testing

SDK (TypeScript):
- Add getChannels() method to Client, wrapping channelsV1GetChannels RPC
- Transform RPC response into typed Channel[] with PaginationMetadata

Tested end-to-end with local clearnode Docker setup against Sepolia.
- channelStatusFromString now returns error on unknown status instead
  of silently defaulting to Void
- Centralise default limit=100 in handler before store call, removing
  the duplicate default in the store layer
- Remove redundant limit>0 guards (limit is guaranteed non-zero after
  the handler default is applied)
- Remove unsupported sort field from SDK PaginationParams and
  getChannels request mapping (backend does not implement sort)
The GetAppSessions query used SELECT DISTINCT app_sessions_v1.id with
ORDER BY created_at, which PostgreSQL rejects because created_at is not
in the SELECT list. Replace with a subquery (WHERE id IN (SELECT id ...))
to filter by participant without conflicting with ORDER BY.
Compatibility layer bridging v0.5.3 API to v1.0.0 TS SDK, enabling
minimal-diff migrations for existing apps. Includes client, auth,
signers, RPC helpers, event polling, config, and comprehensive
documentation with migration guides.

WIP: beatwav and nitrolite example app integrations pending testing.
Handler tests (6): success, status filter, pagination, empty result,
missing wallet validation, and store error propagation.

Database tests (6): multi-channel retrieval, status filtering, asset
filtering, pagination limiting, empty result for unknown user, and
invalid status filter rejection.
@ihsraham ihsraham requested a review from a team as a code owner February 22, 2026 17:11
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 22, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a Channels GetChannels API with optional status/asset/channel_type filters and offset pagination, store and DB support (GetUserChannels), RPC wiring and SDK client method, unit and DB tests, and a large Nitrolite compat SDK providing a compat client, types, signers, stubs, docs, and examples.

Changes

Cohort / File(s) Summary
API Handler & Tests
clearnode/api/channel_v1/get_channels.go, clearnode/api/channel_v1/get_channels_test.go
New Handler.GetChannels endpoint: request parsing/validation, wallet requirement, pagination (default 100, max 1000), calls store GetUserChannels, maps to RPC DTO, returns pagination metadata; includes unit tests for success, filters, pagination and error cases.
Store Interface & Mocks
clearnode/api/channel_v1/interface.go, clearnode/api/channel_v1/testing.go
Adds Store.GetUserChannels(...) to Store interface and a mock implementation used by tests.
DB Store & Tests
clearnode/store/database/channel.go, clearnode/store/database/channel_test.go, clearnode/store/database/interface.go
Implements DBStore.GetUserChannels with filters (wallet, status, asset, channel type), total count + paginated rows ordered by created_at; adds DB tests (duplicate test block present).
RPC Router & Types
clearnode/api/rpc_router.go, pkg/rpc/api.go, sdk/ts/src/rpc/api.ts
Registers RPC route for ChannelV1.GetChannels and extends ChannelsV1GetChannelsRequest with optional channel_type field.
SDK TypeScript Client & Types
sdk/ts/src/client.ts, sdk/ts/src/core/types.ts, sdk/ts/src/rpc/types.ts
Adds Client.getChannels(...) mapping to the new RPC, and removes sort from pagination types (type deletions in pagination interfaces).
App sessions DB tweak
clearnode/store/database/app_session.go
Replaces JOIN+DISTINCT participant lookup with a subquery IN filter for app session participant queries.
Nitrolite Compat SDK
sdk/compat/... (package.json, README.md, src/*, docs/*, examples/*, tsconfig*, .gitignore)
Adds a new compat package @erc7824/nitrolite-compat: NitroliteClient facade, signers, auth stubs, RPC stubs, EventPoller, rich types, error classes, migration docs, examples, build configs and packaging.
Misc
sdk/compat/.gitignore
Adds common ignore entries for the new compat package.

Sequence Diagram(s)

sequenceDiagram
    participant SDK as SDK Client (TS)
    participant RPC as RPC Router
    participant Handler as channel_v1.Handler
    participant Store as Store (DBStore)
    participant DB as Database

    SDK->>RPC: channelsV1GetChannels(request)
    RPC->>Handler: invoke GetChannels
    Handler->>Store: GetUserChannels(wallet, status, asset, type, limit, offset)
    Store->>DB: COUNT(*) and SELECT rows with WHERE, ORDER BY created_at DESC LIMIT/OFFSET
    DB-->>Store: rows, totalCount
    Store-->>Handler: []core.Channel, totalCount
    Handler-->>RPC: ChannelsV1GetChannelsResponse (channels + pagination)
    RPC-->>SDK: response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

ready

Suggested reviewers

  • philanton
  • dimast-x
  • nksazonov

Poem

"I hopped through code with nimble paws,
New channels sprouted from my claws.
Pages, filters, tests in tune,
I left a carrot for the prune.
— a rabbit, debugging by the moon 🐇"

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.54% 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 'feat: get_channels RPC, GetAppSessions fix, compat SDK & tests' clearly summarizes the main changes: a new RPC endpoint, a bug fix, and SDK additions with test coverage.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/compat-layer-and-get-channels

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

❤️ Share

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

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @ihsraham, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the clearnode's API by introducing a robust method for fetching user channels, which was a missing feature in the v1.0.0 migration. It also addresses a crucial database query issue affecting app session retrieval in PostgreSQL, ensuring data consistency and application stability. Furthermore, a new compatibility SDK is provided to streamline the transition for dApps upgrading from the previous SDK version, minimizing the development overhead associated with protocol changes.

Highlights

  • New get_channels RPC Endpoint: Introduced a new RPC method (channels.v1.get_channels) to retrieve all channels for a user, supporting optional status and asset filtering, as well as pagination. This restores functionality previously available in v0.5.3.
  • GetAppSessions PostgreSQL Fix: Resolved a critical bug in the GetAppSessions database query that caused errors in PostgreSQL due to an incompatibility between DISTINCT and ORDER BY clauses. The fix employs a subquery approach to ensure correct and deterministic results.
  • Nitrolite Compat SDK (@erc7824/nitrolite-compat): Added a new TypeScript compatibility layer designed to simplify the migration of existing dApps from the v0.5.3 SDK to the v1.0.0 runtime. This SDK wraps the new API behind the old surface, significantly reducing code changes for dApp developers.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • clearnode/api/channel_v1/get_channels.go
    • Added new handler for the channels.v1.get_channels RPC method, enabling channel retrieval with filtering and pagination.
  • clearnode/api/channel_v1/get_channels_test.go
    • Added comprehensive tests for the new get_channels handler, covering success cases, filtering, pagination, and error handling.
  • clearnode/api/channel_v1/interface.go
    • Added GetUserChannels method to the Store interface, defining the contract for retrieving user channels.
  • clearnode/api/channel_v1/testing.go
    • Implemented the GetUserChannels method in the MockStore for testing purposes.
  • clearnode/api/rpc_router.go
    • Registered the new channels.v1.get_channels RPC route to make the endpoint accessible.
  • clearnode/store/database/app_session.go
    • Fixed the GetAppSessions query to resolve a PostgreSQL DISTINCT and ORDER BY conflict by using a subquery for deduplication.
  • clearnode/store/database/channel.go
    • Implemented the GetUserChannels method, providing database logic for fetching channels with status/asset filters and pagination.
  • clearnode/store/database/channel_test.go
    • Added new tests for the GetUserChannels database function, verifying its filtering, pagination, and error handling capabilities.
  • clearnode/store/database/interface.go
    • Added GetUserChannels method to the DatabaseStore interface.
  • sdk/compat/.gitignore
    • Added a new .gitignore file for the @erc7824/nitrolite-compat SDK.
  • sdk/compat/README.md
    • Added comprehensive README.md detailing the purpose, installation, quick start, and method cheat sheet for the compat SDK.
  • sdk/compat/docs/migration-offchain.md
    • Added a migration guide specifically for off-chain operations when moving to the compat layer.
  • sdk/compat/docs/migration-onchain.md
    • Added a migration guide specifically for on-chain operations when moving to the compat layer.
  • sdk/compat/docs/migration-overview.md
    • Added an overview migration guide explaining the benefits and key changes when adopting the compat layer.
  • sdk/compat/examples/lifecycle.ts
    • Added an example script demonstrating the full lifecycle of using the compat SDK to interact with a clearnode.
  • sdk/compat/package-lock.json
    • Added package-lock.json to manage dependencies for the new compat SDK.
  • sdk/compat/package.json
    • Added package.json defining the new @erc7824/nitrolite-compat SDK, its dependencies, and scripts.
  • sdk/compat/src/auth.ts
    • Added authentication stubs as no-op functions to ensure compatibility with existing v0.5.3 auth code.
  • sdk/compat/src/client.ts
    • Implemented the NitroliteClient compatibility facade, bridging v0.5.3 API calls to the v1.0.0 SDK.
  • sdk/compat/src/config.ts
    • Added configuration utilities, including buildClientOptions and blockchainRPCsFromEnv, for the compat SDK.
  • sdk/compat/src/errors.ts
    • Added custom error classes (AllowanceError, UserRejectedError, etc.) and a utility for user-facing messages.
  • sdk/compat/src/events.ts
    • Implemented an EventPoller to simulate v0.5.3's push-based event model using polling for the v1.0.0 SDK.
  • sdk/compat/src/index.ts
    • Created a barrel export file to consolidate and re-export all public modules of the compat SDK.
  • sdk/compat/src/rpc.ts
    • Added RPC helper stubs and pass-through functions to maintain compatibility with v0.5.3's RPC message creation and parsing patterns.
  • sdk/compat/src/signers.ts
    • Added compatibility classes for signers (WalletStateSigner, createECDSAMessageSigner) to align with v0.5.3 interfaces.
  • sdk/compat/src/types.ts
    • Defined various types and enums to ensure type compatibility between the v0.5.3 API and the v1.0.0 SDK.
  • sdk/compat/tsconfig.json
    • Added tsconfig.json for the new compat SDK, configuring TypeScript compilation options.
  • sdk/compat/tsconfig.prod.json
    • Added tsconfig.prod.json for production builds of the compat SDK, extending the base configuration.
  • sdk/ts/src/client.ts
    • Added the getChannels method to the main TypeScript SDK client, allowing retrieval of user channels with filters and pagination.
  • sdk/ts/src/core/types.ts
    • Removed the sort property from the PaginationParams interface.
  • sdk/ts/src/rpc/types.ts
    • Removed the sort property from the PaginationParamsV1 interface.
Activity
  • This pull request supersedes a previous attempt (feat: implement channels.v1.get_channels RPC method #572) which was closed due to issues with Docker image pushes in CI, stemming from a fork-based PR.
  • The author, ihsraham, has implemented three core changes as part of the ongoing v0.5.3 to v1.0.0 migration effort, tracked under YNU-788.
  • The compatibility package is noted as complete, with integration testing against beatwav and the nitrolite example app pending.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@ihsraham ihsraham self-assigned this Feb 22, 2026
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

The pull request introduces a new get_channels RPC endpoint and a compatibility layer for the TypeScript SDK to facilitate migration from v0.5.3 to v1.0.0. The Go implementation for the new endpoint is well-structured and includes thorough tests. The PostgreSQL fix for GetAppSessions correctly resolves a known issue with DISTINCT and ORDER BY. However, the compatibility SDK contains several critical logic errors, particularly regarding the handling of asset amounts (mixing raw units and human-readable decimals) and a hardcoded chain ID in the EIP-712 domain. Additionally, there are some pagination and efficiency concerns in both the Go and TypeScript code that should be addressed to ensure production readiness.

High priority:
- Use wallet chain ID instead of hardcoded chainId=1 in EIP-712 domain
- Convert raw-unit amounts to human-readable decimals before passing
  to v1 SDK in submitAppState, closeAppSession, and transfer
- Convert v1 human-readable amounts back to raw units in
  getAppSessionsList allocations for v0.5.3 compatibility

Medium priority:
- Cap get_channels pagination limit to 1000 to prevent abuse
- Use uint64 for pageCount calculation to prevent uint32 overflow
- Add lazy-load retry for asset map (ensureAssets on every resolve)
- Replace cross-decimal balance sum with per-asset balances in AccountInfo
- Optimize closeChannel to query open channels instead of all assets
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: 10

🧹 Nitpick comments (16)
clearnode/store/database/app_session.go (2)

101-105: PostgreSQL DISTINCT/ORDER BY fix looks correct — minor simplification available.

GORM v2 automatically generates subqueries when a *gorm.DB is passed as a parameter, and the Where("... > (?)", db.Table(...).Select(...)) form with parenthesised ? is the documented idiomatic pattern — so Where("id IN (?)", subQuery) is correct here.

The fix correctly removes SELECT DISTINCT … ORDER BY from within a single SELECT (the PostgreSQL SQLSTATE 42P10 trigger) and pushes deduplication into the SQL engine via IN, which handles duplicates implicitly.

That said, the subquery joins back through app_sessions_v1 purely to project its id column — an extra table hop that isn't needed. AppParticipantV1 already holds app_session_id, so the join can be eliminated:

♻️ Simplified subquery (drop the unnecessary JOIN)
-	subQuery := s.db.Model(&AppSessionV1{}).
-		Select("app_sessions_v1.id").
-		Joins("JOIN app_session_participants_v1 ON app_sessions_v1.id = app_session_participants_v1.app_session_id").
-		Where("app_session_participants_v1.wallet_address = ?", strings.ToLower(*participant))
+	subQuery := s.db.Model(&AppParticipantV1{}).
+		Select("app_session_id").
+		Where("wallet_address = ?", strings.ToLower(*participant))
	query = query.Where("id IN (?)", subQuery)

This generates WHERE id IN (SELECT app_session_id FROM app_session_participants_v1 WHERE wallet_address = ?), which is equivalent and avoids the join entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clearnode/store/database/app_session.go` around lines 101 - 105, The subquery
currently builds from AppSessionV1 and joins to app_session_participants_v1
unnecessarily; instead construct subQuery from the AppParticipantV1 model
selecting app_session_id with the wallet_address filter (use
strings.ToLower(*participant)) and keep the outer filter query = query.Where("id
IN (?)", subQuery); update references to subQuery, AppSessionV1,
AppParticipantV1 and participant accordingly to remove the JOIN and select the
app_session_id directly.

101-105: PostgreSQL DISTINCT/ORDER BY fix looks correct.

The subquery approach sidesteps the SELECT DISTINCT … ORDER BY incompatibility (SQLSTATE 42P10) entirely. SQL IN implicitly deduplicates, so no explicit DISTINCT is needed, and the outer query's ORDER BY created_at DESC remains unaffected.

That said, the subquery joins app_sessions_v1 to app_session_participants_v1 purely to project back app_sessions_v1.id—a JOIN that can be eliminated by querying AppParticipantV1 directly:

♻️ Simplified subquery (avoids the unnecessary JOIN)
-	subQuery := s.db.Model(&AppSessionV1{}).
-		Select("app_sessions_v1.id").
-		Joins("JOIN app_session_participants_v1 ON app_sessions_v1.id = app_session_participants_v1.app_session_id").
-		Where("app_session_participants_v1.wallet_address = ?", strings.ToLower(*participant))
+	subQuery := s.db.Model(&AppParticipantV1{}).
+		Select("app_session_id").
+		Where("wallet_address = ?", strings.ToLower(*participant))
	query = query.Where("id IN (?)", subQuery)

Note: GORM v2's Where("id IN (?)", subQuery) correctly renders a *gorm.DB value as an inline subquery—this is the idiomatic v2 pattern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clearnode/store/database/app_session.go` around lines 101 - 105, The subquery
currently joins AppSessionV1 to app_session_participants_v1 just to return
session IDs; replace that JOIN by building the subquery from AppParticipantV1
directly (e.g.
s.db.Model(&AppParticipantV1{}).Select("app_session_id").Where("wallet_address =
?", strings.ToLower(*participant))) and then use query = query.Where("id IN
(?)", subQuery) so you avoid the unnecessary JOIN while keeping the outer ORDER
BY and leveraging GORM v2's inline subquery rendering.
sdk/compat/tsconfig.prod.json (1)

4-4: declaration: true is redundant — the base config already sets it.

Removing it from tsconfig.prod.json reduces noise without changing behavior.

♻️ Proposed cleanup
 {
     "extends": "./tsconfig.json",
     "compilerOptions": {
-        "declaration": true,
         "declarationMap": false,
         "sourceMap": false,
         "removeComments": true,
         "inlineSources": false
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/compat/tsconfig.prod.json` at line 4, Remove the redundant "declaration":
true setting from tsconfig.prod.json: locate the JSON key "declaration" in
tsconfig.prod.json (the explicit declaration property) and delete that entry
since the base config already provides it, leaving the prod config to inherit
the declaration setting.
clearnode/api/channel_v1/interface.go (1)

61-62: Same pagination convention inconsistency as clearnode/store/database/interface.go line 30.

For consistency with how pagination is expressed elsewhere in the Store interface (and the underlying DatabaseStore), consider using *core.PaginationParams and returning core.PaginationMetadata here as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clearnode/api/channel_v1/interface.go` around lines 61 - 62, The
GetUserChannels method signature in the channel API interface uses raw
limit/offset which is inconsistent with the Store/DatabaseStore pagination
convention; update the GetUserChannels declaration to accept a
*core.PaginationParams instead of limit and offset and change its return to
include core.PaginationMetadata (i.e., GetUserChannels(wallet string, status
*string, asset *string, params *core.PaginationParams) ([]core.Channel,
core.PaginationMetadata, error)) so it matches the Store interface and
DatabaseStore implementations (update callers and implementations of
GetUserChannels accordingly).
sdk/ts/src/client.ts (1)

1075-1095: Pagination API inconsistency with getTransactions / getAppSessions.

getTransactions (line 1044) and getAppSessions (line 1194) both accept { page, pageSize } and compute offset/limit internally. getChannels exposes the lower-level core.PaginationParams directly. SDK consumers now face two different pagination shapes for equivalent operations.

♻️ Proposed alignment
  async getChannels(
    wallet: Address,
-   options?: { status?: string; asset?: string; pagination?: core.PaginationParams }
+   options?: { status?: string; asset?: string; page?: number; pageSize?: number }
  ): Promise<{ channels: core.Channel[]; metadata: core.PaginationMetadata }> {
    const req: API.ChannelsV1GetChannelsRequest = {
      wallet,
      status: options?.status,
      asset: options?.asset,
-     pagination: options?.pagination
-       ? {
-           offset: options.pagination.offset,
-           limit: options.pagination.limit,
-         }
-       : undefined,
+     pagination: options?.page && options?.pageSize ? {
+       offset: (options.page - 1) * options.pageSize,
+       limit: options.pageSize,
+     } : undefined,
    };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/ts/src/client.ts` around lines 1075 - 1095, getChannels currently accepts
core.PaginationParams (offset/limit) while getTransactions and getAppSessions
accept {page, pageSize}; update getChannels signature and request construction
to match the higher-level pagination shape for consistency: change
getChannels(wallet, options?: { status?: string; asset?: string; pagination?: {
page?: number; pageSize?: number } }) and inside getChannels convert
page/pageSize to offset = (page-1)*pageSize and limit = pageSize when building
the API.ChannelsV1GetChannelsRequest; keep transformChannel and
transformPaginationMetadata usage unchanged and ensure any callers/tests are
updated to the new pagination shape (or add a thin adapter if you must preserve
backward compatibility with core.PaginationParams).
clearnode/store/database/interface.go (1)

53-54: GetUserChannels pagination style diverges from GetUserTransactions.

GetUserTransactions (line 30) takes *core.PaginationParams and returns the richer core.PaginationMetadata, while GetUserChannels uses flat limit, offset uint32 and returns a bare uint32 total count. The calling handler must reconstruct pagination metadata manually. Consider aligning signatures for a consistent persistence interface.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clearnode/store/database/interface.go` around lines 53 - 54, The
GetUserChannels method uses flat limit/offset and a bare uint32 total which
diverges from GetUserTransactions; update the GetUserChannels signature to
accept *core.PaginationParams (like GetUserTransactions) and return the richer
pagination result (e.g., *core.PaginationMetadata or the same type returned by
GetUserTransactions) along with []core.Channel and error, then update all
implementations and call sites to construct/consume core.PaginationParams and
the metadata instead of separate limit/offset and total count.
sdk/compat/package.json (1)

43-48: @types/node@^25.3.0 overreaches the declared engine minimum (>=20.0.0).

@types/node major and minor version conventions dictate that they must match your Node.js environment's major and minor version. With engines.node >= 20.0.0, developers running on Node.js 20 or 22 will have type definitions for Node.js 25 APIs that are not available in their runtime, risking false-confidence type errors.

Consider pinning to the minimum supported major:

diff
-        "@types/node": "^25.3.0",
+        "@types/node": "^20.0.0",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/compat/package.json` around lines 43 - 48, The devDependency
"@types/node" is set to "^25.3.0" which is incompatible with the declared
engines.node >=20.0.0; change the entry in package.json devDependencies
("@types/node") to a version that matches the minimum supported Node major
(e.g., "^20.0.0" or a 20.x.x pin) so the type definitions align with Node 20,
then run your package manager to update the lockfile; edit the package.json
devDependencies block where "@types/node" appears to make this change.
clearnode/store/database/channel_test.go (1)

765-784: Consider adding an offset/page-2 sub-test.

The pagination test verifies that limit=2 returns 2 rows out of 5 with the correct total, but no sub-test exercises offset > 0 to confirm the result window is correctly shifted. A case like limit=2, offset=2 should return channels 3–4 and total=5, which would also validate the page-number metadata computed in the handler.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clearnode/store/database/channel_test.go` around lines 765 - 784, Add a
sub-test to the existing "Success - Pagination limits results" test that calls
store.GetUserChannels with limit=2 and offset=2 to verify pagination shifting;
after creating the five channels via NewDBStore and CreateChannel, call
GetUserChannels("0xuser_pg", nil, nil, 2, 2), assert no error, assert the
returned slice length is 2, assert total equals uint32(5), and optionally assert
the returned ChannelIDs match the expected "0xch_pg_2" and "0xch_pg_3" to ensure
the window is correctly shifted.
clearnode/api/channel_v1/get_channels.go (1)

39-51: Read-only handler wrapped in a write transaction.

GetChannels only reads data, but useStoreInTx executes it inside ExecuteInTransaction (a write transaction). For a high-frequency query endpoint this adds unnecessary lock acquisition overhead. Consider either adding a read-only store accessor or using dbStore directly without a transaction for read-only handlers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clearnode/api/channel_v1/get_channels.go` around lines 39 - 51, The
GetChannels handler is performing a read-only operation inside useStoreInTx
(which invokes ExecuteInTransaction / a write transaction); change it to avoid
write transactions by either calling the store read method directly (replace the
useStoreInTx block and call h.dbStore.GetUserChannels(req.Wallet, req.Status,
req.Asset, limit, offset) and handle the returned channels/totalCount/error) or
add a dedicated read-only helper (e.g., useStoreReadOnly that wraps an
ExecuteReadOnlyTransaction / read-only accessor) and call Store.GetUserChannels
inside that helper; update error handling to behave exactly like before but
without acquiring a write transaction.
sdk/compat/examples/lifecycle.ts (1)

52-56: Hardcoded sepolia chain may conflict with CHAIN_ID env var.

The wallet client is always created with the sepolia chain config, but CHAIN_ID can be overridden to a different chain. For an example file this is understandable, but a brief comment noting this limitation would help users who copy-paste the example for other networks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/compat/examples/lifecycle.ts` around lines 52 - 56, The example hardcodes
the sepolia chain when creating walletClient via createWalletClient({ chain:
sepolia, ... }), which can conflict if callers set a different CHAIN_ID; update
the example to either derive the chain from the CHAIN_ID env var (use a simple
mapping or helper to convert CHAIN_ID to the correct chain constant) or add a
one-line comment above the createWalletClient call explicitly stating that
sepolia is hardcoded for the example and should be replaced when using a
different CHAIN_ID so users who copy-paste don’t accidentally target the wrong
network.
clearnode/api/channel_v1/get_channels_test.go (1)

16-34: Ignored error from NewChannelDefaultSigner in test helper.

Line 19 discards the error from core.NewChannelDefaultSigner. If the mock setup ever changes and this starts returning an error, all tests using newGetChannelsHandler will silently get a nil nodeSigner and panic with an unhelpful stack trace. Consider accepting t *testing.T and using require.NoError.

🔧 Proposed fix
-func newGetChannelsHandler(mockTxStore *MockStore) *Handler {
+func newGetChannelsHandler(t *testing.T, mockTxStore *MockStore) *Handler {
+	t.Helper()
 	mockAssetStore := new(MockAssetStore)
 	mockSigner := NewMockSigner()
-	nodeSigner, _ := core.NewChannelDefaultSigner(mockSigner)
+	nodeSigner, err := core.NewChannelDefaultSigner(mockSigner)
+	require.NoError(t, err)
 	nodeAddress := mockSigner.PublicKey().Address().String()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clearnode/api/channel_v1/get_channels_test.go` around lines 16 - 34, The
helper newGetChannelsHandler ignores the error returned by
core.NewChannelDefaultSigner causing silent nil nodeSigner; change
newGetChannelsHandler to accept t *testing.T, call nodeSigner, err :=
core.NewChannelDefaultSigner(mockSigner) and assert require.NoError(t, err) (or
t.Fatalf) before using nodeSigner, and update all test callers to pass the
testing.T from their tests so the helper fails loudly if signer creation errors;
refer to newGetChannelsHandler and core.NewChannelDefaultSigner when making the
change.
sdk/compat/src/rpc.ts (1)

61-64: Parse stubs will throw on invalid JSON, unlike parseAnyRPCResponse.

parseAnyRPCResponse wraps JSON.parse in try/catch, but all the parse*Response stubs (e.g., parseGetChannelsResponse, etc.) do not. If any remaining callsite passes non-JSON input, these will throw an unhandled error. Since these are documented no-op stubs and callers should migrate to NitroliteClient methods, this is low risk — but worth noting for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/compat/src/rpc.ts` around lines 61 - 64, The parseGetChannelsResponse
stub currently calls JSON.parse(raw) directly and will throw on invalid JSON;
update parseGetChannelsResponse to mirror parseAnyRPCResponse by wrapping
JSON.parse in a try/catch and returning the safe default shape ({ params: {
channels: [] } }) when parsing fails or the expected path is missing; locate the
function parseGetChannelsResponse and adjust the parsing block to catch any JSON
errors (optionally log or ignore) and return the no-op default instead of
allowing an exception to propagate.
sdk/compat/src/types.ts (1)

95-104: RPCLedgerEntry and LedgerEntry have identical shapes.

Both interfaces define the same fields (id, account_id, account_type, asset, participant, credit, debit, created_at). If they're intentionally kept separate for wire-format vs. client-response clarity, consider a brief comment noting the relationship to prevent accidental divergence during future changes.

Also applies to: 306-315

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/compat/src/types.ts` around lines 95 - 104, RPCLedgerEntry and
LedgerEntry currently have identical shapes; locate the RPCLedgerEntry and
LedgerEntry interfaces and either replace one with a type alias (e.g., "export
type RPCLedgerEntry = LedgerEntry" or vice versa) to prevent accidental
divergence, or add a short comment above both definitions explaining they
intentionally mirror each other (wire-format vs client-response) so future
maintainers don't change one without the other; update both occurrences (the one
at RPCLedgerEntry and the duplicate at lines referenced around LedgerEntry)
accordingly.
sdk/compat/src/client.ts (3)

569-591: Magic number 3 for close intent — use INTENT_MAP or a named constant.

Line 579 uses intent: 3 directly, while a INTENT_MAP is already defined at lines 605-610 with close: 3. Consider reusing that to keep intent values consistent and readable.

Proposed fix
         const appUpdate = {
             appSessionId,
-            intent: 3, // Close
+            intent: NitroliteClient.INTENT_MAP['close'],
             version: session.version + 1n,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/compat/src/client.ts` around lines 569 - 591, Replace the magic literal
intent: 3 in closeAppSession with the named intent from the existing map;
reference INTENT_MAP.close (or introduce a local constant like INTENT_CLOSE =
INTENT_MAP.close) and use that instead in the appUpdate object so intent values
are readable and consistent with the rest of the file (change usage in
closeAppSession to use INTENT_MAP.close or the new INTENT_CLOSE identifier).

262-271: Double classification of errors is redundant and obscures the logic.

Line 266 checks !(err instanceof AllowanceError) and then !(NitroliteClient.classifyError(err) instanceof AllowanceError). Since classifyError already handles AllowanceError instances (they pass through at line 259), the first instanceof check is redundant. Additionally, if err is not an AllowanceError and classification also doesn't produce one, the code throws the classified error — which is correct. Simplify to a single classification call.

Simplified version
     private async checkpointWithApproval(symbol: string, chainId: bigint, tokenAddress: string): Promise<any> {
         try {
             return await this.innerClient.checkpoint(symbol);
         } catch (err) {
-            if (!(err instanceof AllowanceError) && !(NitroliteClient.classifyError(err) instanceof AllowanceError)) throw NitroliteClient.classifyError(err);
+            const classified = NitroliteClient.classifyError(err);
+            if (!(classified instanceof AllowanceError)) throw classified;
             console.log('[compat] Allowance insufficient, requesting token approval…');
             await this.innerClient.approveToken(chainId, tokenAddress, NitroliteClient.MAX_UINT256);
             return await this.innerClient.checkpoint(symbol);
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/compat/src/client.ts` around lines 262 - 271, In checkpointWithApproval,
the error handling redundantly checks both err instanceof AllowanceError and
NitroliteClient.classifyError(err) instanceof AllowanceError; replace this with
a single classification: call NitroliteClient.classifyError(err) once, store the
result, and if that classified error is not an AllowanceError re-throw it;
otherwise proceed to log the allowance message, call
this.innerClient.approveToken(chainId, tokenAddress,
NitroliteClient.MAX_UINT256) and retry this.innerClient.checkpoint(symbol).
Ensure references to AllowanceError, NitroliteClient.classifyError,
checkpointWithApproval, innerClient.checkpoint, innerClient.approveToken, and
NitroliteClient.MAX_UINT256 are used to locate the change.

277-283: setHomeBlockchain errors are silently swallowed.

Line 279 uses .catch(() => {}) to suppress all errors from setHomeBlockchain. If this call fails for a legitimate reason (e.g., network error, invalid symbol), the subsequent deposit will likely fail with a confusing error. The same pattern appears at line 333. Consider at least logging the error.

Proposed fix
-        await this.innerClient.setHomeBlockchain(symbol, chainId).catch(() => {});
+        await this.innerClient.setHomeBlockchain(symbol, chainId).catch((err) => {
+            console.warn('[compat] setHomeBlockchain failed, continuing:', (err as Error).message);
+        });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/compat/src/client.ts` around lines 277 - 283, In deposit (and the other
occurrence) do not silently swallow errors from
this.innerClient.setHomeBlockchain; instead catch the error, log a descriptive
message including symbol and chainId and the error object (e.g., via
this.logger.error or equivalent) so failures are visible, and decide whether to
rethrow or continue based on expected behavior — update the setHomeBlockchain
call in deposit and the similar call at the other site to use a .catch(err => {
this.logger.error(`setHomeBlockchain failed for ${symbol}@${chainId}`, err); /*
optionally: throw err; */ }) pattern so errors are recorded with context before
proceeding to innerClient.deposit and checkpointWithApproval.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@clearnode/api/channel_v1/get_channels.go`:
- Around line 21-34: Clamp the requested pagination limit to an enforced maximum
to prevent DoS via huge LIMIT values: introduce a maxLimit constant (e.g.,
maxLimit uint32 = 1000) in get_channels.go and when reading req.Pagination.Limit
(referencing variables limit, defaultLimit, and req.Pagination.Limit) replace
the current assignment with logic that sets limit to defaultLimit if zero or
nil, then if limit > maxLimit set limit = maxLimit (and optionally ensure limit
>= 1). Update any downstream code that uses limit accordingly so GORM never
receives an unbounded LIMIT.

In `@sdk/compat/package.json`:
- Around line 13-18: The "clean" npm script currently calls "rimraf dist" but
rimraf is not declared in devDependencies; fix by adding rimraf (e.g., "rimraf":
"^5.0.0") to devDependencies in package.json or replace the "clean" script value
with a Node-native command like "node -e \"fs.rmSync('dist', { recursive: true,
force: true })\"" (or an equivalent cross-platform JS snippet) so the "clean"
script works without an undeclared dependency; update the "clean" script or
devDependencies accordingly and ensure package.json's "scripts" block still
contains "clean": "..." after the change.

In `@sdk/compat/README.md`:
- Line 7: The README contains bare fenced code blocks triggering markdownlint
MD040; update the two raw triple-backtick blocks that contain the ASCII diagram
and the env-var example to use a language specifier "text" (i.e., change the
fences to ```text) so the diagram and the NEXT_PUBLIC_BLOCKCHAIN_RPCS
environment line are marked as plain text.
- Line 53: The README heading currently reads "Initialise the client" (British
spelling) but code identifiers and badges use American spelling (e.g.,
NotInitializedError); change the heading text to "Initialize the client" and
scan the README for any other "initialise" occurrences to replace with
"initialize" so spelling is consistent with NotInitializedError and the rest of
the codebase.

In `@sdk/compat/src/client.ts`:
- Around line 651-655: The transfer method currently performs sequential
innerClient.transfer calls and will stop on the first failure, causing partial
execution; update the async transfer(destination: Address, allocations:
TransferAllocation[]) in client.ts to (1) track which allocations completed
(e.g., collect completed indexes or allocations) while calling
innerClient.transfer, (2) wrap each await in a try/catch so on error you throw a
new error that includes the original error plus structured metadata about which
allocations succeeded and which failed, and (3) add/update the method’s
docstring to explicitly document that transfers are not atomic and callers may
receive partial success information in the thrown error; reference the transfer
method, innerClient.transfer calls and the TransferAllocation shape so reviewers
can find the changes.
- Around line 197-201: getTokenDecimals currently returns a silent default of 6
when a token key is missing, which can produce incorrect amounts; change
getTokenDecimals (and its usage points like formatAmount and parseAmount) to
surface the error instead of silently defaulting: look up the token in
assetsByToken inside getTokenDecimals, and if not found either throw an Error
(consistent with resolveToken) or at minimum emit a warning via the logger and
then fallback; prefer throwing to force callers to handle unknown tokens—update
any callers/tests that assumed the 6-decimal fallback accordingly and reference
getTokenDecimals, assetsByToken, formatAmount, parseAmount, and resolveToken
when making the change.
- Around line 252-260: The allowance check in classifyError is too broad and
matches positive messages; update the condition in classifyError (the branch
that currently checks lower.includes('allowance') &&
lower.includes('sufficient')) to detect negative allowance phrases instead —
e.g. match patterns like "insufficient allowance", "allowance insufficient", or
"allowance not sufficient" (or generally the presence of 'allowance' together
with 'insufficient'/'not sufficient') and only then return new
AllowanceError(msg); leave other branches (UserRejectedError,
InsufficientFundsError, NotInitializedError) unchanged.
- Around line 521-535: In getAssetsList(), the symbol field is incorrectly
populated from asset.name while refreshAssets() expects asset.symbol; update
getAssetsList() to use asset.symbol for the symbol property (and optionally fall
back to asset.name if asset.symbol is undefined) so ClearNodeAsset.symbol
consistently contains the ticker across both getAssetsList and refreshAssets.
- Around line 60-82: WalletTxSigner currently hardcodes chainId: 1 in
signMessage which breaks EIP-712 signatures on non-mainnet chains; modify the
WalletTxSigner class to accept a chainId (number) in its constructor, store it
as a private property, and use that property in the domain object inside
signMessage instead of the literal 1; finally, update whatever
factory/constructor in this module that instantiates WalletTxSigner to pass the
appropriate chainId (from config or WalletClient context) and adjust types for
the new constructor parameter.

In `@sdk/compat/src/events.ts`:
- Around line 48-68: The poll() method uses Promise.allSettled so the outer
try/catch never catches per-request failures and rejected results are ignored;
update poll() to inspect each settled result (channels, balances, assets) for
status === 'rejected' and forward the rejection reason(s) to
this.callbacks.onError (e.g., call this.callbacks.onError with the Error or
converted Error for each rejected result, or aggregate multiple reasons into one
Error) while still invoking onChannelUpdate/onBalanceUpdate/onAssetsUpdate for
fulfilled results; reference the settled values from Promise.allSettled of
this.client.getChannels(), this.client.getBalances(), and
this.client.getAssetsList() and ensure you convert non-Error reasons into Error
objects before calling this.callbacks.onError.

---

Nitpick comments:
In `@clearnode/api/channel_v1/get_channels_test.go`:
- Around line 16-34: The helper newGetChannelsHandler ignores the error returned
by core.NewChannelDefaultSigner causing silent nil nodeSigner; change
newGetChannelsHandler to accept t *testing.T, call nodeSigner, err :=
core.NewChannelDefaultSigner(mockSigner) and assert require.NoError(t, err) (or
t.Fatalf) before using nodeSigner, and update all test callers to pass the
testing.T from their tests so the helper fails loudly if signer creation errors;
refer to newGetChannelsHandler and core.NewChannelDefaultSigner when making the
change.

In `@clearnode/api/channel_v1/get_channels.go`:
- Around line 39-51: The GetChannels handler is performing a read-only operation
inside useStoreInTx (which invokes ExecuteInTransaction / a write transaction);
change it to avoid write transactions by either calling the store read method
directly (replace the useStoreInTx block and call
h.dbStore.GetUserChannels(req.Wallet, req.Status, req.Asset, limit, offset) and
handle the returned channels/totalCount/error) or add a dedicated read-only
helper (e.g., useStoreReadOnly that wraps an ExecuteReadOnlyTransaction /
read-only accessor) and call Store.GetUserChannels inside that helper; update
error handling to behave exactly like before but without acquiring a write
transaction.

In `@clearnode/api/channel_v1/interface.go`:
- Around line 61-62: The GetUserChannels method signature in the channel API
interface uses raw limit/offset which is inconsistent with the
Store/DatabaseStore pagination convention; update the GetUserChannels
declaration to accept a *core.PaginationParams instead of limit and offset and
change its return to include core.PaginationMetadata (i.e.,
GetUserChannels(wallet string, status *string, asset *string, params
*core.PaginationParams) ([]core.Channel, core.PaginationMetadata, error)) so it
matches the Store interface and DatabaseStore implementations (update callers
and implementations of GetUserChannels accordingly).

In `@clearnode/store/database/app_session.go`:
- Around line 101-105: The subquery currently builds from AppSessionV1 and joins
to app_session_participants_v1 unnecessarily; instead construct subQuery from
the AppParticipantV1 model selecting app_session_id with the wallet_address
filter (use strings.ToLower(*participant)) and keep the outer filter query =
query.Where("id IN (?)", subQuery); update references to subQuery, AppSessionV1,
AppParticipantV1 and participant accordingly to remove the JOIN and select the
app_session_id directly.
- Around line 101-105: The subquery currently joins AppSessionV1 to
app_session_participants_v1 just to return session IDs; replace that JOIN by
building the subquery from AppParticipantV1 directly (e.g.
s.db.Model(&AppParticipantV1{}).Select("app_session_id").Where("wallet_address =
?", strings.ToLower(*participant))) and then use query = query.Where("id IN
(?)", subQuery) so you avoid the unnecessary JOIN while keeping the outer ORDER
BY and leveraging GORM v2's inline subquery rendering.

In `@clearnode/store/database/channel_test.go`:
- Around line 765-784: Add a sub-test to the existing "Success - Pagination
limits results" test that calls store.GetUserChannels with limit=2 and offset=2
to verify pagination shifting; after creating the five channels via NewDBStore
and CreateChannel, call GetUserChannels("0xuser_pg", nil, nil, 2, 2), assert no
error, assert the returned slice length is 2, assert total equals uint32(5), and
optionally assert the returned ChannelIDs match the expected "0xch_pg_2" and
"0xch_pg_3" to ensure the window is correctly shifted.

In `@clearnode/store/database/interface.go`:
- Around line 53-54: The GetUserChannels method uses flat limit/offset and a
bare uint32 total which diverges from GetUserTransactions; update the
GetUserChannels signature to accept *core.PaginationParams (like
GetUserTransactions) and return the richer pagination result (e.g.,
*core.PaginationMetadata or the same type returned by GetUserTransactions) along
with []core.Channel and error, then update all implementations and call sites to
construct/consume core.PaginationParams and the metadata instead of separate
limit/offset and total count.

In `@sdk/compat/examples/lifecycle.ts`:
- Around line 52-56: The example hardcodes the sepolia chain when creating
walletClient via createWalletClient({ chain: sepolia, ... }), which can conflict
if callers set a different CHAIN_ID; update the example to either derive the
chain from the CHAIN_ID env var (use a simple mapping or helper to convert
CHAIN_ID to the correct chain constant) or add a one-line comment above the
createWalletClient call explicitly stating that sepolia is hardcoded for the
example and should be replaced when using a different CHAIN_ID so users who
copy-paste don’t accidentally target the wrong network.

In `@sdk/compat/package.json`:
- Around line 43-48: The devDependency "@types/node" is set to "^25.3.0" which
is incompatible with the declared engines.node >=20.0.0; change the entry in
package.json devDependencies ("@types/node") to a version that matches the
minimum supported Node major (e.g., "^20.0.0" or a 20.x.x pin) so the type
definitions align with Node 20, then run your package manager to update the
lockfile; edit the package.json devDependencies block where "@types/node"
appears to make this change.

In `@sdk/compat/src/client.ts`:
- Around line 569-591: Replace the magic literal intent: 3 in closeAppSession
with the named intent from the existing map; reference INTENT_MAP.close (or
introduce a local constant like INTENT_CLOSE = INTENT_MAP.close) and use that
instead in the appUpdate object so intent values are readable and consistent
with the rest of the file (change usage in closeAppSession to use
INTENT_MAP.close or the new INTENT_CLOSE identifier).
- Around line 262-271: In checkpointWithApproval, the error handling redundantly
checks both err instanceof AllowanceError and NitroliteClient.classifyError(err)
instanceof AllowanceError; replace this with a single classification: call
NitroliteClient.classifyError(err) once, store the result, and if that
classified error is not an AllowanceError re-throw it; otherwise proceed to log
the allowance message, call this.innerClient.approveToken(chainId, tokenAddress,
NitroliteClient.MAX_UINT256) and retry this.innerClient.checkpoint(symbol).
Ensure references to AllowanceError, NitroliteClient.classifyError,
checkpointWithApproval, innerClient.checkpoint, innerClient.approveToken, and
NitroliteClient.MAX_UINT256 are used to locate the change.
- Around line 277-283: In deposit (and the other occurrence) do not silently
swallow errors from this.innerClient.setHomeBlockchain; instead catch the error,
log a descriptive message including symbol and chainId and the error object
(e.g., via this.logger.error or equivalent) so failures are visible, and decide
whether to rethrow or continue based on expected behavior — update the
setHomeBlockchain call in deposit and the similar call at the other site to use
a .catch(err => { this.logger.error(`setHomeBlockchain failed for
${symbol}@${chainId}`, err); /* optionally: throw err; */ }) pattern so errors
are recorded with context before proceeding to innerClient.deposit and
checkpointWithApproval.

In `@sdk/compat/src/rpc.ts`:
- Around line 61-64: The parseGetChannelsResponse stub currently calls
JSON.parse(raw) directly and will throw on invalid JSON; update
parseGetChannelsResponse to mirror parseAnyRPCResponse by wrapping JSON.parse in
a try/catch and returning the safe default shape ({ params: { channels: [] } })
when parsing fails or the expected path is missing; locate the function
parseGetChannelsResponse and adjust the parsing block to catch any JSON errors
(optionally log or ignore) and return the no-op default instead of allowing an
exception to propagate.

In `@sdk/compat/src/types.ts`:
- Around line 95-104: RPCLedgerEntry and LedgerEntry currently have identical
shapes; locate the RPCLedgerEntry and LedgerEntry interfaces and either replace
one with a type alias (e.g., "export type RPCLedgerEntry = LedgerEntry" or vice
versa) to prevent accidental divergence, or add a short comment above both
definitions explaining they intentionally mirror each other (wire-format vs
client-response) so future maintainers don't change one without the other;
update both occurrences (the one at RPCLedgerEntry and the duplicate at lines
referenced around LedgerEntry) accordingly.

In `@sdk/compat/tsconfig.prod.json`:
- Line 4: Remove the redundant "declaration": true setting from
tsconfig.prod.json: locate the JSON key "declaration" in tsconfig.prod.json (the
explicit declaration property) and delete that entry since the base config
already provides it, leaving the prod config to inherit the declaration setting.

In `@sdk/ts/src/client.ts`:
- Around line 1075-1095: getChannels currently accepts core.PaginationParams
(offset/limit) while getTransactions and getAppSessions accept {page, pageSize};
update getChannels signature and request construction to match the higher-level
pagination shape for consistency: change getChannels(wallet, options?: {
status?: string; asset?: string; pagination?: { page?: number; pageSize?: number
} }) and inside getChannels convert page/pageSize to offset = (page-1)*pageSize
and limit = pageSize when building the API.ChannelsV1GetChannelsRequest; keep
transformChannel and transformPaginationMetadata usage unchanged and ensure any
callers/tests are updated to the new pagination shape (or add a thin adapter if
you must preserve backward compatibility with core.PaginationParams).

Comment on lines 651 to 655
async transfer(destination: Address, allocations: TransferAllocation[]): Promise<void> {
for (const alloc of allocations) {
await this.innerClient.transfer(destination, alloc.asset, new Decimal(alloc.amount));
}
}
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

Sequential transfers fail partway with no rollback.

If one of the allocations fails mid-loop, prior transfers have already been submitted and subsequent ones are skipped. This partial-execution behavior should at least be documented so callers know they may need to handle partial failures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/compat/src/client.ts` around lines 651 - 655, The transfer method
currently performs sequential innerClient.transfer calls and will stop on the
first failure, causing partial execution; update the async transfer(destination:
Address, allocations: TransferAllocation[]) in client.ts to (1) track which
allocations completed (e.g., collect completed indexes or allocations) while
calling innerClient.transfer, (2) wrap each await in a try/catch so on error you
throw a new error that includes the original error plus structured metadata
about which allocations succeeded and which failed, and (3) add/update the
method’s docstring to explicitly document that transfers are not atomic and
callers may receive partial success information in the thrown error; reference
the transfer method, innerClient.transfer calls and the TransferAllocation shape
so reviewers can find the changes.

@codecov
Copy link

codecov bot commented Feb 22, 2026

Codecov Report

❌ Patch coverage is 71.55172% with 33 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
clearnode/api/channel_v1/get_channels.go 67.81% 24 Missing and 4 partials ⚠️
clearnode/store/database/channel.go 77.77% 2 Missing and 2 partials ⚠️
clearnode/api/rpc_router.go 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

- Fix onError never firing in EventPoller (Promise.allSettled never
  rejects, so try/catch was dead code; now forwards rejected reasons)
- Fix getAssetsList using asset.name instead of asset.symbol
- Add missing await on async resolveAssetDisplay in lifecycle example
- Replace rimraf with Node-native fs.rmSync in clean script
- Add language specifiers to bare markdown code blocks
- Normalize "Initialise" to "Initialize" for consistency
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

🧹 Nitpick comments (1)
sdk/compat/src/client.ts (1)

612-620: Use NitroliteClient.INTENT_MAP['close'] instead of magic number 3.

INTENT_MAP is defined at line 638 precisely for this purpose and is already used in submitAppState. Using the named constant here avoids the unexplained literal.

♻️ Proposed fix
         const appUpdate = {
             appSessionId,
-            intent: 3, // Close
+            intent: NitroliteClient.INTENT_MAP['close'],
             version: session.version + 1n,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/compat/src/client.ts` around lines 612 - 620, The appUpdate object uses a
magic number for intent (intent: 3)—replace that literal with the named constant
from the class map: use NitroliteClient.INTENT_MAP['close'] for intent so it
matches the existing pattern used in submitAppState; locate the appUpdate
construction and set intent to NitroliteClient.INTENT_MAP['close'] instead of 3
to improve clarity and consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@clearnode/api/channel_v1/get_channels.go`:
- Around line 62-67: The page/pageCount mismatch occurs when totalCount == 0:
pageCount becomes 0 but page defaults to 1; fix by clamping the computed page
against pageCount after calculating both. After computing pageCount (uint32) and
deriving page from offset/limit, if pageCount == 0 set page = 0, otherwise set
page = min(page, pageCount) to ensure page never exceeds pageCount; update the
logic around the variables pageCount, page, offset and limit in get_channels.go
accordingly.

In `@sdk/compat/examples/lifecycle.ts`:
- Around line 121-124: The example uses info.available which doesn't exist on
AccountInfo; update the success message after client.getAccountInfo() to
reference valid fields on AccountInfo (e.g., info.balances.length to show number
of balances or info.balances[0] to display the first balance entry) and keep
channelCount (info.channelCount) as-is so ok('getAccountInfo()', ...) prints
meaningful data; locate the call to client.getAccountInfo() and the
ok('getAccountInfo()', `available=...`) invocation and replace the undefined
field with either balances.length or a formatted first balance value.
- Around line 166-173: The three calls to the async client methods need to be
awaited: change the calls to client.resolveToken(token.token),
client.resolveAsset(resolved.symbol), and client.getTokenDecimals(token.token)
to use await so you get actual results instead of Promises; specifically update
the variables resolved, bySymbol, and decimals to await
client.resolveToken(...), await client.resolveAsset(...), and await
client.getTokenDecimals(...) respectively (keeping the existing
ok('resolveToken()', ...), ok('resolveAsset()', ...) and
ok('getTokenDecimals()', ...) usage).
- Line 26: The CHAIN_ID assignment uses Number(process.env.CHAIN_ID ||
'11155111') which lets Number('abc') produce NaN and silently bypass the
default; change it to parse and validate the env var first and only use it if
numeric (e.g. parse with Number() or parseInt and check Number.isFinite /
!Number.isNaN), otherwise fall back to the Sepolia literal 11155111; update the
variable referenced as CHAIN_ID and ensure the validated numeric value is what
gets passed into NitroliteClient.create (or any other use sites) so NaN cannot
propagate.

In `@sdk/compat/src/client.ts`:
- Around line 227-239: resolveAssetDisplay currently looks up assetsByToken
(keyed by lowercase token address) but refreshAssets overwrites same-address
entries across chains, so the alternate-chain find is unreachable; change
assetsByToken from Map<string, AssetInfo> to Map<string, Map<number, AssetInfo>>
(or Map<string, AssetInfo[]>) and update refreshAssets to append/store AssetInfo
under the token address keyed by chainId instead of overwriting, then change
resolveAssetDisplay to first get the per-address map/array and select the entry
whose chainId matches the provided chainId (or pick a sensible default when
chainId is undefined); also update related helpers like resolveToken to use the
new nested structure when choosing the best-matching AssetInfo.
- Around line 181-183: ensureAssets currently uses assetsBySymbol.size as the
"loaded" sentinel which stays zero for chains without registered symbols and
causes repeated refreshAssets network calls; update ensureAssets (or create a
boolean like this._assetsLoaded) to check assetsByToken.size (or set the boolean
inside refreshAssets) instead so a single successful refreshAssets() marks
assets as loaded; update refreshAssets() to set the flag (or populate
assetsByToken) reliably and have getDecimalsForAsset / closeChannel rely on the
new sentinel to avoid refetching on every call.

---

Duplicate comments:
In `@clearnode/api/channel_v1/get_channels.go`:
- Around line 21-38: The pagination clamp logic ensures limit is constrained to
[1,1000] before reaching the store: keep the current logic around defaultLimit,
maxLimit, and the req.Pagination handling (variables limit, offset), and add a
unit test that exercises req.Pagination.Limit == nil, == 0, and > maxLimit to
assert limit becomes defaultLimit (100) for zero/nil and maxLimit (1000) when
over the limit; no functional code change required other than adding the test.

In `@sdk/compat/src/client.ts`:
- Around line 266-274: In classifyError, the allowance branch currently checks
lower.includes('sufficient') which also matches phrases like "allowance
insufficient"; change that condition to check for 'insufficient' (e.g., use
lower.includes('allowance') && lower.includes('insufficient')) so only true
insufficient messages map to AllowanceError; update the static method
classifyError (and keep existing AllowanceError, UserRejectedError,
InsufficientFundsError, NotInitializedError references) accordingly.
- Around line 210-215: getTokenDecimals currently falls back to 6 for unknown
tokens causing large amount errors; change it to throw for unrecognized
addresses like resolveToken does. In the getTokenDecimals function (use
tokenAddress param, ensureAssets call, assetsByToken map lookup), if info is
undefined/null do not return 6 — instead throw a descriptive Error including the
tokenAddress (or its lowercase key) so callers must handle unknown tokens; keep
the existing await this.ensureAssets() behavior and use the same lookup key
generation (tokenAddress.toString().toLowerCase()) as currently implemented.
- Around line 691-697: The transfer method currently performs sequential
innerClient.transfer calls inside transfer(...) and will stop mid-loop on an
error leaving prior transfers committed; update transfer to either 1) use a
single batch API if innerClient exposes one (e.g., innerClient.batchTransfer) to
make the whole allocations set atomic, or 2) if no batch API exists, run all
transfers concurrently using Promise.all over allocations (mapping each alloc ->
innerClient.transfer(destination, alloc.asset, humanAmount)) so failures
propagate instead of being silently skipped, and wrap the Promise.all in
try/catch to log and rethrow the error; also add a brief comment in transfer
mentioning the chosen behavior so callers know whether partial commits are
possible. Ensure you reference getDecimalsForAsset, TransferAllocation,
innerClient.transfer and the transfer(...) method when making the change.

---

Nitpick comments:
In `@sdk/compat/src/client.ts`:
- Around line 612-620: The appUpdate object uses a magic number for intent
(intent: 3)—replace that literal with the named constant from the class map: use
NitroliteClient.INTENT_MAP['close'] for intent so it matches the existing
pattern used in submitAppState; locate the appUpdate construction and set intent
to NitroliteClient.INTENT_MAP['close'] instead of 3 to improve clarity and
consistency.

Comment on lines 227 to 239
async resolveAssetDisplay(tokenAddress: Address | string, chainId?: number): Promise<{ symbol: string; decimals: number } | null> {
await this.ensureAssets();
const key = tokenAddress.toString().toLowerCase();
const info = this.assetsByToken.get(key);
if (!info) return null;
if (chainId !== undefined && Number(info.chainId) !== chainId) {
const alt = Array.from(this.assetsByToken.values()).find(
(a) => a.tokenAddress === key && Number(a.chainId) === chainId,
);
return alt ? { symbol: alt.symbol, decimals: alt.decimals } : { symbol: info.symbol, decimals: info.decimals };
}
return { symbol: info.symbol, decimals: info.decimals };
}
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

resolveAssetDisplay multi-chain alternative lookup is a no-op.

assetsByToken is a Map<string, AssetInfo> keyed by lowercase token address (line 173). If the same token contract exists on multiple chains, refreshAssets() overwrites the entry with the last-iterated chain — only one AssetInfo per address survives. The find at line 233 therefore can never locate an entry with the same tokenAddress but a different chainId than the one already in info, making the entire alt branch unreachable.

To support per-chain resolution for the same token address, the map would need to be Map<string, Map<bigint, AssetInfo>> (or Map<string, AssetInfo[]>), with refreshAssets appending rather than overwriting.

♻️ Minimal structural fix
-    private assetsByToken = new Map<string, AssetInfo>();
+    private assetsByToken = new Map<string, AssetInfo[]>();

In refreshAssets():

-                this.assetsByToken.set(info.tokenAddress, info);
+                const existing = this.assetsByToken.get(info.tokenAddress) ?? [];
+                existing.push(info);
+                this.assetsByToken.set(info.tokenAddress, existing);

Then resolveToken and resolveAssetDisplay pick the best-matching entry by chainId.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/compat/src/client.ts` around lines 227 - 239, resolveAssetDisplay
currently looks up assetsByToken (keyed by lowercase token address) but
refreshAssets overwrites same-address entries across chains, so the
alternate-chain find is unreachable; change assetsByToken from Map<string,
AssetInfo> to Map<string, Map<number, AssetInfo>> (or Map<string, AssetInfo[]>)
and update refreshAssets to append/store AssetInfo under the token address keyed
by chainId instead of overwriting, then change resolveAssetDisplay to first get
the per-address map/array and select the entry whose chainId matches the
provided chainId (or pick a sensible default when chainId is undefined); also
update related helpers like resolveToken to use the new nested structure when
choosing the best-matching AssetInfo.

- Warn on unknown token fallback to 6 decimals instead of silent default
- Tighten classifyError to match 'insufficient' not just 'sufficient'
- Use assetsByToken.size in ensureAssets to avoid repeated refetch
  when configured chain has no assets
- Remove dead multi-chain alt lookup in resolveAssetDisplay
- Fix page=0/pageCount=0 for empty results in get_channels
- Add missing await on resolveToken/resolveAsset/getTokenDecimals
  in lifecycle example
- Fix info.available -> info.balances in lifecycle example
- Validate CHAIN_ID env var for NaN in lifecycle example
- Add JSDoc noting non-atomic transfer behavior
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

🧹 Nitpick comments (5)
sdk/compat/src/client.ts (5)

273-281: First condition in checkpointWithApproval is always false — dead code.

err instanceof AllowanceError can never be true because the inner v1 client has no knowledge of the compat AllowanceError type and will never throw it. The whole expression reduces to !(NitroliteClient.classifyError(err) instanceof AllowanceError).

♻️ Proposed fix — drop the unreachable condition
-            if (!(err instanceof AllowanceError) && !(NitroliteClient.classifyError(err) instanceof AllowanceError)) throw NitroliteClient.classifyError(err);
+            if (!(NitroliteClient.classifyError(err) instanceof AllowanceError)) throw NitroliteClient.classifyError(err);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/compat/src/client.ts` around lines 273 - 281, In checkpointWithApproval,
remove the unreachable check for err instanceof AllowanceError and rely solely
on NitroliteClient.classifyError(err) to detect allowance errors; update the
catch in the checkpointWithApproval method (which calls
this.innerClient.checkpoint and this.innerClient.approveToken) so it throws
NitroliteClient.classifyError(err) unless the classified error is an instance of
AllowanceError, and only then log the message, call approveToken(chainId,
tokenAddress, NitroliteClient.MAX_UINT256) and retry
this.innerClient.checkpoint(symbol).

185-189: getDecimalsForAsset silently falls back to 6 decimals without any warning.

getTokenDecimals (lines 214–216) logs a console.warn for unknown tokens; getDecimalsForAsset takes the same silent-fallback path but is used in arguably higher-stakes paths: closeAppSession, submitAppState, and transfer. An unknown symbol here would silently corrupt amounts with no diagnostic signal.

♻️ Proposed fix — add warning parity with `getTokenDecimals`
     private async getDecimalsForAsset(assetSymbol: string): Promise<number> {
         await this.ensureAssets();
         const info = this.assetsBySymbol.get(assetSymbol.toLowerCase());
+        if (!info) {
+            console.warn(`[compat] Unknown asset symbol ${assetSymbol}, falling back to 6 decimals`);
+        }
         return info?.decimals ?? 6;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/compat/src/client.ts` around lines 185 - 189, getDecimalsForAsset
currently silently returns a default of 6 for unknown symbols; update it to
mirror getTokenDecimals by emitting a console.warn when assetsBySymbol.lookup of
assetSymbol.toLowerCase() yields no info so callers (closeAppSession,
submitAppState, transfer) get a diagnostic signal; keep the existing
ensureAssets() call, include the assetSymbol and context in the warning, and
then return the existing fallback of 6.

511-514: status ?? undefined is a no-op.

status is already typed as string | undefined, so status ?? undefined is identical to status alone.

-            status: status ?? undefined,
+            status,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/compat/src/client.ts` around lines 511 - 514, The call to
this.innerClient.getAppSessions uses "status ?? undefined", which is redundant
because status is already string | undefined; change the argument to pass status
directly (i.e., replace status ?? undefined with status) and leave wallet as
wallet ?? this.userAddress so the call becomes this.innerClient.getAppSessions({
wallet: wallet ?? this.userAddress, status });—update the invocation around
getAppSessions to remove the unnecessary nullish coalescing on status.

355-371: getChannelData bypasses the populated asset cache and issues a fresh network call on every invocation.

Line 356 calls this.innerClient.getAssets() directly, ignoring the assetsByToken/assetsBySymbol maps that ensureAssets() already maintains. For an operation that iterates all assets to find a single channel, this is an unnecessary network round-trip.

♻️ Proposed fix — use `ensureAssets()` + cached maps
     async getChannelData(_channelId: string): Promise<any> {
-        const assets = await this.innerClient.getAssets();
-        for (const asset of assets) {
+        await this.ensureAssets();
+        for (const [, info] of this.assetsBySymbol) {
+            const asset = { symbol: info.symbol };
             try {
                 const ch = await this.innerClient.getHomeChannel(this.userAddress, asset.symbol);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/compat/src/client.ts` around lines 355 - 371, getChannelData currently
calls this.innerClient.getAssets() causing an unnecessary network call; change
it to await this.ensureAssets() and iterate the cached maps (this.assetsBySymbol
or this.assetsByToken) to find the asset matching the channelId, then call
this.innerClient.getHomeChannel and getLatestState only for the matched asset;
specifically, inside getChannelData replace the direct getAssets() usage with
await this.ensureAssets(), iterate this.assetsBySymbol/this.assetsByToken to
locate the asset whose channelId matches _channelId (handling try/catch as
before), and return the same object structure using the cached asset symbol when
calling getHomeChannel/getLatestState.

320-336: Dead else branch in the bulk-close loop — info is always truthy here.

The if (!symbol) continue guard on line 325 ensures the loop skips any iteration where info is falsy. By line 328 info is guaranteed to be defined, making the else at line 330 unreachable.

♻️ Proposed fix — remove dead branch
                 await this.innerClient.closeHomeChannel(symbol);
-                if (info) {
-                    await this.checkpointWithApproval(symbol, info.chainId, info.tokenAddress);
-                } else {
-                    await this.innerClient.checkpoint(symbol);
-                }
+                await this.checkpointWithApproval(symbol, info.chainId, info.tokenAddress);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/compat/src/client.ts` around lines 320 - 336, The loop's else branch is
dead because the earlier `if (!symbol) continue` guarantees `info` is defined;
remove the unreachable `else` and call the approved checkpoint path
unconditionally for the open-channel case. In the for-loop that calls
`this.ensureAssets()` and reads `const info =
this.assetsByToken.get(ch.token.toLowerCase())`, replace the `if (info) { await
this.checkpointWithApproval(symbol, info.chainId, info.tokenAddress); } else {
await this.innerClient.checkpoint(symbol); }` with a single call to `await
this.checkpointWithApproval(symbol, info.chainId, info.tokenAddress);` (keeping
the surrounding `try/catch` and the `if (!symbol) continue` guard).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@clearnode/api/channel_v1/get_channels.go`:
- Around line 16-19: The handler currently trusts req.Wallet directly (see
req.Wallet and c.Fail) which allows IDOR; ensure the authenticated principal
from the RPC context is asserted before using req.Wallet by retrieving the
caller identity from rpc.Context (or your auth middleware) and comparing it to
req.Wallet, returning c.Fail on mismatch; if no context binding exists,
add/authenticate middleware that binds the authenticated wallet ID into the
request context (or into the handler input) and update this handler to validate
req.Wallet against that bound identity (apply the same pattern to all handlers
that accept a wallet).

---

Duplicate comments:
In `@clearnode/api/channel_v1/get_channels.go`:
- Around line 62-69: The page calculation can exceed pageCount when offset
overshoots results; after computing pageCount and page in the get_channels
handler (variables pageCount, page, totalCount, limit, offset), add a clamp so
that if page > pageCount then page = pageCount (preserving the existing
totalCount > 0 guard and zero-case behavior). This ensures page is bounded to
the last available page when offset is beyond the result set.

---

Nitpick comments:
In `@sdk/compat/src/client.ts`:
- Around line 273-281: In checkpointWithApproval, remove the unreachable check
for err instanceof AllowanceError and rely solely on
NitroliteClient.classifyError(err) to detect allowance errors; update the catch
in the checkpointWithApproval method (which calls this.innerClient.checkpoint
and this.innerClient.approveToken) so it throws
NitroliteClient.classifyError(err) unless the classified error is an instance of
AllowanceError, and only then log the message, call approveToken(chainId,
tokenAddress, NitroliteClient.MAX_UINT256) and retry
this.innerClient.checkpoint(symbol).
- Around line 185-189: getDecimalsForAsset currently silently returns a default
of 6 for unknown symbols; update it to mirror getTokenDecimals by emitting a
console.warn when assetsBySymbol.lookup of assetSymbol.toLowerCase() yields no
info so callers (closeAppSession, submitAppState, transfer) get a diagnostic
signal; keep the existing ensureAssets() call, include the assetSymbol and
context in the warning, and then return the existing fallback of 6.
- Around line 511-514: The call to this.innerClient.getAppSessions uses "status
?? undefined", which is redundant because status is already string | undefined;
change the argument to pass status directly (i.e., replace status ?? undefined
with status) and leave wallet as wallet ?? this.userAddress so the call becomes
this.innerClient.getAppSessions({ wallet: wallet ?? this.userAddress, status
});—update the invocation around getAppSessions to remove the unnecessary
nullish coalescing on status.
- Around line 355-371: getChannelData currently calls
this.innerClient.getAssets() causing an unnecessary network call; change it to
await this.ensureAssets() and iterate the cached maps (this.assetsBySymbol or
this.assetsByToken) to find the asset matching the channelId, then call
this.innerClient.getHomeChannel and getLatestState only for the matched asset;
specifically, inside getChannelData replace the direct getAssets() usage with
await this.ensureAssets(), iterate this.assetsBySymbol/this.assetsByToken to
locate the asset whose channelId matches _channelId (handling try/catch as
before), and return the same object structure using the cached asset symbol when
calling getHomeChannel/getLatestState.
- Around line 320-336: The loop's else branch is dead because the earlier `if
(!symbol) continue` guarantees `info` is defined; remove the unreachable `else`
and call the approved checkpoint path unconditionally for the open-channel case.
In the for-loop that calls `this.ensureAssets()` and reads `const info =
this.assetsByToken.get(ch.token.toLowerCase())`, replace the `if (info) { await
this.checkpointWithApproval(symbol, info.chainId, info.tokenAddress); } else {
await this.innerClient.checkpoint(symbol); }` with a single call to `await
this.checkpointWithApproval(symbol, info.chainId, info.tokenAddress);` (keeping
the surrounding `try/catch` and the `if (!symbol) continue` guard).

Comment on lines +16 to +19
if req.Wallet == "" {
c.Fail(rpc.Errorf("wallet is required"), "missing wallet")
return
}
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check wallet validation patterns in channel_v1 handlers
echo "=== Files in channel_v1 ==="
fd -e go . clearnode/api/channel_v1 2>/dev/null || find clearnode/api/channel_v1 -name "*.go" 2>/dev/null | head -20

echo -e "\n=== Wallet usage in get_channels.go and nearby handlers ==="
fd -e go . clearnode/api/channel_v1 --exec grep -l "req\.Wallet\|Wallet" {} \; 2>/dev/null | head -10

echo -e "\n=== rpc.Context structure (check for auth/identity fields) ==="
find . -path ./node_modules -prune -o -type f -name "*.go" -print | xargs grep -l "type.*Context" | xargs grep -B5 -A10 "type.*Context" | grep -E "(type|Wallet|Identity|Caller|Principal|Auth)" | head -40

echo -e "\n=== Check if get_channels.go compares wallet with context identity ==="
cat clearnode/api/channel_v1/get_channels.go 2>/dev/null | head -100

Repository: erc7824/nitrolite

Length of output: 5541


🏁 Script executed:

#!/bin/bash
# More targeted search for authentication patterns
echo "=== All .go files in channel_v1 ==="
ls -la clearnode/api/channel_v1/*.go 2>/dev/null || echo "Directory listing failed, trying find:"
find clearnode/api/channel_v1 -name "*.go" 2>/dev/null

echo -e "\n=== Check request type definition ==="
rg "type.*Request" clearnode/api/channel_v1/ --type=go -A5 2>/dev/null | head -50

echo -e "\n=== Look for wallet field usage across multiple handlers ==="
rg "req\.Wallet" clearnode/api/ --type=go -B3 -A3 2>/dev/null | head -80

Repository: erc7824/nitrolite

Length of output: 5728


🏁 Script executed:

#!/bin/bash
# Check for rpc package structure and context
echo "=== rpc package files ==="
find . -path "*/pkg/rpc*" -o -path "*/rpc/*.go" 2>/dev/null | head -20

echo -e "\n=== Search rpc.Context definition ==="
rg "type Context" . --type=go -A10 2>/dev/null | head -50

echo -e "\n=== Check how wallet param is used with store ==="
rg "GetUserChannels\|StoreGetUserChannels" clearnode/ --type=go -B2 -A5 2>/dev/null | head -60

Repository: erc7824/nitrolite

Length of output: 1257


🏁 Script executed:

#!/bin/bash
echo "=== Full rpc.Context definition ==="
cat pkg/rpc/context.go | head -100

echo -e "\n=== Check for auth/wallet methods on Context ==="
rg "func.*Context.*Wallet\|func.*Context.*Auth\|func.*Context.*Identity\|func.*Context.*Caller" pkg/rpc/ --type=go -A3

echo -e "\n=== Check Connection type for auth info ==="
rg "type Connection" pkg/rpc/ --type=go -A20 | head -60

echo -e "\n=== Look for middleware or handlers that validate wallet ==="
rg "Storage\|wallet\|Wallet" pkg/rpc/ --type=go | grep -i "auth\|valid\|check\|identity\|principal" | head -20

Repository: erc7824/nitrolite

Length of output: 7011


🏁 Script executed:

#!/bin/bash
echo "=== Check handler setup and middleware ==="
cat clearnode/api/channel_v1/handler.go | head -80

echo -e "\n=== Look for any existing wallet validation in handler setup ==="
rg "Wallet" clearnode/api/channel_v1/handler.go --type=go -B3 -A3

echo -e "\n=== Check if there's auth wrapper or middleware in other files ==="
rg "GetWallet\|AuthWallet\|ValidateWallet\|IsAuthorized" clearnode/ --type=go | head -20

Repository: erc7824/nitrolite

Length of output: 4761


🏁 Script executed:

#!/bin/bash
echo "=== Check for wallet/auth extraction middleware ==="
rg "authMapping\|UserID\|authenticated\|middleware" pkg/rpc/ --type=go -B2 -A5 | head -80

echo -e "\n=== Check how handlers are registered/called ==="
rg "api.go\|methods.go" pkg/rpc/ --type=go | head -5
cat pkg/rpc/api.go | head -100

echo -e "\n=== Check if node setup passes auth to handlers ==="
rg "newHandler\|NewHandler" clearnode/api/ --type=go -B3 -A5 | grep -A5 "Handler"  | head -40

echo -e "\n=== Check user_v1 handlers for wallet validation ==="
cat clearnode/api/user_v1/get_balances.go | head -50

Repository: erc7824/nitrolite

Length of output: 4868


🏁 Script executed:

#!/bin/bash
echo "=== Check how requests are authenticated and dispatched ==="
rg "Handle\|RegisterMethod\|dispatch" pkg/rpc/ --type=go -B2 -A8 | head -100

echo -e "\n=== Check if there's any auth context setup before handler invocation ==="
cat pkg/rpc/node.go | head -120

echo -e "\n=== Check test files for how wallet validation should work ==="
head -100 clearnode/api/channel_v1/get_channels_test.go

echo -e "\n=== Check if SafeStorage has auth-related methods ==="
rg "SafeStorage\|type SafeStorage" pkg/rpc/ --type=go -A20 | head -60

Repository: erc7824/nitrolite

Length of output: 7637


🏁 Script executed:

#!/bin/bash
echo "=== Search for userID/wallet extraction from connections ==="
rg "connHub\|connectionID\|userID" clearnode/api/ --type=go -B2 -A3 | head -80

echo -e "\n=== Check how node.Notify uses userID and if handlers have access to it ==="
cat pkg/rpc/node.go | grep -A 30 "Notify\|userID" | head -60

echo -e "\n=== Look for connection authentication or identity storage ==="
rg "authMapping\|GetUser\|Identity\|Principal" pkg/rpc/connection_hub.go --type=go -B2 -A5 | head -100

echo -e "\n=== Check if there's any middleware that sets auth context ==="
rg "middleware\|Use\|auth" clearnode/api/ --type=go -i | grep -E "handler|middleware|auth" | head -30

echo -e "\n=== Directly search for wallet validation patterns across all handlers ==="
rg "req\.Wallet.*==\|ValidateWallet\|c\.Request\..*Wallet\|authenticated" clearnode/api/ --type=go -B3 -A3 | head -80

Repository: erc7824/nitrolite

Length of output: 2790


🏁 Script executed:

#!/bin/bash
echo "=== Check connection_hub authMapping implementation ==="
cat pkg/rpc/connection_hub.go | head -150

echo -e "\n=== Check if there's any auth setup in clearnode main/init ==="
find clearnode -name "main.go" -o -name "init.go" -o -name "*setup*" | xargs cat 2>/dev/null | head -150

echo -e "\n=== Final check: does rpc.Context expose connection/userID? ==="
cat pkg/rpc/context.go

echo -e "\n=== Verify: how is GetChannels actually called (trace the flow) ==="
rg "GetChannels" clearnode/ pkg/rpc/ --type=go -B2 -A2 | head -60

Repository: erc7824/nitrolite

Length of output: 19480


🏁 Script executed:

#!/bin/bash
echo "=== Check rpc_router setup for middleware/auth ==="
cat clearnode/api/rpc_router.go

echo -e "\n=== Check if there's any authentication decorator or wrapper on handlers ==="
rg "Handle.*Wallet\|middleware.*auth\|func.*auth\|AuthMiddleware" clearnode/api/ --type=go -B3 -A5 | head -100

echo -e "\n=== Final check: trace NewRPCRouter to see if auth is wired ==="
rg "NewRPCRouter" clearnode/api/ --type=go -A50 | head -150

Repository: erc7824/nitrolite

Length of output: 9602


🏁 Script executed:

#!/bin/bash
echo "=== Final verification: check if there's any client-level wallet binding ==="
rg "authWallet\|authenticatedWallet\|connectionWallet\|sessionWallet" . --type=go | head -20

echo -e "\n=== Check if Connection interface carries any auth info ==="
grep -A 50 "type Connection interface" pkg/rpc/connection.go

echo -e "\n=== Confirm: no auth validation in any handler ==="
rg "req\.Wallet.*==" clearnode/api/ --type=go -B2 -A2 | head -40

Repository: erc7824/nitrolite

Length of output: 3117


Verify wallet authorization against authenticated session identity to prevent IDOR.

req.Wallet is accepted directly from the request payload without validation against the authenticated caller's identity. This allows any authenticated user to query channels, balances, or states for arbitrary wallets.

Check whether the RPC layer enforces that the wallet in the request matches the authenticated principal (via rpc.Context or middleware). If not, add authentication middleware to bind the authenticated wallet to the context and validate req.Wallet against it before handler execution. This pattern should be applied consistently to all handlers that accept a wallet parameter.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clearnode/api/channel_v1/get_channels.go` around lines 16 - 19, The handler
currently trusts req.Wallet directly (see req.Wallet and c.Fail) which allows
IDOR; ensure the authenticated principal from the RPC context is asserted before
using req.Wallet by retrieving the caller identity from rpc.Context (or your
auth middleware) and comparing it to req.Wallet, returning c.Fail on mismatch;
if no context binding exists, add/authenticate middleware that binds the
authenticated wallet ID into the request context (or into the handler input) and
update this handler to validate req.Wallet against that bound identity (apply
the same pattern to all handlers that accept a wallet).

}

const defaultLimit uint32 = 100
const maxLimit uint32 = 1000
Copy link
Contributor

Choose a reason for hiding this comment

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

This setting should be configurable and have an adequate value

Copy link
Collaborator Author

@ihsraham ihsraham Feb 23, 2026

Choose a reason for hiding this comment

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

The current constants (defaultLimit=100, maxLimit=1000) follow the same pattern as store/database/list_options.go (DefaultLimit=10, MaxLimit=100). Making these configurable via handler config is a valid improvement. happy to address in a follow-up if the we later want to standardize configurable pagination across all endpoints.

Copy link
Contributor

Choose a reason for hiding this comment

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

Would be good to have an optional channel_type parameter accepting either core.ChannelTypeHome or core.ChannelTypeEscrow value

Copy link
Collaborator Author

@ihsraham ihsraham Feb 23, 2026

Choose a reason for hiding this comment

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

Added in 05f5c3b: channel_type is now an optional filter accepting "home" or "escrow". Wired through the RPC request type, handler, store interface, mock, DB layer, and TS SDK.

Copy link
Contributor

Choose a reason for hiding this comment

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

Didn't the current implementation work?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Old query failed because it asked PostgreSQL to DISTINCT by id while sorting by created_at, and Postgres refuses to sort by a column that isn’t included in the distinct result set since it can be ambiguous after deduplication
While SQLite allows it and that's why the tests passed.

return channels, uint32(totalCount), nil
}

func channelStatusFromString(s string) (core.ChannelStatus, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be better to have this function on api level and expect status as core.ChannelStatus here above

Copy link
Collaborator Author

@ihsraham ihsraham Feb 23, 2026

Choose a reason for hiding this comment

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

Addressed in 05f5c3b: moved channelStatusFromString from the database layer to the API handler. The store now accepts typed *core.ChannelStatus and *core.ChannelType instead of raw strings.

…atus parsing to API layer

- Add optional channel_type filter ("home"/"escrow") to get_channels
  endpoint across RPC types, handler, store, mock, and TS SDK
- Move channelStatusFromString from database layer to API handler;
  store now accepts typed *core.ChannelStatus and *core.ChannelType
- Add channelTypeFromString parser at API level
- Add DB test for channel type filtering
- Remove invalid-status DB test (validation now in handler)
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

♻️ Duplicate comments (1)
clearnode/api/channel_v1/get_channels.go (1)

45-48: Outstanding: req.Wallet is still not validated against the authenticated caller's identity (IDOR).

Any authenticated user can query channels for an arbitrary wallet by supplying a different address in the payload. This was flagged in the previous review cycle and remains unaddressed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clearnode/api/channel_v1/get_channels.go` around lines 45 - 48, The handler
currently only checks req.Wallet for emptiness but does not verify it matches
the authenticated caller (causing IDOR); fetch the caller's authenticated
wallet/identity from the request context or auth claims (e.g., the auth
principal in the handler/context or session used by this endpoint) and compare
it to req.Wallet, and if they differ return a forbidden error (use c.Fail with a
proper rpc.Errorf("permission denied" or "forbidden")) instead of proceeding;
update the logic in get_channels.go around the req.Wallet check to perform this
equality/ownership check and log/return immediately on mismatch.
🧹 Nitpick comments (2)
clearnode/api/channel_v1/get_channels_test.go (2)

204-236: TestGetChannels_EmptyResult omits Page and PageCount metadata assertions.

When totalCount=0, the page computation yields Page=1 (floor(0/limit)+1) and PageCount=0 (ceil(0/limit)), an asymmetric state (Page=1 of 0 pages) that is worth pinning explicitly.

 assert.Equal(t, uint32(0), response.Metadata.TotalCount)
+assert.Equal(t, uint32(1), response.Metadata.Page)
+assert.Equal(t, uint32(0), response.Metadata.PageCount)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clearnode/api/channel_v1/get_channels_test.go` around lines 204 - 236, Update
the test TestGetChannels_EmptyResult to assert the pagination metadata: after
translating the response in the test, add assertions that response.Metadata.Page
equals uint32(1) and response.Metadata.PageCount equals uint32(0). This ensures
the handler's computed pagination for totalCount=0 (Page=1 and PageCount=0) is
explicitly verified; locate these additions in the TestGetChannels_EmptyResult
function immediately after err = ctx.Response.Payload.Translate(&response).

36-202: Handler-level tests for channel_type and asset filters are absent.

The GetUserChannels mock signature accepts both a *core.ChannelType and a *string (asset) filter, and the PR adds channelTypeFromString parsing at the handler level. None of the six tests exercise either filter, leaving the handler-side parsing logic uncovered.

Add at minimum one test for each filter, analogous to TestGetChannels_WithStatusFilter:

func TestGetChannels_WithChannelTypeFilter(t *testing.T) {
	mockTxStore := new(MockStore)
	handler := newGetChannelsHandler(mockTxStore)

	userWallet := "0x1234567890123456789012345678901234567890"
	chType := core.ChannelTypeHome

	mockTxStore.On("GetUserChannels", userWallet,
		(*core.ChannelStatus)(nil), (*string)(nil), &chType, uint32(100), uint32(0)).
		Return([]core.Channel{{ChannelID: "0xCh", Type: core.ChannelTypeHome,
			Status: core.ChannelStatusOpen}}, uint32(1), nil)

	typeStr := "home"
	reqPayload := rpc.ChannelsV1GetChannelsRequest{
		Wallet:      userWallet,
		ChannelType: &typeStr,
	}
	// ... build ctx, invoke handler, assert response ...
}

func TestGetChannels_InvalidChannelType(t *testing.T) {
	// ensures handler rejects unknown channel_type strings
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clearnode/api/channel_v1/get_channels_test.go` around lines 36 - 202, Tests
for GetChannels are missing coverage for the channel_type and asset filters; add
handler-level tests that exercise channelTypeFromString parsing and asset
forwarding to the store. Create a TestGetChannels_WithChannelTypeFilter that
sets ChannelType = "home", expects handler.GetChannels to call GetUserChannels
with &core.ChannelTypeHome (reference chType) and returns a matching channel,
then assert the response contains the filtered channel; also add a
TestGetChannels_InvalidChannelType that passes an invalid type string and
asserts the handler returns an error (no store call). Ensure the mock
expectations reference GetUserChannels with the appropriate
(*core.ChannelStatus)(nil), (*string)(nil) or &assetString, &chType, and
pagination params so the parsing logic is exercised.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@clearnode/api/channel_v1/get_channels_test.go`:
- Around line 238-259: Add a new unit test that verifies handler-level
validation rejects invalid status strings: create TestGetChannels_InvalidStatus
using newGetChannelsHandler(mockTxStore) and rpc.ChannelsV1GetChannelsRequest
with a valid Wallet and Status set to "garbage", wrap it in rpc.NewPayload and
rpc.Context, call handler.GetChannels(ctx), then assert ctx.Response.Error() is
non-nil and its message contains "status", and finally call
mockTxStore.AssertExpectations(t) to ensure the MockStore (MockStore /
GetChannels interaction) was not invoked.

---

Duplicate comments:
In `@clearnode/api/channel_v1/get_channels.go`:
- Around line 45-48: The handler currently only checks req.Wallet for emptiness
but does not verify it matches the authenticated caller (causing IDOR); fetch
the caller's authenticated wallet/identity from the request context or auth
claims (e.g., the auth principal in the handler/context or session used by this
endpoint) and compare it to req.Wallet, and if they differ return a forbidden
error (use c.Fail with a proper rpc.Errorf("permission denied" or "forbidden"))
instead of proceeding; update the logic in get_channels.go around the req.Wallet
check to perform this equality/ownership check and log/return immediately on
mismatch.

---

Nitpick comments:
In `@clearnode/api/channel_v1/get_channels_test.go`:
- Around line 204-236: Update the test TestGetChannels_EmptyResult to assert the
pagination metadata: after translating the response in the test, add assertions
that response.Metadata.Page equals uint32(1) and response.Metadata.PageCount
equals uint32(0). This ensures the handler's computed pagination for
totalCount=0 (Page=1 and PageCount=0) is explicitly verified; locate these
additions in the TestGetChannels_EmptyResult function immediately after err =
ctx.Response.Payload.Translate(&response).
- Around line 36-202: Tests for GetChannels are missing coverage for the
channel_type and asset filters; add handler-level tests that exercise
channelTypeFromString parsing and asset forwarding to the store. Create a
TestGetChannels_WithChannelTypeFilter that sets ChannelType = "home", expects
handler.GetChannels to call GetUserChannels with &core.ChannelTypeHome
(reference chType) and returns a matching channel, then assert the response
contains the filtered channel; also add a TestGetChannels_InvalidChannelType
that passes an invalid type string and asserts the handler returns an error (no
store call). Ensure the mock expectations reference GetUserChannels with the
appropriate (*core.ChannelStatus)(nil), (*string)(nil) or &assetString, &chType,
and pagination params so the parsing logic is exercised.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7c59a0f and 05f5c3b.

📒 Files selected for processing (10)
  • clearnode/api/channel_v1/get_channels.go
  • clearnode/api/channel_v1/get_channels_test.go
  • clearnode/api/channel_v1/interface.go
  • clearnode/api/channel_v1/testing.go
  • clearnode/store/database/channel.go
  • clearnode/store/database/channel_test.go
  • clearnode/store/database/interface.go
  • pkg/rpc/api.go
  • sdk/ts/src/client.ts
  • sdk/ts/src/rpc/api.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • sdk/ts/src/client.ts

Verifies that get_channels rejects unrecognized status strings at the
API layer (before hitting the store) now that channelStatusFromString
was moved from the database layer to the handler.
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

♻️ Duplicate comments (1)
clearnode/api/channel_v1/get_channels_test.go (1)

280-282: Weak error-content assertion is an acknowledged trade-off.

The assertion only checks ctx.Response.Type == rpc.MsgTypeRespErr without verifying the error message. Per the prior review thread this is intentional because NewErrorPayload causes Payload.Error() to return nil when the message contains quoted strings. No further action needed here, but the discrepancy versus TestGetChannels_MissingWallet and TestGetChannels_StoreError is worth documenting inline so future readers understand why the pattern differs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clearnode/api/channel_v1/get_channels_test.go` around lines 280 - 282, The
test only asserts ctx.Response.Type == rpc.MsgTypeRespErr without checking the
error message; add an inline comment above that assertion in
get_channels_test.go (near the failing assertion in the TestGetChannels_* tests)
explaining that this weaker check is intentional because NewErrorPayload causes
Payload.Error() to return nil when the message contains quoted strings, and note
that this differs from TestGetChannels_MissingWallet and
TestGetChannels_StoreError so future readers understand the discrepancy.
🧹 Nitpick comments (2)
clearnode/api/channel_v1/get_channels_test.go (2)

19-19: Consider using require.NoError for core.NewChannelDefaultSigner.

Silently discarding the error (_) means a misconfigured MockSigner will produce a nil nodeSigner and cause cryptic downstream panics instead of a clear setup failure.

🔧 Proposed fix
-	nodeSigner, _ := core.NewChannelDefaultSigner(mockSigner)
+	nodeSigner, err := core.NewChannelDefaultSigner(mockSigner)
+	require.NoError(t, err)

(This requires t *testing.T to be passed into the helper or use testing.TB.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clearnode/api/channel_v1/get_channels_test.go` at line 19, The test currently
ignores the error returned by core.NewChannelDefaultSigner, which can hide setup
failures; update the test helper to accept t (testing.T or testing.TB) and call
core.NewChannelDefaultSigner(mockSigner) capturing the error, then assert
require.NoError(t, err) before using nodeSigner so any signer creation failure
fails clearly; reference the nodeSigner variable and
core.NewChannelDefaultSigner in your changes.

208-208: Use a valid Ethereum address format for consistency.

"0xNoChannelsUser" is not a valid address. If address-format validation is added to the handler later, this test will break without any obvious connection to the change.

🔧 Proposed fix
-	userWallet := "0xNoChannelsUser"
+	userWallet := "0x0000000000000000000000000000000000000001"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clearnode/api/channel_v1/get_channels_test.go` at line 208, The test uses an
invalid Ethereum address string in the userWallet variable ("0xNoChannelsUser");
update userWallet in get_channels_test.go to a syntactically valid Ethereum
address (for example a checksummed or zero-prefixed 40-hex char address like
"0x0000000000000000000000000000000000000001") so future address-format
validation in the handler won't break the test; locate and replace the
userWallet declaration in the test function that sets userWallet to ensure it
uses a proper 0x-prefixed 40-hex character address.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@clearnode/api/channel_v1/get_channels_test.go`:
- Around line 255-256: Replace the non-fatal assertion before dereferencing the
error with a fatal one: in TestGetChannels_MissingWallet and
TestGetChannels_StoreError, use require.NotNil(t, ctx.Response.Error()) instead
of assert.NotNil(t, ctx.Response.Error()) so the test stops if the error is nil
before calling ctx.Response.Error().Error(), then keep the subsequent
assert.Contains checks as-is to verify the "wallet" message.

---

Duplicate comments:
In `@clearnode/api/channel_v1/get_channels_test.go`:
- Around line 280-282: The test only asserts ctx.Response.Type ==
rpc.MsgTypeRespErr without checking the error message; add an inline comment
above that assertion in get_channels_test.go (near the failing assertion in the
TestGetChannels_* tests) explaining that this weaker check is intentional
because NewErrorPayload causes Payload.Error() to return nil when the message
contains quoted strings, and note that this differs from
TestGetChannels_MissingWallet and TestGetChannels_StoreError so future readers
understand the discrepancy.

---

Nitpick comments:
In `@clearnode/api/channel_v1/get_channels_test.go`:
- Line 19: The test currently ignores the error returned by
core.NewChannelDefaultSigner, which can hide setup failures; update the test
helper to accept t (testing.T or testing.TB) and call
core.NewChannelDefaultSigner(mockSigner) capturing the error, then assert
require.NoError(t, err) before using nodeSigner so any signer creation failure
fails clearly; reference the nodeSigner variable and
core.NewChannelDefaultSigner in your changes.
- Line 208: The test uses an invalid Ethereum address string in the userWallet
variable ("0xNoChannelsUser"); update userWallet in get_channels_test.go to a
syntactically valid Ethereum address (for example a checksummed or zero-prefixed
40-hex char address like "0x0000000000000000000000000000000000000001") so future
address-format validation in the handler won't break the test; locate and
replace the userWallet declaration in the test function that sets userWallet to
ensure it uses a proper 0x-prefixed 40-hex character address.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 05f5c3b and 81fc923.

📒 Files selected for processing (1)
  • clearnode/api/channel_v1/get_channels_test.go

Comment on lines +255 to +256
assert.NotNil(t, ctx.Response.Error())
assert.Contains(t, ctx.Response.Error().Error(), "wallet")
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

Use require.NotNil before dereferencing the error to avoid a nil-pointer panic.

assert.NotNil is non-fatal — if it fails, execution continues and the immediate .Error() call on a nil value panics, masking the real failure. This applies to both TestGetChannels_MissingWallet (lines 255–256) and TestGetChannels_StoreError (lines 307–308).

🐛 Proposed fix
-	assert.NotNil(t, ctx.Response.Error())
-	assert.Contains(t, ctx.Response.Error().Error(), "wallet")
+	require.NotNil(t, ctx.Response.Error())
+	assert.Contains(t, ctx.Response.Error().Error(), "wallet")
-	assert.NotNil(t, ctx.Response.Error())
-	assert.Contains(t, ctx.Response.Error().Error(), "failed to get channels")
+	require.NotNil(t, ctx.Response.Error())
+	assert.Contains(t, ctx.Response.Error().Error(), "failed to get channels")

Also applies to: 307-308

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@clearnode/api/channel_v1/get_channels_test.go` around lines 255 - 256,
Replace the non-fatal assertion before dereferencing the error with a fatal one:
in TestGetChannels_MissingWallet and TestGetChannels_StoreError, use
require.NotNil(t, ctx.Response.Error()) instead of assert.NotNil(t,
ctx.Response.Error()) so the test stops if the error is nil before calling
ctx.Response.Error().Error(), then keep the subsequent assert.Contains checks
as-is to verify the "wallet" message.

@alessio
Copy link
Contributor

alessio commented Feb 23, 2026

This PR is growing too much. It risks to become unreadable. Hence, I am merging.
@ihsraham take note of suggestions, comments, and whatever is left to be done and please, open a follow up PR and continue there 👍

Great job, team!

@alessio alessio merged commit 9c8cc27 into main Feb 23, 2026
13 checks passed
@alessio alessio deleted the feat/compat-layer-and-get-channels branch February 23, 2026 12:51
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