From d5e9984b94a64c175a839ffc7f3976d98e3886de Mon Sep 17 00:00:00 2001 From: furiosa Date: Fri, 20 Feb 2026 13:13:53 -0700 Subject: [PATCH] feat: add decentralized RPC provider and privacy types Extend RpcProviderType enum with DECENTRALIZED and USER values. Add UserRpcEndpoint, UserRpcConfig, and PrivacyConfig interfaces. Extend RpcProviderConfig with privacy and userOverrides fields. Bump to v1.3.0. Phase 1 of en-tfkn (Decentralized RPC Provider Strategy). --- etc/data-models.api.md | 27 ++- package.json | 2 +- src/enums/RpcProviderType.ts | 8 +- src/index.ts | 3 + src/interfaces/PrivacyConfig.ts | 32 +++ src/interfaces/RpcProviderConfig.ts | 10 + src/interfaces/UserRpcConfig.ts | 36 ++++ src/interfaces/UserRpcEndpoint.ts | 37 ++++ tests/unit/rpc-config.test.ts | 321 +++++++++++++++++++++++++++- 9 files changed, 470 insertions(+), 6 deletions(-) create mode 100644 src/interfaces/PrivacyConfig.ts create mode 100644 src/interfaces/UserRpcConfig.ts create mode 100644 src/interfaces/UserRpcEndpoint.ts diff --git a/etc/data-models.api.md b/etc/data-models.api.md index 95d7f1e..e78d5b7 100644 --- a/etc/data-models.api.md +++ b/etc/data-models.api.md @@ -510,6 +510,13 @@ export interface Price { value?: number; } +// @public +export interface PrivacyConfig { + privacyMode: boolean; + queryJitterMs: number; + rotateWithinTier: boolean; +} + // @public export interface RetryConfig { baseDelayMs: number; @@ -534,7 +541,9 @@ export interface RpcProviderConfig { chains: Record; circuitBreaker: CircuitBreakerConfig; healthCheck: HealthCheckConfig; + privacy: PrivacyConfig; retry: RetryConfig; + userOverrides?: UserRpcConfig; } // @public @@ -548,8 +557,10 @@ export enum RpcProviderRole { // @public export enum RpcProviderType { COMMUNITY = "COMMUNITY", + DECENTRALIZED = "DECENTRALIZED", MANAGED = "MANAGED", - PUBLIC = "PUBLIC" + PUBLIC = "PUBLIC", + USER = "USER" } // @public @@ -656,6 +667,20 @@ export enum TransactionType { UNSTAKE = "UNSTAKE" } +// @public +export interface UserRpcConfig { + endpoints: UserRpcEndpoint[]; + mode: 'override' | 'prepend'; +} + +// @public +export interface UserRpcEndpoint { + chainId: string; + label?: string; + url: string; + wsUrl?: string; +} + // @public export interface VaultPosition { apy?: number; diff --git a/package.json b/package.json index 61f95cd..af0fec1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cygnus-wealth/data-models", - "version": "1.2.0", + "version": "1.3.0", "description": "Shared TypeScript data models for CygnusWealth project", "main": "dist/cjs/index.js", "module": "dist/index.js", diff --git a/src/enums/RpcProviderType.ts b/src/enums/RpcProviderType.ts index e22340d..dbf34a4 100644 --- a/src/enums/RpcProviderType.ts +++ b/src/enums/RpcProviderType.ts @@ -28,5 +28,11 @@ export enum RpcProviderType { PUBLIC = 'PUBLIC', /** Community-run node (POKT, Llama Nodes) — variable reliability */ - COMMUNITY = 'COMMUNITY' + COMMUNITY = 'COMMUNITY', + + /** Decentralized RPC network (POKT Gateway, Lava Network) — censorship-resistant */ + DECENTRALIZED = 'DECENTRALIZED', + + /** User-provided custom endpoint — unverified, user-managed */ + USER = 'USER' } diff --git a/src/index.ts b/src/index.ts index bc674ef..0cfa653 100644 --- a/src/index.ts +++ b/src/index.ts @@ -87,6 +87,9 @@ export { CircuitBreakerConfig } from './interfaces/CircuitBreakerConfig'; export { RetryConfig } from './interfaces/RetryConfig'; export { HealthCheckConfig } from './interfaces/HealthCheckConfig'; export { RpcProviderConfig } from './interfaces/RpcProviderConfig'; +export type { UserRpcEndpoint } from './interfaces/UserRpcEndpoint'; +export type { UserRpcConfig } from './interfaces/UserRpcConfig'; +export type { PrivacyConfig } from './interfaces/PrivacyConfig'; // Network Environment export { NetworkEnvironment, EnvironmentConfig } from './types/NetworkEnvironment'; diff --git a/src/interfaces/PrivacyConfig.ts b/src/interfaces/PrivacyConfig.ts new file mode 100644 index 0000000..658790e --- /dev/null +++ b/src/interfaces/PrivacyConfig.ts @@ -0,0 +1,32 @@ +/** + * Privacy-related configuration for RPC request handling. + * + * Controls endpoint rotation and query obfuscation strategies to + * reduce correlation of user activity across RPC providers. + * + * @example + * ```typescript + * import type { PrivacyConfig } from '@cygnus-wealth/data-models'; + * + * const privacy: PrivacyConfig = { + * rotateWithinTier: true, + * privacyMode: true, + * queryJitterMs: 150 + * }; + * ``` + * + * @since 1.3.0 + * @stability extended + * + * @see {@link RpcProviderConfig} for top-level usage + */ +export interface PrivacyConfig { + /** Rotate between endpoints of the same tier/role to avoid fingerprinting */ + rotateWithinTier: boolean; + + /** Enable full privacy mode (distributes queries across providers) */ + privacyMode: boolean; + + /** Random jitter added to query timing to resist timing analysis (in milliseconds) */ + queryJitterMs: number; +} diff --git a/src/interfaces/RpcProviderConfig.ts b/src/interfaces/RpcProviderConfig.ts index bd507d8..9aab943 100644 --- a/src/interfaces/RpcProviderConfig.ts +++ b/src/interfaces/RpcProviderConfig.ts @@ -2,6 +2,8 @@ import { ChainRpcConfig } from './ChainRpcConfig'; import { CircuitBreakerConfig } from './CircuitBreakerConfig'; import { RetryConfig } from './RetryConfig'; import { HealthCheckConfig } from './HealthCheckConfig'; +import { PrivacyConfig } from './PrivacyConfig'; +import { UserRpcConfig } from './UserRpcConfig'; /** * Top-level RPC provider configuration for multi-chain operations. @@ -57,6 +59,8 @@ import { HealthCheckConfig } from './HealthCheckConfig'; * @see {@link CircuitBreakerConfig} for failure isolation policy * @see {@link RetryConfig} for retry strategy * @see {@link HealthCheckConfig} for endpoint monitoring + * @see {@link PrivacyConfig} for privacy and rotation policy + * @see {@link UserRpcConfig} for user-provided endpoint overrides */ export interface RpcProviderConfig { /** Per-chain RPC configurations, keyed by chain ID string (e.g. "1", "137") */ @@ -70,4 +74,10 @@ export interface RpcProviderConfig { /** Health check policy for endpoint monitoring */ healthCheck: HealthCheckConfig; + + /** Privacy and endpoint rotation policy */ + privacy: PrivacyConfig; + + /** Optional user-provided RPC endpoint overrides */ + userOverrides?: UserRpcConfig; } diff --git a/src/interfaces/UserRpcConfig.ts b/src/interfaces/UserRpcConfig.ts new file mode 100644 index 0000000..bf1ed2f --- /dev/null +++ b/src/interfaces/UserRpcConfig.ts @@ -0,0 +1,36 @@ +import { UserRpcEndpoint } from './UserRpcEndpoint'; + +/** + * Configuration for user-provided RPC endpoint overrides. + * + * Determines how user-supplied endpoints interact with the + * platform-managed endpoint list. In 'override' mode, user endpoints + * completely replace managed endpoints for the given chains. In + * 'prepend' mode, user endpoints are tried first before falling back + * to managed endpoints. + * + * @example + * ```typescript + * import type { UserRpcConfig } from '@cygnus-wealth/data-models'; + * + * const userConfig: UserRpcConfig = { + * endpoints: [ + * { chainId: '1', url: 'https://my-eth.example.com/rpc', label: 'My ETH' } + * ], + * mode: 'prepend' + * }; + * ``` + * + * @since 1.3.0 + * @stability extended + * + * @see {@link UserRpcEndpoint} for individual endpoint definition + * @see {@link RpcProviderConfig} for top-level usage + */ +export interface UserRpcConfig { + /** List of user-provided RPC endpoints */ + endpoints: UserRpcEndpoint[]; + + /** How user endpoints interact with managed endpoints: 'override' replaces, 'prepend' adds before */ + mode: 'override' | 'prepend'; +} diff --git a/src/interfaces/UserRpcEndpoint.ts b/src/interfaces/UserRpcEndpoint.ts new file mode 100644 index 0000000..859553a --- /dev/null +++ b/src/interfaces/UserRpcEndpoint.ts @@ -0,0 +1,37 @@ +/** + * A user-provided custom RPC endpoint configuration. + * + * Represents an RPC endpoint supplied by the end user, enabling + * self-sovereign infrastructure choices. These endpoints are not + * managed or verified by the platform. + * + * @example + * ```typescript + * import type { UserRpcEndpoint } from '@cygnus-wealth/data-models'; + * + * const myNode: UserRpcEndpoint = { + * chainId: '1', + * url: 'https://my-private-node.example.com/rpc', + * wsUrl: 'wss://my-private-node.example.com/ws', + * label: 'My Private Ethereum Node' + * }; + * ``` + * + * @since 1.3.0 + * @stability extended + * + * @see {@link UserRpcConfig} for aggregating user endpoints + */ +export interface UserRpcEndpoint { + /** Target chain ID as a string (e.g. "1", "137") */ + chainId: string; + + /** HTTP(S) RPC endpoint URL */ + url: string; + + /** Optional WebSocket endpoint URL for subscriptions */ + wsUrl?: string; + + /** Optional human-readable label for the endpoint */ + label?: string; +} diff --git a/tests/unit/rpc-config.test.ts b/tests/unit/rpc-config.test.ts index d8ae46f..7f77d22 100644 --- a/tests/unit/rpc-config.test.ts +++ b/tests/unit/rpc-config.test.ts @@ -9,6 +9,11 @@ import { HealthCheckConfig, RpcProviderConfig } from '../../src/index'; +import type { + UserRpcEndpoint, + UserRpcConfig, + PrivacyConfig +} from '../../src/index'; /** * Unit tests for RPC Provider Configuration types. @@ -56,6 +61,8 @@ describe('RPC Provider Configuration Types', () => { expect(RpcProviderType.MANAGED).toBe('MANAGED'); expect(RpcProviderType.PUBLIC).toBe('PUBLIC'); expect(RpcProviderType.COMMUNITY).toBe('COMMUNITY'); + expect(RpcProviderType.DECENTRALIZED).toBe('DECENTRALIZED'); + expect(RpcProviderType.USER).toBe('USER'); }); it('should have unique values (no duplicates)', () => { @@ -64,9 +71,9 @@ describe('RPC Provider Configuration Types', () => { expect(values.length).toBe(uniqueValues.size); }); - it('should have exactly 3 types', () => { + it('should have exactly 5 types', () => { const values = Object.values(RpcProviderType); - expect(values).toHaveLength(3); + expect(values).toHaveLength(5); }); it('should distinguish infrastructure ownership models', () => { @@ -336,6 +343,11 @@ describe('RPC Provider Configuration Types', () => { intervalMs: 30000, timeoutMs: 5000, method: 'eth_blockNumber' + }, + privacy: { + rotateWithinTier: false, + privacyMode: false, + queryJitterMs: 0 } }; @@ -373,6 +385,11 @@ describe('RPC Provider Configuration Types', () => { intervalMs: 30000, timeoutMs: 5000, method: 'eth_blockNumber' + }, + privacy: { + rotateWithinTier: false, + privacyMode: false, + queryJitterMs: 0 } }; @@ -418,6 +435,11 @@ describe('RPC Provider Configuration Types', () => { intervalMs: 30000, timeoutMs: 5000, method: 'eth_blockNumber' + }, + privacy: { + rotateWithinTier: false, + privacyMode: false, + queryJitterMs: 0 } }; @@ -432,6 +454,299 @@ describe('RPC Provider Configuration Types', () => { }); }); + describe('UserRpcEndpoint', () => { + it('should accept a fully specified user endpoint', () => { + const endpoint: UserRpcEndpoint = { + chainId: '1', + url: 'https://my-node.example.com/rpc', + wsUrl: 'wss://my-node.example.com/ws', + label: 'My Private Node' + }; + + expect(endpoint.chainId).toBe('1'); + expect(endpoint.url).toBe('https://my-node.example.com/rpc'); + expect(endpoint.wsUrl).toBe('wss://my-node.example.com/ws'); + expect(endpoint.label).toBe('My Private Node'); + }); + + it('should accept an endpoint without optional fields', () => { + const endpoint: UserRpcEndpoint = { + chainId: '137', + url: 'https://polygon-node.example.com/rpc' + }; + + expect(endpoint.chainId).toBe('137'); + expect(endpoint.url).toBe('https://polygon-node.example.com/rpc'); + expect(endpoint.wsUrl).toBeUndefined(); + expect(endpoint.label).toBeUndefined(); + }); + + it('should be JSON serializable', () => { + const endpoint: UserRpcEndpoint = { + chainId: '42161', + url: 'https://arb-node.example.com/rpc', + label: 'Arbitrum Node' + }; + + const json = JSON.stringify(endpoint); + const parsed = JSON.parse(json); + + expect(parsed).toEqual(endpoint); + }); + }); + + describe('UserRpcConfig', () => { + it('should accept override mode config', () => { + const config: UserRpcConfig = { + endpoints: [ + { chainId: '1', url: 'https://my-eth.example.com/rpc' } + ], + mode: 'override' + }; + + expect(config.endpoints).toHaveLength(1); + expect(config.mode).toBe('override'); + }); + + it('should accept prepend mode config', () => { + const config: UserRpcConfig = { + endpoints: [ + { chainId: '1', url: 'https://primary.example.com/rpc', label: 'Primary' }, + { chainId: '1', url: 'https://backup.example.com/rpc', label: 'Backup' } + ], + mode: 'prepend' + }; + + expect(config.endpoints).toHaveLength(2); + expect(config.mode).toBe('prepend'); + }); + + it('should accept multi-chain user endpoints', () => { + const config: UserRpcConfig = { + endpoints: [ + { chainId: '1', url: 'https://eth.example.com/rpc' }, + { chainId: '137', url: 'https://polygon.example.com/rpc' }, + { chainId: '42161', url: 'https://arb.example.com/rpc' } + ], + mode: 'prepend' + }; + + expect(config.endpoints).toHaveLength(3); + const chainIds = config.endpoints.map(e => e.chainId); + expect(chainIds).toEqual(['1', '137', '42161']); + }); + + it('should be JSON serializable', () => { + const config: UserRpcConfig = { + endpoints: [ + { chainId: '1', url: 'https://eth.example.com/rpc', label: 'My ETH' } + ], + mode: 'override' + }; + + const json = JSON.stringify(config); + const parsed = JSON.parse(json); + + expect(parsed).toEqual(config); + }); + }); + + describe('PrivacyConfig', () => { + it('should accept a complete privacy configuration', () => { + const config: PrivacyConfig = { + rotateWithinTier: true, + privacyMode: true, + queryJitterMs: 150 + }; + + expect(config.rotateWithinTier).toBe(true); + expect(config.privacyMode).toBe(true); + expect(config.queryJitterMs).toBe(150); + }); + + it('should accept privacy-disabled configuration', () => { + const config: PrivacyConfig = { + rotateWithinTier: false, + privacyMode: false, + queryJitterMs: 0 + }; + + expect(config.rotateWithinTier).toBe(false); + expect(config.privacyMode).toBe(false); + expect(config.queryJitterMs).toBe(0); + }); + + it('should be JSON serializable', () => { + const config: PrivacyConfig = { + rotateWithinTier: true, + privacyMode: false, + queryJitterMs: 100 + }; + + const json = JSON.stringify(config); + const parsed = JSON.parse(json); + + expect(parsed).toEqual(config); + }); + }); + + describe('RpcProviderConfig (extended with privacy and userOverrides)', () => { + it('should accept config with privacy field', () => { + const config: RpcProviderConfig = { + chains: { + '1': { + chainId: 1, + chainName: 'Ethereum Mainnet', + endpoints: [], + totalOperationTimeoutMs: 30000, + cacheStaleAcceptanceMs: 60000 + } + }, + circuitBreaker: { + failureThreshold: 5, + openDurationMs: 30000, + halfOpenMaxAttempts: 2, + monitorWindowMs: 60000 + }, + retry: { + maxAttempts: 3, + baseDelayMs: 1000, + maxDelayMs: 10000 + }, + healthCheck: { + intervalMs: 30000, + timeoutMs: 5000, + method: 'eth_blockNumber' + }, + privacy: { + rotateWithinTier: true, + privacyMode: true, + queryJitterMs: 150 + } + }; + + expect(config.privacy).toBeDefined(); + expect(config.privacy.rotateWithinTier).toBe(true); + expect(config.privacy.privacyMode).toBe(true); + expect(config.privacy.queryJitterMs).toBe(150); + }); + + it('should accept config with userOverrides field', () => { + const config: RpcProviderConfig = { + chains: { + '1': { + chainId: 1, + chainName: 'Ethereum Mainnet', + endpoints: [], + totalOperationTimeoutMs: 30000, + cacheStaleAcceptanceMs: 60000 + } + }, + circuitBreaker: { + failureThreshold: 5, + openDurationMs: 30000, + halfOpenMaxAttempts: 2, + monitorWindowMs: 60000 + }, + retry: { + maxAttempts: 3, + baseDelayMs: 1000, + maxDelayMs: 10000 + }, + healthCheck: { + intervalMs: 30000, + timeoutMs: 5000, + method: 'eth_blockNumber' + }, + privacy: { + rotateWithinTier: false, + privacyMode: false, + queryJitterMs: 0 + }, + userOverrides: { + endpoints: [ + { chainId: '1', url: 'https://my-eth.example.com/rpc', label: 'My Node' } + ], + mode: 'prepend' + } + }; + + expect(config.userOverrides).toBeDefined(); + expect(config.userOverrides!.endpoints).toHaveLength(1); + expect(config.userOverrides!.mode).toBe('prepend'); + }); + + it('should accept config without optional userOverrides', () => { + const config: RpcProviderConfig = { + chains: {}, + circuitBreaker: { + failureThreshold: 5, + openDurationMs: 30000, + halfOpenMaxAttempts: 2, + monitorWindowMs: 60000 + }, + retry: { + maxAttempts: 3, + baseDelayMs: 1000, + maxDelayMs: 10000 + }, + healthCheck: { + intervalMs: 30000, + timeoutMs: 5000, + method: 'eth_blockNumber' + }, + privacy: { + rotateWithinTier: false, + privacyMode: false, + queryJitterMs: 0 + } + }; + + expect(config.userOverrides).toBeUndefined(); + }); + + it('should serialize extended config to JSON', () => { + const config: RpcProviderConfig = { + chains: {}, + circuitBreaker: { + failureThreshold: 5, + openDurationMs: 30000, + halfOpenMaxAttempts: 2, + monitorWindowMs: 60000 + }, + retry: { + maxAttempts: 3, + baseDelayMs: 1000, + maxDelayMs: 10000 + }, + healthCheck: { + intervalMs: 30000, + timeoutMs: 5000, + method: 'eth_blockNumber' + }, + privacy: { + rotateWithinTier: true, + privacyMode: true, + queryJitterMs: 200 + }, + userOverrides: { + endpoints: [ + { chainId: '1', url: 'https://my-node.example.com/rpc' } + ], + mode: 'override' + } + }; + + const json = JSON.stringify(config); + const parsed = JSON.parse(json); + + expect(parsed.privacy.rotateWithinTier).toBe(true); + expect(parsed.privacy.queryJitterMs).toBe(200); + expect(parsed.userOverrides.mode).toBe('override'); + expect(parsed.userOverrides.endpoints[0].chainId).toBe('1'); + }); + }); + describe('Contract Tests (Breaking Change Detection)', () => { it('should not remove RpcProviderRole values', () => { const coreRoles = ['PRIMARY', 'SECONDARY', 'TERTIARY', 'EMERGENCY']; @@ -442,7 +757,7 @@ describe('RPC Provider Configuration Types', () => { }); it('should not remove RpcProviderType values', () => { - const coreTypes = ['MANAGED', 'PUBLIC', 'COMMUNITY']; + const coreTypes = ['MANAGED', 'PUBLIC', 'COMMUNITY', 'DECENTRALIZED', 'USER']; const values = Object.values(RpcProviderType); coreTypes.forEach(type => { expect(values).toContain(type);