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);