Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/forty-coins-lose.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"ensapi": minor
"ensindexer": minor
---

Enable QuickNode RPC provider support with `QUICKNODE_API_KEY` and `QUICKNODE_ENDPOINT_NAME` environment variables.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
Enable QuickNode RPC provider support with `QUICKNODE_API_KEY` and `QUICKNODE_ENDPOINT_NAME` environment variables.
Enable auto-generated QuickNode RPC provider support with `QUICKNODE_API_KEY` and `QUICKNODE_ENDPOINT_NAME` environment variables.

5 changes: 5 additions & 0 deletions .changeset/smooth-lines-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ensnode/ensnode-sdk": minor
---

Add QuickNode RPC provider support for auto-generated chain RPC URLs.
18 changes: 0 additions & 18 deletions .github/workflows/deploy_ensnode_blue_green.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,24 +33,6 @@ jobs:
RAILWAY_PROJECT_ID: ${{ secrets.RAILWAY_PROJECT_ID }}
RAILWAY_ENVIRONMENT_ID: ${{ secrets.RAILWAY_ENVIRONMENT_ID}}
RAILWAY_TEAM_TOKEN: ${{ secrets.RAILWAY_TEAM_TOKEN }}
# Terraform related envs

# AWS_REGION is required for aws-actions/configure-aws-credentials@v4
# Terraform keeps it's state inside S3 bucket. This bucket needs to be created before running Terraform apply.
# AWS_REGION should be the same as Terraform S3 bucket state region.
AWS_REGION: us-east-1
TF_VAR_ensnode_version: ${{ inputs.tag }}
TF_VAR_ensindexer_label_set_id: ${{ vars.LABEL_SET_ID }}
TF_VAR_ensindexer_label_set_version: ${{ vars.LABEL_SET_VERSION }}
TF_VAR_ensrainbow_label_set_id: ${{ vars.LABEL_SET_ID }}
TF_VAR_ensrainbow_label_set_version: ${{ vars.LABEL_SET_VERSION }}
TF_VAR_db_schema_version: ${{ vars.DB_SCHEMA_VERSION }}
TF_VAR_anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# For now only one Terraform environment is active. In future this will be calculated based on workflow input.
TF_VAR_render_environment: "yellow"
TF_VAR_render_api_key: ${{ secrets.RENDER_API_KEY }}
TF_VAR_render_owner_id: ${{ secrets.RENDER_OWNER_ID }}
TF_VAR_alchemy_api_key: ${{ secrets.ALCHEMY_API_KEY }}

steps:
- name: Checkout repository
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/deploy_ensnode_yellow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ jobs:
TF_VAR_render_api_key: ${{ secrets.RENDER_API_KEY }}
TF_VAR_render_owner_id: ${{ secrets.RENDER_OWNER_ID }}
TF_VAR_alchemy_api_key: ${{ secrets.ALCHEMY_API_KEY }}
TF_VAR_quicknode_api_key: ${{ secrets.QUICKNODE_API_KEY }}
TF_VAR_quicknode_endpoint_name: ${{ secrets.QUICKNODE_ENDPOINT_NAME}}

steps:
- name: Checkout repository
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/test_ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ jobs:
ENSRAINBOW_URL: https://api.ensrainbow.io
ENSINDEXER_URL: http://localhost:42069
ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }}
QUICKNODE_API_KEY: ${{ secrets.QUICKNODE_API_KEY }}
QUICKNODE_ENDPOINT_NAME: ${{ secrets.QUICKNODE_ENDPOINT_NAME}}
# healthcheck script env variables
HEALTH_CHECK_TIMEOUT: 60
run: ./.github/scripts/run_ensindexer_healthcheck.sh
12 changes: 10 additions & 2 deletions apps/ensapi/.env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,25 @@ DATABASE_URL=postgresql://dbuser:abcd1234@localhost:5432/my_database
# The following environment variables are supported:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm concerned about gaps between our docs here for RPC-related environment variables and the actual use of RPCs within ENSApi.

It's important to note how ENSApi has fundamentally different RPC logic than ENSIndexer.

  1. ENSApi exclusively uses a SINGLE chain RPC, no matter what the config of the ENSIndexer is.
  2. The SINGLE chain RPC used by ENSApi is always the ENS root chain associated with the related ENSIndexer's config.
  3. If ENSApi has multiple RPC providers defined for this single chain, can you please investigate how we handle that? I understand that the single purpose of RPC definitions in ENSApi is for the resolution APIs in the case that we must make a dynamic RPC call for one reason or another and cannot rely on our indexed data to generate the response.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Looking at the current state of ENSApi and RPC configurations.

From what I can see, ENSApi allows passing multiple RPC URLs for a single chain to be managed by Viem's transport fallback.

export function getPublicClient(chainId: ChainId): PublicClient {
// Invariant: ENSIndexer must have an rpcConfig for the requested `chainId`
const rpcConfig = config.rpcConfigs.get(chainId);
if (!rpcConfig) {
throw new Error(`Invariant: ENSIndexer does not have an RPC to chain id '${chainId}'.`);
}
// create an un-cached publicClient that uses a fallback() transport with all specified HTTP RPCs
return createPublicClient({
transport: fallback(rpcConfig.httpRPCs.map((url) => http(url.toString()))),
});
}

Also, there's been a case where our protocol for Forward Resolution allows calls to chains other than the Root chain. defersToRegistry account may refer to Base chain, or Linea chain.

//////////////////////////////////////////////////
// Protocol Acceleration: CCIP-Read Shadow Registry Resolvers
// If:
// 1) the caller requested acceleration, and
// 2) the ProtocolAcceleration Plugin is active, and
// 3) the activeResolver is a CCIP-Read Shadow Registry Resolver,
// then we can short-circuit the CCIP-Read and defer resolution to the indicated
// (shadow)Registry.
//////////////////////////////////////////////////
if (accelerate) {
const defersToRegistry = possibleKnownCCIPReadShadowRegistryResolverDefersTo({
chainId,
address: activeResolver,
});
if (canAccelerate && defersToRegistry !== null) {
return withProtocolStepAsync(
TraceableENSProtocol.ForwardResolution,
ForwardResolutionProtocolStep.AccelerateKnownOffchainLookupResolver,
{},
() => _resolveForward(name, selection, { ...options, registry: defersToRegistry }),
);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@tk-o Really appreciate your investigation here 👍 Based on what you found I think it's best for now that we:

  1. Create a separate follow up issue to better clarify these topics. I've just completed that here: Improve maturity of RPC endpoint handling and docs in ENSApi #1351
  2. Consider these questions as closed for the purpose of getting PR 1345 successfully merged 👍

# - ALCHEMY_API_KEY — if set, an Alchemy RPC URL will be provided for each of the chains it supports
# - DRPC_API_KEY — if set, an DRPC RPC URL will be provided for each of the chains it supports
# - QUICKNODE_API_KEY & QUICKNODE_ENDPOINT_NAME — if both set, an QuickNode RPC URL will be provided
# for each of the chains it supports. Please note that:
# - Only multi-chain QuickNode endpoints are supported.
# https://www.quicknode.com/guides/quicknode-products/how-to-use-multichain-endpoint
# - QuickNode platform does not support Linea Sepolia RPC (as of 2025-12-03).
# https://www.quicknode.com/docs/linea
# - RPC_URL_${chainId} — specific, per-chain RPC settings (see below).
#
# If RPC_URL_${chainId} is specified, it will take precedence over the automatic RPC URLs from
# Alchemy or DRPC. It must be a comma-separated list of HTTP/HTTPS RPC endpoints. If multiple
# Alchemy, DRPC, or QuickNode. It must be a comma-separated list of HTTP/HTTPS RPC endpoints. If multiple
# HTTP RPC endpoints are provided for a single chain, they will be used in order, falling back if
# RPC errors are encountered. If multiple automatic RPC environment variables are specified, they
# will be used in the following order: Alchemy > DRPC.
# will be used in the following order: Alchemy > DRPC > QuickNode.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please see other comments on ordering.

#
Comment thread
lightwalker-eth marked this conversation as resolved.
# Automatic:
# ALCHEMY_API_KEY=xyz
# DRPC_API_KEY=xyz
# QUICKNODE_API_KEY=your-api-key
# QUICKNODE_ENDPOINT_NAME=your-endpoint-name
#
# Chain-Specific:
# RPC_URL_1=https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY
Expand Down
45 changes: 29 additions & 16 deletions apps/ensindexer/.env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,24 @@
# The following environment variables are supported:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
# The following environment variables are supported:
# Configuring the following environment variables enables auto-generation of RPC endpoint URLs for each indexed chain (with limitations as noted below):

# - ALCHEMY_API_KEY — if set, Alchemy RPC URLs (HTTP & WS) will be provided for each of the chains it supports
# - DRPC_API_KEY — if set, an DRPC RPC URL (HTTP) will be provided for each of the chains it supports
# - QUICKNODE_API_KEY & QUICKNODE_ENDPOINT_NAME — if both set, an QuickNode RPC URL will be provided
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's not clear here how to interpret both of these environment variables. Suggest to give an example of the string pattern of a full multichain Quicknode endpoint URL and then how components of that string map into these environment variable names.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please make it explicit what happens if only one of these is set (not both). A quick suggestion is to reject the config and terminate as it's probably a situation where it's better to fail loud to help the operator avoid a situation where they accidentally made a mistake.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is QUICKNODE_ENDPOINT_NAME the optimal name for this idea? It seems to me that this could easily be misinterpreted as the full endpoint URL. Appreciate your suggestions for terminology that might be more precise and less likely to have a misinterpretation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

QuickNode docs call it exactly this way, should we still aim to change that terminology:
image

I believe we should follow the terms from the official QuickNode docs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ok cool. I find their terminology confusing but agree that we should align with it 👍

# for each of the chains it supports. Please note that:
# - Only multi-chain QuickNode endpoints are supported.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please make this more clear. What exactly makes a QuickNode endpoint multichain or not? Is there a distinct format to the full URL pattern?

# https://www.quicknode.com/guides/quicknode-products/how-to-use-multichain-endpoint
# - QuickNode platform does not support Linea Sepolia RPC (as of 2025-12-03).
# https://www.quicknode.com/docs/linea
# - RPC_URL_${chainId} — specific, per-chain RPC settings (see below).
#
# If RPC_URL_${chainId} is specified, that value will take precedence over the automatic RPC URLs
# from Alchemy or DRPC. If both Alchemy and DRPC API Keys are specified, ENSIndexer will provide
# both to Ponder, which will balance requests between them (see below).
# from Alchemy, DRPC, or QuickNode. ENSIndexer will provide all available
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please update our ordering logic. QuickNode should always come before dRPC.

Please also ensure we always write it as dRPC and not DRPC (outside of contexts where of course it should be in all caps such as an environment variable name).

# RPC URLs across configured RPC providers to Ponder, which will balance
# requests between them (see below).

# Automatic:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
# Automatic:
# Auto-generated RPC URLs:

# ALCHEMY_API_KEY=xyz
# DRPC_API_KEY=xyz
# QUICKNODE_API_KEY=your-api-key
# QUICKNODE_ENDPOINT_NAME=your-endpoint-name
#
# For chain-specific RPC configuration, use the RPC_URL_{chainId} environment variable.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
# For chain-specific RPC configuration, use the RPC_URL_{chainId} environment variable.
# For full control of chain-specific RPC configuration, use the RPC_URL_{chainId} environment variable.

# Its value is a comma-separated list of one or more HTTP RPC URLs and at most one WebSocket RPC URL.
Expand Down Expand Up @@ -56,64 +69,64 @@
# Ethereum Mainnet
# - required if the configured namespace is mainnet
# - required by plugins: subgraph, protocol-acceleration, registrars, tokenscope
RPC_URL_1=
# RPC_URL_1=

# Optimism Mainnet
# - required by plugins: threedns, protocol-acceleration, tokenscope
RPC_URL_10=
# RPC_URL_10=

# Base Mainnet
# - required by plugins: basenames, threedns, protocol-acceleration, registrars, tokenscope
RPC_URL_8453=
# RPC_URL_8453=

# Arbitrum Mainnet
# - required by plugins: protocol-acceleration
RPC_URL_42161=
# RPC_URL_42161=

# Linea Mainnet
# - required by plugins: lineanames, protocol-acceleration, registrars, tokenscope
RPC_URL_59144=
# RPC_URL_59144=

# Scroll Mainnet
# - required by plugins: protocol-acceleration
RPC_URL_534352=
# RPC_URL_534352=

# === ENS Namespace: Sepolia ===
# Ethereum Sepolia (public testnet)
# - required if the configured namespace is sepolia
# - required by plugins: subgraph, protocol-acceleration, registrars, tokenscope
RPC_URL_11155111=
# RPC_URL_11155111=

# Base Sepolia (public testnet)
# - required by plugins: basenames, protocol-acceleration, registrars, tokenscope
RPC_URL_84532=
# RPC_URL_84532=

# Linea Sepolia (public testnet)
# - required by plugins: lineanames, protocol-acceleration, registrars, tokenscope
RPC_URL_59141=
# RPC_URL_59141=

# Optimism Sepolia (public testnet)
# - required by plugins: protocol-acceleration, tokenscope
RPC_URL_11155420=
# RPC_URL_11155420=

# Arbitrum Sepolia (public testnet)
# - required by plugins: protocol-acceleration
RPC_URL_421614=
# RPC_URL_421614=

# Scroll Sepolia (public testnet)
# - required by plugins: protocol-acceleration
RPC_URL_534351=
# RPC_URL_534351=

# === ENS Namespace: Holesky ===
# Ethereum Holesky (public testnet)
# - required if the configured namespace is holesky
# - required by plugins: subgraph, protocol-acceleration, tokenscope
RPC_URL_17000=
# RPC_URL_17000=

# === ENS Namespace: ens-test-env ===
# ens-test-env (local testnet)
# - required if the configured namespace is ens-test-env
RPC_URL_1337=
# RPC_URL_1337=

# Database configuration
# Required. This is a namespace for the tables that the indexer will create to store indexed data.
Expand Down
13 changes: 12 additions & 1 deletion packages/ensnode-sdk/src/shared/config/build-rpc-urls.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { lineaSepolia } from "viem/chains";
import { describe, expect, it } from "vitest";

import {
Expand All @@ -8,7 +9,7 @@ import {
getENSNamespace,
} from "@ensnode/datasources";

import { buildAlchemyBaseUrl, buildDRPCUrl } from "./build-rpc-urls";
import { buildAlchemyBaseUrl, buildDRPCUrl, buildQuickNodeURL } from "./build-rpc-urls";

const KEY = "whatever";

Expand All @@ -25,5 +26,15 @@ describe("build-rpc-urls", () => {
expect(buildAlchemyBaseUrl(chainId, KEY), `Alchemy ${chainId}`).not.toBeUndefined();
expect(buildDRPCUrl(chainId, KEY), `DRPC ${chainId}`).not.toBeUndefined();
});

// QuickNode does not support Linea Sepolia RPC (as of 2025-12-03).
ALL_KNOWN_PUBLIC_CHAIN_IDS.filter((chainId) => chainId !== lineaSepolia.id).forEach(
(chainId) => {
expect(
buildQuickNodeURL(chainId, KEY, "endpoint-name"),
`QuickNode ${chainId}`,
).not.toBeUndefined();
},
);
});
});
63 changes: 63 additions & 0 deletions packages/ensnode-sdk/src/shared/config/build-rpc-urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,73 @@ export function buildDRPCUrl(chainId: ChainId, key: string): string | undefined
}
}

/**
* Builds a QuickNode RPC base URL for the specified chain ID.
*
* @param chainId - The chain ID to build the RPC base URL for
* @param apiKey - The QuickNode API key
* @param endpointName - The QuickNode Endpoint name
* @returns The QuickNode RPC base URL, or undefined if the chain is not supported
*
* NOTE:
* - Only multi-chain QuickNode endpoints are supported.
* https://www.quicknode.com/guides/quicknode-products/how-to-use-multichain-endpoint
* - QuickNode platform does not support Linea Sepolia RPC (as of 2025-12-03).
* https://www.quicknode.com/docs/linea
*
* @example
* ```typescript
* const url = buildQuickNodeURL(1, "your-api-key", "your-endpoint-name");
* // Returns: "your-endpoint-name.quiknode.pro/your-api-key"
* ```
*/
export function buildQuickNodeURL(
chainId: ChainId,
apiKey: string,
endpointName: string,
): string | undefined {
switch (chainId) {
case mainnet.id:
return `${endpointName}.quiknode.pro/${apiKey}`;
case sepolia.id:
return `${endpointName}.ethereum-sepolia.quiknode.pro/${apiKey}`;
case holesky.id:
return `${endpointName}.ethereum-holesky.quiknode.pro/${apiKey}`;
case arbitrum.id:
return `${endpointName}.arbitrum-mainnet.quiknode.pro/${apiKey}`;
case arbitrumSepolia.id:
return `${endpointName}.arbitrum-sepolia.quiknode.pro/${apiKey}`;
case base.id:
return `${endpointName}.base-mainnet.quiknode.pro/${apiKey}`;
case baseSepolia.id:
return `${endpointName}.base-sepolia.quiknode.pro/${apiKey}`;
case optimism.id:
return `${endpointName}.optimism.quiknode.pro/${apiKey}`;
case optimismSepolia.id:
return `${endpointName}.optimism-sepolia.quiknode.pro/${apiKey}`;
case linea.id:
return `${endpointName}.linea-mainnet.quiknode.pro/${apiKey}`;
case lineaSepolia.id:
//QuickNode platform does not provide RPC for Linea Sepolia
// https://www.quicknode.com/docs/linea
return undefined;
case scroll.id:
return `${endpointName}.scroll-mainnet.quiknode.pro/${apiKey}`;
case scrollSepolia.id:
return `${endpointName}.scroll-testnet.quiknode.pro/${apiKey}`;
default:
return undefined;
}
}

export function alchemySupportsChain(chainId: ChainId) {
return buildAlchemyBaseUrl(chainId, "") !== undefined;
}

export function drpcSupportsChain(chainId: ChainId) {
return buildDRPCUrl(chainId, "") !== undefined;
}

export function quickNodeSupportsChain(chainId: ChainId) {
return buildQuickNodeURL(chainId, "", "") !== undefined;
}
2 changes: 2 additions & 0 deletions packages/ensnode-sdk/src/shared/config/environments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export interface RpcEnvironment {
[x: `RPC_URL_${number}`]: ChainIdSpecificRpcEnvironmentVariable | undefined;
ALCHEMY_API_KEY?: string;
DRPC_API_KEY?: string;
QUICKNODE_API_KEY?: string;
QUICKNODE_ENDPOINT_NAME?: string;
}

/**
Expand Down
Loading