Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 8 additions & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ RPC_BSC=https://bsc-dataseed1.binance.org:8545
RPC_ARBITRUM=https://arb1.arbitrum.io/rpc
RPC_OPTIMISM=https://mainnet.optimism.io/

# Dynamic Chain Configuration - Development
# Include testnet chains in development
# INCLUDE_TESTNETS=true

# Override individual chain RPC endpoints
# CHAIN_ETHEREUM_RPC_URL=https://localhost:8545
# CHAIN_POLYGON_RPC_URL=https://polygon-rpc.com/

# Logging Configuration
LOG_LEVEL=debug
LOG_FORMAT=simple
Expand Down
22 changes: 22 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,28 @@ RPC_BSC=https://bsc-dataseed.binance.org
RPC_ARBITRUM=https://arb1.arbitrum.io/rpc
RPC_OPTIMISM=https://mainnet.optimism.io

# ========== Dynamic Chain Configuration ==========
# Override RPC endpoints via environment variables
# Format: CHAIN_<CHAIN_ID>_<PROPERTY>=value
# Examples:
# CHAIN_ETHEREUM_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY
# CHAIN_POLYGON_RPC_URL=https://polygon-mainnet.g.alchemy.com/v2/YOUR_KEY
# CHAIN_ARBITRUM_RPC_URL=https://arb-mainnet.g.alchemy.com/v2/YOUR_KEY

# Add new chains dynamically
# CHAIN_AVALANCHE_NAME=Avalanche
# CHAIN_AVALANCHE_SYMBOL=AVAX
# CHAIN_AVALANCHE_CHAIN_ID=43114
# CHAIN_AVALANCHE_RPC_URL=https://api.avax.network/ext/bc/C/rpc
# CHAIN_AVALANCHE_EXPLORER_URL=https://snowtrace.io
# CHAIN_AVALANCHE_TYPE=EVM
# CHAIN_AVALANCHE_SUPPORTS_BRIDGING=true
# CHAIN_AVALANCHE_SUPPORTS_QUOTES=true
# CHAIN_AVALANCHE_NATIVE_DECIMALS=18

# Enable/disable testnets (true to include testnet chains)
# INCLUDE_TESTNETS=false

# ========== Logging Configuration ==========
LOG_LEVEL=debug
LOG_FORMAT=simple
Expand Down
10 changes: 10 additions & 0 deletions .env.production
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ RPC_BSC=https://bsc-mainnet.public.blastapi.io
RPC_ARBITRUM=https://arbitrum-mainnet.public.blastapi.io
RPC_OPTIMISM=https://optimism-mainnet.public.blastapi.io

# Dynamic Chain Configuration - Production
# Only mainnet chains, no testnets
# INCLUDE_TESTNETS=false

# Override individual chain endpoints with production-grade endpoints
# CHAIN_ETHEREUM_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_PROD_KEY
# CHAIN_POLYGON_RPC_URL=https://polygon-mainnet.g.alchemy.com/v2/YOUR_PROD_KEY
# CHAIN_ARBITRUM_RPC_URL=https://arb-mainnet.g.alchemy.com/v2/YOUR_PROD_KEY
# CHAIN_BASE_RPC_URL=https://base-mainnet.g.alchemy.com/v2/YOUR_PROD_KEY

# Logging Configuration - Production level
LOG_LEVEL=warn
LOG_FORMAT=json
Expand Down
8 changes: 8 additions & 0 deletions .env.staging
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ RPC_BSC=https://bsc-dataseed1.binance.org:8545
RPC_ARBITRUM=https://arb1.arbitrum.io/rpc
RPC_OPTIMISM=https://mainnet.optimism.io/

# Dynamic Chain Configuration - Staging
# Mix of mainnet and testnet chains for validation
# INCLUDE_TESTNETS=false

# Override individual chain endpoints with private/more reliable ones
# CHAIN_ETHEREUM_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY
# CHAIN_POLYGON_RPC_URL=https://polygon-mainnet.g.alchemy.com/v2/YOUR_KEY

# Logging Configuration
LOG_LEVEL=info
LOG_FORMAT=json
Expand Down
199 changes: 199 additions & 0 deletions apps/api/src/config/chain-config-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/**
* Chain Configuration Schema and Types
* Defines the structure and validation for blockchain chain configurations
*/

export interface ChainFeatures {
supportsBridging: boolean;
supportsQuotes: boolean;
nativeCurrencyDecimals: number;
}

export interface ChainConfig {
id: string;
name: string;
symbol: string;
chainId: number;
rpcUrl: string;
explorerUrl: string;
type: 'EVM' | 'Stellar' | 'Cosmos' | 'Solana';
isTestnet?: boolean;
features: ChainFeatures;
// Optional metadata
logoUrl?: string;
color?: string;
nativeCurrency?: {
name: string;
symbol: string;
decimals: number;
};
}

export interface ChainsConfiguration {
chains: ChainConfig[];
version: string;
lastUpdated?: string;
}

export interface ChainConfigOptions {
// Configuration source
fromJson?: boolean;
fromEnv?: boolean;

// Override options
overrideRpcUrls?: Record<string, string>;
overrideExplorerUrls?: Record<string, string>;

// Filter options
includeTestnets?: boolean;
enabledChainIds?: string[];

// Validation options
validateRpcUrls?: boolean;
skipValidation?: boolean;
}

/**
* Default chain configuration template for new chains
*/
export const DEFAULT_CHAIN_TEMPLATE: Partial<ChainConfig> = {
features: {
supportsBridging: true,
supportsQuotes: true,
nativeCurrencyDecimals: 18,
},
type: 'EVM',
isTestnet: false,
};

/**
* Validation rules for chain configuration
*/
export const CHAIN_CONFIG_VALIDATION_RULES = {
requiredFields: ['id', 'name', 'symbol', 'chainId', 'rpcUrl', 'explorerUrl', 'type', 'features'],
idPattern: /^[a-z0-9-]+$/,
minChainId: 0,
maxChainId: 2147483647, // Max 32-bit integer
validTypes: ['EVM', 'Stellar', 'Cosmos', 'Solana'],
minNativeDecimals: 0,
maxNativeDecimals: 36,
};

/**
* Chain type constants
*/
export const CHAIN_TYPES = {
EVM: 'EVM' as const,
STELLAR: 'Stellar' as const,
COSMOS: 'Cosmos' as const,
SOLANA: 'Solana' as const,
};

/**
* Common EVM chain IDs
*/
export const COMMON_EVM_CHAIN_IDS = {
ETHEREUM: 1,
GOERLI: 5,
POLYGON: 137,
MUMBAI: 80001,
BSC: 56,
BSC_TESTNET: 97,
ARBITRUM: 42161,
ARBITRUM_SEPOLIA: 421614,
OPTIMISM: 10,
OPTIMISM_SEPOLIA: 11155420,
BASE: 8453,
BASE_SEPOLIA: 84532,
AVAX: 43114,
AVAX_FUJI: 43113,
FANTOM: 250,
FANTOM_TESTNET: 4002,
GNOSIS: 100,
CELO: 42220,
CELO_ALFAJORES: 44787,
MOONBEAM: 1284,
};

/**
* Validate a chain configuration object
*/
export function validateChainConfig(chain: any): { valid: boolean; errors: string[] } {
const errors: string[] = [];

// Check required fields
for (const field of CHAIN_CONFIG_VALIDATION_RULES.requiredFields) {
if (!(field in chain)) {
errors.push(`Missing required field: ${field}`);
}
}

// Validate ID format
if (chain.id && !CHAIN_CONFIG_VALIDATION_RULES.idPattern.test(chain.id)) {
errors.push(
`Invalid chain ID format. Must match pattern ${CHAIN_CONFIG_VALIDATION_RULES.idPattern.source}`,
);
}

// Validate numeric chain ID
if (chain.chainId !== undefined) {
const chainId = Number(chain.chainId);
if (!Number.isInteger(chainId)) {
errors.push('chainId must be an integer');
} else if (chainId < CHAIN_CONFIG_VALIDATION_RULES.minChainId) {
errors.push(`chainId must be >= ${CHAIN_CONFIG_VALIDATION_RULES.minChainId}`);
} else if (chainId > CHAIN_CONFIG_VALIDATION_RULES.maxChainId) {
errors.push(`chainId must be <= ${CHAIN_CONFIG_VALIDATION_RULES.maxChainId}`);
}
}

// Validate type
if (chain.type && !CHAIN_CONFIG_VALIDATION_RULES.validTypes.includes(chain.type)) {
errors.push(
`Invalid type. Must be one of: ${CHAIN_CONFIG_VALIDATION_RULES.validTypes.join(', ')}`,
);
}

// Validate URLs
if (chain.rpcUrl) {
try {
new URL(chain.rpcUrl);
} catch {
errors.push('Invalid rpcUrl format');
}
}

if (chain.explorerUrl) {
try {
new URL(chain.explorerUrl);
} catch {
errors.push('Invalid explorerUrl format');
}
}

// Validate features
if (chain.features) {
const decimals = chain.features.nativeCurrencyDecimals;
if (
decimals !== undefined &&
(decimals < CHAIN_CONFIG_VALIDATION_RULES.minNativeDecimals ||
decimals > CHAIN_CONFIG_VALIDATION_RULES.maxNativeDecimals)
) {
errors.push(
`nativeCurrencyDecimals must be between ${CHAIN_CONFIG_VALIDATION_RULES.minNativeDecimals} and ${CHAIN_CONFIG_VALIDATION_RULES.maxNativeDecimals}`,
);
}
}

return {
valid: errors.length === 0,
errors,
};
}

/**
* Type guard to check if object is a valid ChainConfig
*/
export function isValidChainConfig(obj: any): obj is ChainConfig {
return validateChainConfig(obj).valid;
}
Loading
Loading