diff --git a/e2e/src/e2e.spec.ts b/e2e/src/e2e.spec.ts index 79da78b577..c85844fd54 100644 --- a/e2e/src/e2e.spec.ts +++ b/e2e/src/e2e.spec.ts @@ -19,6 +19,14 @@ import { } from './helper/tests'; import { init } from './init'; +const RPC_OVERRIDE = process.env['LIT_YELLOWSTONE_PRIVATE_RPC_URL']; +if (RPC_OVERRIDE) { + console.log( + '๐Ÿงช E2E: Using RPC override (LIT_YELLOWSTONE_PRIVATE_RPC_URL):', + RPC_OVERRIDE + ); +} + describe('all', () => { // Singleton baby let ctx: Awaited>; @@ -161,3 +169,114 @@ describe('all', () => { }); }); }); + +describe('rpc override', () => { + const TEST_RPC = process.env.LIT_YELLOWSTONE_PRIVATE_RPC_URL; + // const TEST_RPC = 'https://yellowstone-override.example'; + + // beforeAll(() => { + // process.env.LIT_YELLOWSTONE_PRIVATE_RPC_URL = TEST_RPC; + // }); + + // afterAll(() => { + // process.env.LIT_YELLOWSTONE_PRIVATE_RPC_URL = ORIGINAL_RPC; + // }); + + it('applies env rpc override to module and client', async () => { + const networks = await import('@lit-protocol/networks'); + + // choose module by NETWORK env (same way init.ts does) + const network = process.env.NETWORK || 'naga-dev'; + const importNameMap: Record = { + 'naga-dev': 'nagaDev', + 'naga-test': 'nagaTest', + 'naga-local': 'nagaLocal', + 'naga-staging': 'nagaStaging', + }; + const importName = importNameMap[network]; + const baseModule: any = (networks as any)[importName]; + + // apply override + const mod = + typeof baseModule.withOverrides === 'function' + ? baseModule.withOverrides({ rpcUrl: TEST_RPC }) + : baseModule; + + // log for verification + // base vs effective (when override is supported) + const baseRpcUrl = + typeof baseModule.getRpcUrl === 'function' + ? baseModule.getRpcUrl() + : 'n/a'; + const effRpcUrl = + typeof mod.getRpcUrl === 'function' ? mod.getRpcUrl() : 'n/a'; + // eslint-disable-next-line no-console + console.log('[rpc-override] TEST_RPC:', TEST_RPC); + // eslint-disable-next-line no-console + console.log( + '[rpc-override] module rpc (base โ†’ effective):', + baseRpcUrl, + 'โ†’', + effRpcUrl + ); + try { + const baseChain = + typeof baseModule.getChainConfig === 'function' + ? baseModule.getChainConfig() + : null; + const effChain = + typeof mod.getChainConfig === 'function' ? mod.getChainConfig() : null; + if (baseChain && effChain) { + // eslint-disable-next-line no-console + console.log( + '[rpc-override] module chain id/name (base โ†’ effective):', + `${baseChain.id}/${baseChain.name}`, + 'โ†’', + `${effChain.id}/${effChain.name}` + ); + // eslint-disable-next-line no-console + console.log( + '[rpc-override] module rpcUrls.default.http (base โ†’ effective):', + baseChain.rpcUrls.default.http, + 'โ†’', + effChain.rpcUrls.default.http + ); + // eslint-disable-next-line no-console + console.log( + '[rpc-override] module rpcUrls.public.http (base โ†’ effective):', + (baseChain.rpcUrls as any)['public']?.http, + 'โ†’', + (effChain.rpcUrls as any)['public']?.http + ); + } + } catch {} + + // module reflects override + expect(mod.getRpcUrl()).toBe(TEST_RPC); + const chain = mod.getChainConfig(); + expect(chain.rpcUrls.default.http[0]).toBe(TEST_RPC); + expect((chain.rpcUrls as any)['public'].http[0]).toBe(TEST_RPC); + + // client reflects override + const { createLitClient } = await import('@lit-protocol/lit-client'); + const client = await createLitClient({ network: mod }); + const cc = client.getChainConfig(); + + // eslint-disable-next-line no-console + console.log('[rpc-override] client rpcUrl:', cc.rpcUrl); + // eslint-disable-next-line no-console + console.log( + '[rpc-override] client viem rpcUrls.default:', + cc.viemConfig.rpcUrls.default.http + ); + // eslint-disable-next-line no-console + console.log( + '[rpc-override] client viem rpcUrls.public:', + (cc.viemConfig.rpcUrls as any)['public']?.http + ); + + expect(cc.rpcUrl).toBe(TEST_RPC); + expect(cc.viemConfig.rpcUrls.default.http[0]).toBe(TEST_RPC); + expect((cc.viemConfig.rpcUrls as any)['public'].http[0]).toBe(TEST_RPC); + }); +}); diff --git a/e2e/src/init.ts b/e2e/src/init.ts index 5b8075672c..24079ca3c1 100644 --- a/e2e/src/init.ts +++ b/e2e/src/init.ts @@ -109,7 +109,21 @@ export const init = async ( // Dynamic import of network module const networksModule = await import('@lit-protocol/networks'); - const _networkModule = networksModule[config.importName]; + const _baseNetworkModule = networksModule[config.importName]; + + // Optional RPC override from env + const rpcOverride = process.env['LIT_YELLOWSTONE_PRIVATE_RPC_URL']; + const _networkModule = + rpcOverride && typeof _baseNetworkModule.withOverrides === 'function' + ? _baseNetworkModule.withOverrides({ rpcUrl: rpcOverride }) + : _baseNetworkModule; + + if (rpcOverride) { + console.log( + 'โœ… Using RPC override (LIT_YELLOWSTONE_PRIVATE_RPC_URL):', + rpcOverride + ); + } // Fund accounts based on network type const isLocal = config.type === 'local'; diff --git a/packages/auth-services/src/_setup/initSystemContext.ts b/packages/auth-services/src/_setup/initSystemContext.ts index 0162d554db..7f0b728942 100644 --- a/packages/auth-services/src/_setup/initSystemContext.ts +++ b/packages/auth-services/src/_setup/initSystemContext.ts @@ -15,7 +15,13 @@ declare global { }; } -export async function initSystemContext({ appName }: { appName: string }) { +export async function initSystemContext({ + appName, + rpcUrl, +}: { + appName: string; + rpcUrl?: string; +}) { console.log('๐Ÿ”ฅ [initSystemContext] Initializing system context...'); let networkModule: any; @@ -34,8 +40,22 @@ export async function initSystemContext({ appName }: { appName: string }) { throw new Error(`Unsupported network: ${env.NETWORK}`); } + const overrideRpc = rpcUrl || env.LIT_TXSENDER_RPC_URL; + + // Apply runtime override if rpcUrl provided + const effectiveModule = + overrideRpc && typeof networkModule.withOverrides === 'function' + ? networkModule.withOverrides({ rpcUrl: overrideRpc }) + : networkModule; + + try { + const baseRpc = typeof networkModule.getRpcUrl === 'function' ? networkModule.getRpcUrl() : 'n/a'; + const effRpc = typeof effectiveModule.getRpcUrl === 'function' ? effectiveModule.getRpcUrl() : 'n/a'; + console.log('[initSystemContext] RPC (base โ†’ effective):', baseRpc, 'โ†’', effRpc); + } catch {} + const litClient = await createLitClient({ - network: networkModule, + network: effectiveModule, }); const account = privateKeyToAccount(env.LIT_TXSENDER_PRIVATE_KEY as Hex); diff --git a/packages/auth-services/src/auth-server/src/createAuthServer.ts b/packages/auth-services/src/auth-server/src/createAuthServer.ts index 08f14bbcce..a28e345773 100644 --- a/packages/auth-services/src/auth-server/src/createAuthServer.ts +++ b/packages/auth-services/src/auth-server/src/createAuthServer.ts @@ -91,7 +91,10 @@ export const createLitAuthServer = ( // ============================================================= // Ensure system context is initialized before server fully starts, using appName from config // This was originally at the top level, moved here to use config.appName - await initSystemContext({ appName: config.appName }); + await initSystemContext({ + appName: config.appName, + rpcUrl: config.litTxsenderRpcUrl, + }); }) .get('/', () => ({ message: 'PKP Auth Service is running. PKP minting is now asynchronous.', diff --git a/packages/auth-services/src/queue-manager/worker.ts b/packages/auth-services/src/queue-manager/worker.ts index 92c2f7c128..0836c32e65 100644 --- a/packages/auth-services/src/queue-manager/worker.ts +++ b/packages/auth-services/src/queue-manager/worker.ts @@ -1,6 +1,7 @@ import { initSystemContext } from '../_setup/initSystemContext'; import { bullmqConnectionOptions, mainQueueName } from './src/bullmqSetup'; import { createGenericWorker } from './src/genericWorker'; +import { env } from '../env'; interface ParsedRedisConnectionOpts { host?: string; @@ -10,7 +11,7 @@ interface ParsedRedisConnectionOpts { } export async function startAuthServiceWorker() { - await initSystemContext({ appName: 'auth-services-worker' }); + await initSystemContext({ appName: 'auth-services-worker', rpcUrl: env.LIT_TXSENDER_RPC_URL }); console.log('------------------------------------------------------'); console.log(' Attempting to start Generic BullMQ Worker Process... '); console.log('------------------------------------------------------'); diff --git a/packages/lit-client/src/lib/LitClient/createLitClient.ts b/packages/lit-client/src/lib/LitClient/createLitClient.ts index 618759aef2..69d7e08faf 100644 --- a/packages/lit-client/src/lib/LitClient/createLitClient.ts +++ b/packages/lit-client/src/lib/LitClient/createLitClient.ts @@ -10,9 +10,9 @@ import { } from '@lit-protocol/access-control-conditions'; import { encrypt as blsEncrypt } from '@lit-protocol/crypto'; import { getChildLogger } from '@lit-protocol/logger'; -import type { - LitNetworkModule, - PKPStorageProvider, +import { + type LitNetworkModule, + type PKPStorageProvider, } from '@lit-protocol/networks'; import { AuthContextSchema2, diff --git a/packages/lit-client/src/lib/LitClient/intergrations/createPkpViemAccount.ts b/packages/lit-client/src/lib/LitClient/intergrations/createPkpViemAccount.ts index 3b9c8f98e7..ebec88ae2c 100644 --- a/packages/lit-client/src/lib/LitClient/intergrations/createPkpViemAccount.ts +++ b/packages/lit-client/src/lib/LitClient/intergrations/createPkpViemAccount.ts @@ -120,16 +120,14 @@ export async function createPKPViemAccount({ tx, address, chain, - transportUrl, }: { tx: Partial; address: `0x${string}`; chain: Chain; - transportUrl: string; }): Promise { const client = createPublicClient({ chain, - transport: http(transportUrl), + transport: http(chain.rpcUrls.default.http[0]), }); try { @@ -246,7 +244,6 @@ export async function createPKPViemAccount({ tx: txRequest, address, chain: chainConfig, - transportUrl: chainConfig.rpcUrls.default.http[0], }); } else { // Ensure minimum required fields for transaction type inference diff --git a/packages/networks/src/chains/ChronicleYellowstone.ts b/packages/networks/src/chains/ChronicleYellowstone.ts index 9304adc8b1..5d39e61841 100644 --- a/packages/networks/src/chains/ChronicleYellowstone.ts +++ b/packages/networks/src/chains/ChronicleYellowstone.ts @@ -41,3 +41,27 @@ export const WagmiConfig = createConfig({ [viemChainConfig.id]: http(), }, }); + +/** + * Resolve the effective RPC URL from an optional override. + */ +export function resolveRpcUrl(overrideRpc?: string): string { + return overrideRpc ?? RPC_URL; +} + +/** + * Build a Chain config using the base Chronicle Yellowstone config, + * applying an optional RPC override to both default and public http URLs. + */ +export function buildViemChainConfig(overrideRpc?: string): Chain { + const rpc = resolveRpcUrl(overrideRpc); + const base = viemChainConfig; + return { + ...base, + rpcUrls: { + ...base.rpcUrls, + default: { ...base.rpcUrls.default, http: [rpc] }, + public: { ...(base.rpcUrls as any)['public'], http: [rpc] }, + }, + } as Chain; +} diff --git a/packages/networks/src/networks/vNaga/envs/base/BaseNetworkEnvironment.ts b/packages/networks/src/networks/vNaga/envs/base/BaseNetworkEnvironment.ts index 3e2b4f20db..ee5c110a14 100644 --- a/packages/networks/src/networks/vNaga/envs/base/BaseNetworkEnvironment.ts +++ b/packages/networks/src/networks/vNaga/envs/base/BaseNetworkEnvironment.ts @@ -15,6 +15,7 @@ export interface BaseEnvironmentOptions { minimumThreshold?: number; httpProtocol?: 'http://' | 'https://'; requiredAttestation?: boolean; + rpcUrlOverride?: string; } export abstract class BaseNetworkEnvironment { @@ -24,9 +25,9 @@ export abstract class BaseNetworkEnvironment { this.config = { minimumThreshold: options.minimumThreshold || 3, network: options.network, - rpcUrl: this.getRpcUrl(), + rpcUrl: this.getRpcUrl(options.rpcUrlOverride), abiSignatures: options.abiSignatures, - chainConfig: this.getChainConfig(), + chainConfig: this.getChainConfig(options.rpcUrlOverride), httpProtocol: options.httpProtocol || 'https://', networkSpecificConfigs: options.networkSpecificConfigs, endpoints: this.getEndpoints(), @@ -51,8 +52,8 @@ export abstract class BaseNetworkEnvironment { return this.config.services; } - protected abstract getRpcUrl(): string; - protected abstract getChainConfig(): Chain; + protected abstract getRpcUrl(overrideRpc?: string): string; + protected abstract getChainConfig(overrideRpc?: string): Chain; protected abstract getEndpoints(): NagaEndpointsType; protected abstract getDefaultRealmId(): bigint; } diff --git a/packages/networks/src/networks/vNaga/envs/naga-dev/naga-dev.env.ts b/packages/networks/src/networks/vNaga/envs/naga-dev/naga-dev.env.ts index a132adf503..4f5add57ad 100644 --- a/packages/networks/src/networks/vNaga/envs/naga-dev/naga-dev.env.ts +++ b/packages/networks/src/networks/vNaga/envs/naga-dev/naga-dev.env.ts @@ -20,7 +20,7 @@ export class NagaDevEnvironment extends BaseNetworkEnvironment< NagaDevSignatures, NagaDevSpecificConfigs > { - constructor() { + constructor(options?: { rpcUrlOverride?: string }) { super({ network: NETWORK, abiSignatures: nagaDevSignatures, @@ -34,15 +34,16 @@ export class NagaDevEnvironment extends BaseNetworkEnvironment< minimumThreshold: MINIMUM_THRESHOLD, httpProtocol: PROTOCOL, requiredAttestation: false, + rpcUrlOverride: options?.rpcUrlOverride, }); } - protected getRpcUrl(): string { - return chainInfo.RPC_URL; + protected getRpcUrl(overrideRpc?: string): string { + return chainInfo.resolveRpcUrl(overrideRpc); } - protected getChainConfig(): Chain { - return chainInfo.viemChainConfig; + protected getChainConfig(overrideRpc?: string): Chain { + return chainInfo.buildViemChainConfig(overrideRpc); } protected getEndpoints(): NagaEndpointsType { diff --git a/packages/networks/src/networks/vNaga/envs/naga-local/naga-local.env.ts b/packages/networks/src/networks/vNaga/envs/naga-local/naga-local.env.ts index 7830ee2e4d..f41146f021 100644 --- a/packages/networks/src/networks/vNaga/envs/naga-local/naga-local.env.ts +++ b/packages/networks/src/networks/vNaga/envs/naga-local/naga-local.env.ts @@ -21,7 +21,7 @@ export class NagaLocalEnvironment extends BaseNetworkEnvironment< NagaLocalSignatures, NagaLocalSpecificConfigs > { - constructor() { + constructor(options?: { rpcUrlOverride?: string }) { super({ network: NETWORK, abiSignatures: signatures, // Note: Uses locally generated signatures @@ -36,15 +36,25 @@ export class NagaLocalEnvironment extends BaseNetworkEnvironment< minimumThreshold: MINIMUM_THRESHOLD, httpProtocol: PROTOCOL, // Note: HTTP not HTTPS requiredAttestation: false, + rpcUrlOverride: options?.rpcUrlOverride, }); } - protected getRpcUrl(): string { - return chainInfo.RPC_URL; // Note: Uses Anvil instead of ChronicleYellowstone + protected getRpcUrl(overrideRpc?: string): string { + return overrideRpc ?? chainInfo.RPC_URL; // Note: Uses Anvil instead of ChronicleYellowstone } - protected getChainConfig(): Chain { - return chainInfo.viemChainConfig; // Note: Anvil chain config + protected getChainConfig(overrideRpc?: string): Chain { + const rpc = overrideRpc ?? chainInfo.RPC_URL; + const base = chainInfo.viemChainConfig; // Note: Anvil chain config + return { + ...base, + rpcUrls: { + ...base.rpcUrls, + default: { ...base.rpcUrls.default, http: [rpc] }, + public: { ...(base.rpcUrls as any)['public'], http: [rpc] }, + }, + } as Chain; } protected getEndpoints(): NagaEndpointsType { diff --git a/packages/networks/src/networks/vNaga/envs/naga-staging/naga-staging.env.ts b/packages/networks/src/networks/vNaga/envs/naga-staging/naga-staging.env.ts index 395da779e4..c2bd859d86 100644 --- a/packages/networks/src/networks/vNaga/envs/naga-staging/naga-staging.env.ts +++ b/packages/networks/src/networks/vNaga/envs/naga-staging/naga-staging.env.ts @@ -20,7 +20,7 @@ export class NagaStagingEnvironment extends BaseNetworkEnvironment< NagaStagingSignatures, NagaStagingSpecificConfigs > { - constructor() { + constructor(options?: { rpcUrlOverride?: string }) { super({ network: NETWORK, abiSignatures: nagaStagingSignatures, @@ -34,15 +34,16 @@ export class NagaStagingEnvironment extends BaseNetworkEnvironment< minimumThreshold: MINIMUM_THRESHOLD, httpProtocol: PROTOCOL, requiredAttestation: true, + rpcUrlOverride: options?.rpcUrlOverride, }); } - protected getRpcUrl(): string { - return chainInfo.RPC_URL; + protected getRpcUrl(overrideRpc?: string): string { + return chainInfo.resolveRpcUrl(overrideRpc); } - protected getChainConfig(): Chain { - return chainInfo.viemChainConfig; + protected getChainConfig(overrideRpc?: string): Chain { + return chainInfo.buildViemChainConfig(overrideRpc); } protected getEndpoints(): NagaEndpointsType { diff --git a/packages/networks/src/networks/vNaga/envs/naga-test/naga-test.env.ts b/packages/networks/src/networks/vNaga/envs/naga-test/naga-test.env.ts index d34fb0bdfb..b7413d1445 100644 --- a/packages/networks/src/networks/vNaga/envs/naga-test/naga-test.env.ts +++ b/packages/networks/src/networks/vNaga/envs/naga-test/naga-test.env.ts @@ -20,7 +20,7 @@ export class NagaTestEnvironment extends BaseNetworkEnvironment< NagaTestSignatures, NagaTestSpecificConfigs > { - constructor() { + constructor(options?: { rpcUrlOverride?: string }) { super({ network: NETWORK, abiSignatures: nagaTestSignatures, @@ -34,15 +34,16 @@ export class NagaTestEnvironment extends BaseNetworkEnvironment< minimumThreshold: MINIMUM_THRESHOLD, httpProtocol: PROTOCOL, requiredAttestation: true, + rpcUrlOverride: options?.rpcUrlOverride, }); } - protected getRpcUrl(): string { - return chainInfo.RPC_URL; + protected getRpcUrl(overrideRpc?: string): string { + return chainInfo.resolveRpcUrl(overrideRpc); } - protected getChainConfig(): Chain { - return chainInfo.viemChainConfig; + protected getChainConfig(overrideRpc?: string): Chain { + return chainInfo.buildViemChainConfig(overrideRpc); } protected getEndpoints(): NagaEndpointsType { diff --git a/packages/networks/src/networks/vNaga/shared/factories/BaseModuleFactory.ts b/packages/networks/src/networks/vNaga/shared/factories/BaseModuleFactory.ts index a137525e72..ff976052e3 100644 --- a/packages/networks/src/networks/vNaga/shared/factories/BaseModuleFactory.ts +++ b/packages/networks/src/networks/vNaga/shared/factories/BaseModuleFactory.ts @@ -16,6 +16,7 @@ import { z } from 'zod'; import { LitNetworkModuleBase } from '../../../types'; import type { ExpectedAccountOrWalletClient } from '../managers/contract-manager/createContractsManager'; import type { INetworkConfig } from '../interfaces/NetworkContext'; +import { createChainManagerFactory } from './BaseChainManagerFactory'; // Shared utilities import { NetworkError } from '@lit-protocol/constants'; @@ -1087,6 +1088,54 @@ export function createBaseModule(config: BaseModuleConfig) { }, }, }, + /** + * Returns a wrapped module instance with runtime overrides while keeping the base immutable. + * Currently supports overriding the RPC URL used by consumers of this module. + * + * @param overrides - The overrides to apply to the module. + * @returns A wrapped module instance with the overrides applied. + * @example + * + * import { nagaDev } from '@lit-protocol/networks'; + * const nagaDevWithOverride = nagaDev.withOverrides({ rpcUrl: 'https://custom-rpc-url.com' }); + * const litClient = await createLitClient({ network: nagaDevWithOverride }); + */ + withOverrides: (overrides: { rpcUrl?: string }) => { + const resolvedRpcUrl = overrides.rpcUrl ?? baseModule.getRpcUrl(); + + // Build an overridden network config and a chain manager bound to it + const overriddenChainConfig = { + ...networkConfig.chainConfig, + rpcUrls: { + ...networkConfig.chainConfig.rpcUrls, + default: { + ...networkConfig.chainConfig.rpcUrls.default, + http: [resolvedRpcUrl], + }, + ['public']: { + ...(networkConfig.chainConfig.rpcUrls as any)['public'], + http: [resolvedRpcUrl], + }, + }, + } as typeof networkConfig.chainConfig; + + const overriddenNetworkConfig = { + ...networkConfig, + rpcUrl: resolvedRpcUrl, + chainConfig: overriddenChainConfig, + } as typeof networkConfig; + + const createChainManagerOverridden = (account: ExpectedAccountOrWalletClient) => + createChainManagerFactory(overriddenNetworkConfig, account); + + // Rebuild a fresh module bound to the overridden config + return createBaseModule({ + networkConfig: overriddenNetworkConfig, + moduleName, + createChainManager: createChainManagerOverridden, + verifyReleaseId: baseModule.getVerifyReleaseId(), + }); + }, }; return baseModule;