Skip to content

refactor(ensapi): apply operation result pattern#1545

Open
tk-o wants to merge 40 commits intofeat/ensnode-result-typefrom
ensnode-result-type/registrar-actions-api
Open

refactor(ensapi): apply operation result pattern#1545
tk-o wants to merge 40 commits intofeat/ensnode-result-typefrom
ensnode-result-type/registrar-actions-api

Conversation

@tk-o
Copy link
Contributor

@tk-o tk-o commented Jan 18, 2026

Lite PR

Summary

Registrar Actions API becomes multi-version

  • Registrar Actions API has been split into two versioned endpoints
    • /api/registrar-actions is implicitly versioned as v0
      • This ensures all clients (i.e. ENSAwards website) can use the API without interruptions.
    • /api/v1/registrar-actions is explicitly versioned as v1
      • This allows clients to opt-in to using updated response data model.
      • Also, it's important to add the v1 tag before the API path. This avoids issues where Hono could mix up handlers between API versions.

Registrar Actions API V1

  • The middleware related to this API has been replaced with validateApiHandlerPrerequisites function call which allows to get indexing status object if all prerequisites were met. Otherwise, the validateApiHandlerPrerequisites function will return result error, with appropriate result code.

Result type integrated in ENSApi

  • Returning response from a route handler in ENSApi can be now streamlined with resultIntoHttpResponse function, that produces a HTTP Response object based on the provided operation result.
  • Operation result pattern has been applied to responses returned by:
    • Generic response handlers (i.e. onError, notFound)
    • GET /health endpoint
      • See related HealthServerResult result type
    • GET /amirealtime endpoint
      • See related AmIRealtimeServerResult result type
    • Registrar Actions API V1 endpoints
  • Added OpenAPI routes details for Registrar Action API and "Am I Realtime?" API
    • Route responses describe possible HTTP response codes, including response schema (using Zod schemas here), and examples across all result types that can be returned by the given route.

Result type updates in ENSNode SDK

  • Result values for every ResultCode include data field, which is guaranteed to be an object. The data field is optional for ResultCodes.Loading.
  • Extended RESULT_CODE_SERVER_ERROR_CODES with
    • ResultCodes.ServiceUnavailable
    • ResultCodes.InsufficientIndexingProgress

Why

  • This change integrates the result pattern that was introduced to ENSNode SDK in order to standardize ENSNode response data model.
  • "Explore APIs" are required to include minIndexingCursor field in their response data model to indicate to which point in time the response data is guaranteed to be up to.
  • Documentation for a HTTP endpoint should include all possible HTTP response codes and their meaning.

Testing

  • Ran all typechecks and testing suites.
  • Tested /amirealtime and /api/registrar-actions endpoints locally, simulating conditions where both, ok and error results were returned from ENSApi.

Please find most important HTTP API test cases below.

HTTP API test cases

"Am I Realtime?" API

AmIRealtimeResultOk

Request

http://localhost:4334/amirealtime?requestedMaxWorstCaseDistance=22

Response

{
  "resultCode": "ok",
  "data": {
    "requestedMaxWorstCaseDistance": 22,
    "slowestChainIndexingCursor": 1769600807,
    "worstCaseDistance": 12,
    "serverNow": 1769600819
  }
}

ResultInsufficientIndexingProgress

Request

http://localhost:4334/amirealtime?requestedMaxWorstCaseDistance=10

Response

{
  "resultCode": "insufficient-indexing-progress",
  "data": {
    "currentIndexingStatus": "omnichain-following",
    "currentIndexingCursor": 1769600831,
    "startIndexingCursor": 1489165544,
    "targetIndexingStatus": "omnichain-following",
    "targetIndexingCursor": 1769600839,
    "errorMessage": "Indexing Status 'worstCaseDistance' must be below or equal to the requested 'requestedMaxWorstCaseDistance'; worstCaseDistance = 18; requestedMaxWorstCaseDistance = 10",
    "suggestRetry": true
  }
}

Registrar Actions API

RegistrarActionsResultOk

Request

http://localhost:4334/api/v1/registrar-actions/0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae?sort%5BorderBy%5Btimestamp%5D%5D=desc&page=1&recordsPerPage=3&withReferral=true

Response

{
  "resultCode": "ok",
  "data": {
    "registrarActions": [
      {
        "action": {
          "id": "176959988300000000000000010000000024333055000000000000003850000000000000184",
          "type": "registration",
          "incrementalDuration": 94608000,
          "registrant": "0xbad9895fa47616022e648ddd846119f4c9661ed8",
          "registrationLifecycle": {
            "subregistry": {
              "subregistryId": {
                "chainId": 1,
                "address": "0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85"
              },
              "node": "0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae"
            },
            "node": "0x381c59a32d3e741a484f93d4e46f8c759a91455de19fa56cb8815c22df58a957",
            "expiresAt": 1864207883
          },
          "pricing": {
            "baseCost": {
              "currency": "ETH",
              "amount": "4971302790087598"
            },
            "premium": {
              "currency": "ETH",
              "amount": "0"
            },
            "total": {
              "currency": "ETH",
              "amount": "4971302790087598"
            }
          },
          "referral": {
            "encodedReferrer": "0x000000000000000000000000f919a96d2970380b87917b04f02e6d3d08368b10",
            "decodedReferrer": "0xf919a96d2970380b87917b04f02e6d3d08368b10"
          },
          "block": {
            "number": 24333055,
            "timestamp": 1769599883
          },
          "transactionHash": "0x14b724b0c90af0dcb2a87c3be527d4396c9ab507b20063852e02982fda047266",
          "eventIds": [
            "176959988300000000000000010000000024333055000000000000003850000000000000184",
            "176959988300000000000000010000000024333055000000000000003850000000000000188"
          ]
        },
        "name": "ainrg.eth"
      },
      {
        "action": {
          "id": "176959900700000000000000010000000024332983000000000000010350000000000000298",
          "type": "renewal",
          "incrementalDuration": 29113321,
          "registrant": "0x8bca9f3016a33b1e34135d5f536e46ed541f1a28",
          "registrationLifecycle": {
            "subregistry": {
              "subregistryId": {
                "chainId": 1,
                "address": "0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85"
              },
              "node": "0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae"
            },
            "node": "0xb3fb24ad82ecdfc478ccc8aa82a228f6b73adaa084743cf54ca3f219083e535b",
            "expiresAt": 1924856160
          },
          "pricing": {
            "baseCost": {
              "currency": "ETH",
              "amount": "1529798050017079"
            },
            "premium": {
              "currency": "ETH",
              "amount": "0"
            },
            "total": {
              "currency": "ETH",
              "amount": "1529798050017079"
            }
          },
          "referral": {
            "encodedReferrer": "0x0000000000000000000000007e491cde0fbf08e51f54c4fb6b9e24afbd18966d",
            "decodedReferrer": "0x7e491cde0fbf08e51f54c4fb6b9e24afbd18966d"
          },
          "block": {
            "number": 24332983,
            "timestamp": 1769599007
          },
          "transactionHash": "0x03aae4b10035a9f832a83cc3528489a0dbe738b5e97eb7bb6310a53484943ef0",
          "eventIds": [
            "176959900700000000000000010000000024332983000000000000010350000000000000298",
            "176959900700000000000000010000000024332983000000000000010350000000000000299",
            "176959900700000000000000010000000024332983000000000000010350000000000000300"
          ]
        },
        "name": "vendor.eth"
      },
      {
        "action": {
          "id": "176959883900000000000000010000000024332969000000000000003250000000000000181",
          "type": "registration",
          "incrementalDuration": 126144000,
          "registrant": "0x8bca9f3016a33b1e34135d5f536e46ed541f1a28",
          "registrationLifecycle": {
            "subregistry": {
              "subregistryId": {
                "chainId": 1,
                "address": "0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85"
              },
              "node": "0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae"
            },
            "node": "0xb3fb24ad82ecdfc478ccc8aa82a228f6b73adaa084743cf54ca3f219083e535b",
            "expiresAt": 1924856160
          },
          "pricing": {
            "baseCost": {
              "currency": "ETH",
              "amount": "6628403720116798"
            },
            "premium": {
              "currency": "ETH",
              "amount": "378408936502576335"
            },
            "total": {
              "currency": "ETH",
              "amount": "385037340222693133"
            }
          },
          "referral": {
            "encodedReferrer": "0x0000000000000000000000007e491cde0fbf08e51f54c4fb6b9e24afbd18966d",
            "decodedReferrer": "0x7e491cde0fbf08e51f54c4fb6b9e24afbd18966d"
          },
          "block": {
            "number": 24332969,
            "timestamp": 1769598839
          },
          "transactionHash": "0xfadfa5dd2602bc5a9a0f6717956406dc0e847f508ef08b88b4ee69675968d8ba",
          "eventIds": [
            "176959883900000000000000010000000024332969000000000000003250000000000000181",
            "176959883900000000000000010000000024332969000000000000003250000000000000185"
          ]
        },
        "name": "vendor.eth"
      }
    ],
    "pageContext": {
      "page": 1,
      "recordsPerPage": 3,
      "totalRecords": 23470,
      "totalPages": 7824,
      "hasNext": true,
      "hasPrev": false,
      "startIndex": 0,
      "endIndex": 2
    }
  },
  "minIndexingCursor": 1769600915
}

ResultInsufficientIndexingProgress

Request

http://localhost:4334/api/v1/registrar-actions/0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae?sort%5BorderBy%5Btimestamp%5D%5D=desc&page=1&recordsPerPage=3&withReferral=true

Response

{
  "resultCode": "insufficient-indexing-progress",
  "data": {
    "currentIndexingStatus": "omnichain-backfill",
    "currentIndexingCursor": 1702676652,
    "startIndexingCursor": 1686901632,
    "targetIndexingStatus": "omnichain-following",
    "targetIndexingCursor": 1769092140,
    "suggestRetry": true,
    "errorMessage": "This API is temporarily unavailable for this ENSNode instance. The cached omnichain indexing status of the connected ENSIndexer has insufficient progress."
    }
  }
}

ResultServiceUnavailable

Request

http://localhost:4334/api/v1/registrar-actions/0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae?sort%5BorderBy%5Btimestamp%5D%5D=desc&page=1&recordsPerPage=3&withReferral=true

Response

{
  "resultCode": "service-unavailable",
  "data": {
    "errorMessage": "This API is unavailable for this ENSNode instance. The connected ENSIndexer did not activate all the plugins this API requires. Active plugins: \"subgraph, basenames, lineanames\". Required plugins: \"subgraph, basenames, lineanames, registrars\".",
    "suggestRetry": false
  }
}


Notes for Reviewer (Optional)

  • This PR is a demonstration of a particular design for making Registrar Actions API to use the result pattern from ENSNode SDK. The implementation may change depending on the feedback.
  • OpenAPI spec generator requires Zod schemas to be "serialized". For example, if a given schema includes a field of z.bigint() schema, it needs to be replaced with the z.string() "serialized" couterpart. That's why we use "serialized" copies of Zod response schemas.

Pre-Review Checklist (Blocking)

PR Creation Tips
  • If this PR introduces significant changes or is higher-risk to review use the "Substantial PR" template instead.
  • Changesets should optimize for the narrative of the next autogenerated release notes. Optimize for how the resulting release notes will read to a developer in the ENS Ecosystem. Communicate all ideas with a positive frame.
  • The "Require PR Description Checks" GitHub Action will report a failing CI check on non-draft PRs where there are unchecked checkboxes in the description. You should therefore make your PR a draft PR until it is ready for review.

Related to #1305, #1355, #1368, #1512.

Copilot AI review requested due to automatic review settings January 18, 2026 18:58
@changeset-bot
Copy link

changeset-bot bot commented Jan 18, 2026

⚠️ No Changeset found

Latest commit: 1b33cad

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link
Contributor

vercel bot commented Jan 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
admin.ensnode.io Ready Ready Preview, Comment Jan 28, 2026 0:52am
2 Skipped Deployments
Project Deployment Review Updated (UTC)
ensnode.io Skipped Skipped Jan 28, 2026 0:52am
ensrainbow.io Skipped Skipped Jan 28, 2026 0:52am

@tk-o
Copy link
Contributor Author

tk-o commented Jan 18, 2026

@greptile review

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR applies the operation result pattern from ENSNode SDK to standardize response handling across ENSApi endpoints. The changes introduce new result types and builder functions, and refactor error handling to use a consistent pattern.

Changes:

  • Introduced new result pattern infrastructure in the SDK with buildResultOk, buildResultOkTimestamped, and buildResultServiceUnavailable builders
  • Added ServiceUnavailable result code and updated result type definitions
  • Refactored ENSApi endpoints (/amirealtime, /api/registrar-actions) to use the result pattern with new resultIntoHttpResponse utility
  • Removed legacy errorResponse function in favor of standardized result handling

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/ensnode-sdk/src/shared/result/result-common.ts Added buildResultOk, buildResultOkTimestamped, buildResultServiceUnavailable builders and new result type definitions
packages/ensnode-sdk/src/shared/result/result-code.ts Added ServiceUnavailable result code
packages/ensnode-sdk/src/shared/result/result-base.ts Added AbstractResultOkTimestamped type with indexing cursor support
packages/ensnode-sdk/src/api/registrar-actions/serialize.ts Added serializeNamedRegistrarActions helper function
packages/ensnode-sdk/src/api/registrar-actions/response.ts Added RegistrarActionsResultOkData and RegistrarActionsResult types
apps/ensapi/src/middleware/registrar-actions.middleware.ts Updated to use result pattern for error handling
apps/ensapi/src/lib/result/result-into-http-response.ts New utility to convert operation results to HTTP responses
apps/ensapi/src/lib/result/result-into-http-response.test.ts Comprehensive test coverage for result-to-HTTP conversion
apps/ensapi/src/lib/handlers/validate.ts Updated validation error handling to use result pattern
apps/ensapi/src/lib/handlers/error-response.ts Removed legacy error response handler
apps/ensapi/src/index.ts Updated error handlers and notFound handler to use result pattern
apps/ensapi/src/handlers/registrar-actions-api.ts Refactored to use result pattern for responses
apps/ensapi/src/handlers/amirealtime-api.ts Refactored to use result pattern for responses
apps/ensapi/src/handlers/amirealtime-api.test.ts Updated tests to verify new result pattern behavior
Comments suppressed due to low confidence (1)

apps/ensapi/src/lib/handlers/validate.ts:14

  • The documentation comment still references the old errorResponse function which has been replaced with the result pattern. Update the comment to reflect that it now uses buildResultInvalidRequest and resultIntoHttpResponse for consistent error formatting.
/**
 * Creates a Hono validation middleware with custom error formatting.
 *
 * Wraps the Hono validator with custom error handling that uses the
 * errorResponse function for consistent error formatting across the API.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Jan 18, 2026

Greptile Overview

Greptile Summary

This PR successfully implements a standardized result pattern across ENSApi, replacing ad-hoc error handling with a type-safe, structured approach. The implementation is well-architected with clear separation of concerns.

Key Changes

  • Result Pattern Implementation: Introduced AbstractResult types (AbstractResultOk, AbstractResultError, AbstractResultOkTimestamped) that provide a consistent response structure across all API endpoints
  • Multi-Version API Support: Split Registrar Actions API into v0 (legacy, backward-compatible) and v1 (using result pattern) to avoid breaking existing clients
  • Unified Prerequisite Validation: Replaced endpoint-specific middleware with validateApiHandlerPrerequisites function that returns result objects for indexing status and plugin checks
  • HTTP Response Mapping: Centralized result-to-HTTP conversion in resultIntoHttpResponse with explicit status code mappings
  • OpenAPI Documentation: Added comprehensive response schemas and examples for all result codes (200, 400, 500, 503)
  • Explore APIs Enhancement: V1 endpoints now include minIndexingCursor field to indicate data freshness guarantees

Implementation Quality

The code demonstrates strong engineering practices:

  • Type safety with discriminated unions on resultCode
  • Zod schemas match TypeScript types correctly
  • Comprehensive test coverage for result conversion and API handlers
  • Clean builder pattern for constructing results
  • Serialization support for bigints in API responses

The changes align well with the stated goals of standardizing responses and preparing for "Explore APIs" requirements.

Confidence Score: 5/5

  • This PR is safe to merge - well-tested, maintains backward compatibility, and follows established patterns
  • The implementation is thorough with comprehensive test coverage, type-safe abstractions, and backward compatibility via v0/v1 API versioning. No critical issues found.
  • No files require special attention

Important Files Changed

Filename Overview
packages/ensnode-sdk/src/shared/result/result-base.ts Defines core result pattern types - clean abstract interfaces for Ok, Error, and Loading states
packages/ensnode-sdk/src/shared/result/result-common.ts Implements common result builders for error types (ServiceUnavailable, InsufficientIndexingProgress, etc.)
apps/ensapi/src/lib/result/result-into-http-response.ts Maps result codes to HTTP status codes and converts results to JSON responses
apps/ensapi/src/lib/handlers/api-handler-prerequisites.ts Validates indexing status and plugin requirements, returning appropriate result errors
apps/ensapi/src/handlers/registrar-actions-api/v1.ts V1 endpoint using result pattern with validateApiHandlerPrerequisites and resultIntoHttpResponse
apps/ensapi/src/handlers/amirealtime-api.ts Implements realtime check endpoint using result pattern with comprehensive test coverage
apps/ensapi/src/lib/open-api/registrar-actions-api-responses.ts OpenAPI response schemas with examples for all result codes (200, 400, 500, 503)

Sequence Diagram

sequenceDiagram
    participant Client
    participant Hono as Hono Router
    participant Middleware as indexingStatusMiddleware
    participant Handler as API Handler (v1)
    participant Validator as validateApiHandlerPrerequisites
    participant Builder as Result Builder
    participant Converter as resultIntoHttpResponse
    
    Client->>Hono: GET /api/v1/registrar-actions/:parentNode
    Hono->>Middleware: Execute middleware
    Middleware->>Middleware: Fetch & cache indexing status
    Middleware->>Hono: Set c.var.indexingStatus
    Hono->>Handler: Route to handler
    Handler->>Validator: validateApiHandlerPrerequisites(indexingStatus, requiredPlugins)
    
    alt Prerequisites not met
        Validator->>Validator: Check indexing status exists
        Validator->>Validator: Check required plugins active
        Validator->>Validator: Check indexing progress sufficient
        Validator-->>Handler: Return error result (ServiceUnavailable/InsufficientIndexingProgress)
        Handler->>Converter: resultIntoHttpResponse(c, errorResult)
        Converter->>Converter: Map result code to HTTP status (503)
        Converter-->>Client: Return HTTP 503 with error JSON
    else Prerequisites met
        Validator-->>Handler: Return ResultOk with indexingStatus
        Handler->>Handler: Fetch registrar actions data
        Handler->>Builder: buildResultOkRegistrarActions(data, minIndexingCursor)
        Builder-->>Handler: Return ResultOkRegistrarActions
        Handler->>Handler: serializeResultOkRegistrarActions(result)
        Handler->>Converter: resultIntoHttpResponse(c, serializedResult)
        Converter->>Converter: Map result code to HTTP status (200)
        Converter-->>Client: Return HTTP 200 with success JSON
    end
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

14 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

@tk-o tk-o force-pushed the ensnode-result-type/registrar-actions-api branch from 745bf8b to 902d556 Compare January 18, 2026 19:08
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io January 18, 2026 19:08 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io January 18, 2026 19:08 Inactive
@tk-o
Copy link
Contributor Author

tk-o commented Jan 18, 2026

@greptile re-review

Copilot AI review requested due to automatic review settings January 19, 2026 05:50
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io January 19, 2026 05:50 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io January 19, 2026 05:50 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io January 19, 2026 05:50 Inactive
@tk-o tk-o force-pushed the ensnode-result-type/registrar-actions-api branch from e626f17 to 412e722 Compare January 19, 2026 05:51
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io January 19, 2026 05:51 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io January 19, 2026 05:51 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io January 19, 2026 05:51 Inactive
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@vercel vercel bot temporarily deployed to Preview – ensnode.io January 19, 2026 07:22 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io January 19, 2026 07:22 Inactive
OpenAPI spec generator requires a Zod schema not to include any fields that cannot be serialized, for example, `z.bigint()`, and replace such fields with a serializable schema counterpart, for example, `z.string()`.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 36 out of 36 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Member

@lightwalker-eth lightwalker-eth left a comment

Choose a reason for hiding this comment

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

@tk-o Thanks. Reviewed and shared feedback.

/**
* Successful result data for Health API requests.
*/
export type HealthResultOkData = string;
Copy link
Member

Choose a reason for hiding this comment

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

I'm thinking we want to guarantee that data is always an object? What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it's not necessary, what value would it bring vs keeping the data structure as flat as possible?

Copy link
Member

Choose a reason for hiding this comment

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

@tk-o It's nice if our result data model makes this guarantee for all possible results.

Copy link
Member

Choose a reason for hiding this comment

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

@tk-o For example: Such a guarantee may be helpful for completely generic tooling related to results.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you want for data to be guaranteed to be object, we need the following update:

export interface AbstractResultOk<TDataType extends object> extends AbstractResult<typeof ResultCodes.Ok> {
  /**
   * The data of the result.
   */
  data: TDataType;
}

@lightwalker-eth how does that sound?

});

// log hono errors to console
app.onError((error, ctx) => {
Copy link
Member

Choose a reason for hiding this comment

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

Suggest we use a consistent name for the context variable across all Hono callbacks

Comment on lines +64 to +66
supportedIndexingStatusId:
| typeof OmnichainIndexingStatusIds.Completed
| typeof OmnichainIndexingStatusIds.Following;
Copy link
Member

Choose a reason for hiding this comment

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

Suggest to define a new type in the indexing status file for the idea of an "indexing status end state" that would be a type union of completed and following.

We should document how these are "end states" because once indexing reaches one of these statuses, it will continue in that status forever.

Then you can reference that newly defined type union here and anywhere else where it is relevant.

one of the following:
The Registrar Actions API will be available once the omnichain indexing status becomes{" "}
<Badge variant="secondary">
{formatOmnichainIndexingStatus(registrarActions.supportedIndexingStatusId)}
Copy link
Member

Choose a reason for hiding this comment

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

I'm worried that our client-code is interpreting this value as a strict enum, rather than as an arbitrary string.

It's super important we put a high priority on these distinctions.

Copy link
Contributor Author

@tk-o tk-o Jan 27, 2026

Choose a reason for hiding this comment

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

Can you please expand on that note?

Looking at formatOmnichainIndexingStatus function definition:

export function formatOmnichainIndexingStatus(status: OmnichainIndexingStatusId): string {
  const [, formattedStatus] = status.split("-");

  return formattedStatus;
}

Did you mean its input parameter type should be string, like so?

export function formatOmnichainIndexingStatus(status: string): string {
  const [, formattedStatus] = status.split("-");

  return formattedStatus;
}

Copy link
Member

Choose a reason for hiding this comment

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

@tk-o Your question here is about UI-level concerns. However the main idea I'm trying to flag here is about data-model level concerns in relation to change management.

Here's a principle that should influence all our work: avoid breaking changes to our APIs (as soon as we can achieve the first "stable" API release).

Therefore, we must work hard now to design the first "stable" API release so that we can avoid breaking changes as we continue iterating in the future!

For any enum, we must thoughtfully consider future change management:

  1. Are we ready to commit to NEVER introducing a new value to this enum without it being a breaking change requiring a new incremented /vX/... in the API route?
  2. Or do we want the flexibility to introduce a new value to this enum without it being a breaking change?

Use of option 1 should be VERY selective and deliberate. It should never be an accidental oversight.

Use of option 2 requires that we explicitly prepare clients for gracefully handling new enum values that they won't recognize because they come from a future server version.

Do you have any questions on the above?

What do you recommend for this OmnichainIndexingStatusId enum? Should it use option 1 or option 2? Why is that your recommendation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@lightwalker-eth Yes, it's likely some enums will have to be extended to cover new cases. This may lead to situation where client-side code does not recognize a newly added enum variant included in server response.

I would go ahead with option 1.


As I proposed during our last team call, I think the client should be able to pick which version of the API response schema (i.e. v1, v2, etc.) it expects. This would leave no room for unnoticed breaking changes. The change management would rely on a migration process. This is the option 1 you mentioned above.

For example:

  1. The client app has been using the v1 data model provided by ENSNodeClientV1 instance.
  2. Now the client app needs to use the v2 data model.
    • The client app has to migrate from using ENSNodeClientV1 instance to the ENSNodeClientV2 instance.

As for option 2, if we wanted to use it just for enums, we would need to make the client app to handle the "unrecognized" enum variants. In other words, the client app would have to know which variants of the given enum are "recognized", so all other variants could be considered "unrecognized".

If we went down this way, we would have to wrap all enum values in some data wrapper type to allow expressing a generic "unrecognized" variant, so the client app could handle it accordingly.

* @returns Corresponding HTTP status code
*/
export function resultCodeToHttpStatusCode(resultCode: ServerResultCode): ContentfulStatusCode {
switch (resultCode) {
Copy link
Member

Choose a reason for hiding this comment

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

For optimized maintainability and type-checking, suggest defining a Record that maps from ServerResultCode -> ContentfulStatusCode. Then this function can just:

return recordName[resultCode];

And it gets nice type safety, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Cool idea!

Comment on lines +78 to +81
const errorMessage = [
`Registrar Actions API is not available.`,
`Indexing status is currently unavailable to this ENSApi instance.`,
].join(" ");
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const errorMessage = [
`Registrar Actions API is not available.`,
`Indexing status is currently unavailable to this ENSApi instance.`,
].join(" ");
const errorMessage = [
`This API is temporarily unavailable for this ENSNode instance.`,
`The indexing status has not been loaded by ENSApi yet.`,
].join(" ");

return resultIntoHttpResponse(c, result);
}

if (c.var.indexingStatus instanceof Error) {
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand why we designed the indexing status middleware this way. It's a big pain for all consumers of the middleware to perform this check.

Is there a special reason why the indexing status middleware doesn't internally ensure that the indexing status will be available to downstream consumers, and if not, to return this error?

const targetIndexingStatus =
registrarActionsPrerequisites.getSupportedIndexingStatus(configTypeId);

const errorMessage = [
Copy link
Member

Choose a reason for hiding this comment

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

We're going to need to repeat this error logic in many places. We should define it in a more general way that's not specific to registrar actions. Please see my other related comments.

@@ -28,10 +46,96 @@ app.get(
200: {
Copy link
Member

Choose a reason for hiding this comment

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

Please see my other feedback about these massive objects.

getSupportedIndexingStatus,
} = registrarActionsPrerequisites;

/**
Copy link
Member

Choose a reason for hiding this comment

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

A key goal of our work here is to fully remove the need for the whole "use stateful registrar actions" concept.

We should just have a "use registrar actions" that makes a single stateless API request. The client can then appropriately handle errors associated with insufficient indexing status or a config that won't support the API request.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree on the goal, but this PR is not aiming to update the client code. It will be done in a follow-up PR to reduce the PR scope.

tk-o added 9 commits January 27, 2026 17:47
Require Result OK to always include an object in `data` field. Also, require all Result errors to include `data` object with mandatory `errorMessage` and `suggestRetry` fields.
Refine naming convention for Result type variants.
This change supports sharing logic that determines if API handler is supported by the current Indexing Status
This will improve readability on the API handlers level
Use `IndexingStatusForSupportedApiResult` object to replace middleware approach and simplify route handler code.
Use newly added `data` property for accessing `errorMessage`
Flatten the structure, refine field names
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 49 out of 49 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (2)

packages/ensnode-sdk/src/shared/result/zod-schemas.ts:1

  • The schema name makeResultErrorInsufficientIndexingProgressDataSchema is inconsistent with the actual type ResultInsufficientIndexingProgressData. The schema includes "Error" but the type does not. Consider renaming to makeResultInsufficientIndexingProgressDataSchema to match the type name exactly.
    packages/ensnode-sdk/src/api/shared/pagination/response.ts:1
  • The removal of startIndex and endIndex fields from ResponsePageContextWithNoRecords is not documented or explained. These fields were previously defined as undefined when there are no records. Consider adding a comment explaining why these fields are no longer needed for the no-records case, or ensuring this change is intentional and doesn't break existing consumers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 51 out of 52 changed files in this pull request and generated 8 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@tk-o
Copy link
Contributor Author

tk-o commented Jan 28, 2026

@greptile review

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 55 out of 55 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

7 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

tk-o added 2 commits January 28, 2026 13:04
Make sure Hono is not mixing up handlers between different API versions.
@tk-o
Copy link
Contributor Author

tk-o commented Jan 28, 2026

@greptile review

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

7 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 56 out of 56 changed files in this pull request and generated 11 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor Author

@tk-o tk-o left a comment

Choose a reason for hiding this comment

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

Self-review completed

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.

4 participants