// // Network.swift // OS Vault // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // import Foundation import BigInt /// The EVM chains OS Vault can talk to. Fully data-driven: adding a chain is /// a new case + ChainConfig entry. Every chain id and RPC below was verified /// live before being listed (see docs/RESEARCH-MULTICHAIN.md). Non-EVM chains /// (Bitcoin, Solana, Tron, TON, XRPL…) use their own adapters. public enum Network: String, CaseIterable, Codable, Identifiable, Sendable { case baseSepolia // testnet — safe default for development case baseMainnet case ethereum case arbitrum case optimism case polygon case bnb case avalanche case gnosis case linea case scroll public var id: String { rawValue } /// How transaction fees actually work on this chain — the research /// finding is that "EIP-1559" hides four distinct realities. public enum FeeModel: Sendable { /// Standard: maxFee = 2×baseFee + tip. case eip1559 /// BNB Chain (BEP-226): baseFee is 0; price via eth_gasPrice. case zeroBaseFee /// OP-stack (Base, Optimism): L2 fee + L1 data fee from the /// GasPriceOracle predeploy at 0x42…0F, deducted silently. case opStackL1Fee /// Scroll: same idea, oracle at 0x5300…0002. case scrollL1Fee /// Arbitrum: estimateGas already folds the L1 buffer into gasLimit; /// suggested tip is 0. case arbitrumInclusive /// Linea: base fee pinned at 7 wei; real cost rides in the tip. case lineaPinnedBase } public struct ChainConfig: Sendable { public let chainID: BigUInt public let displayName: String public let nativeSymbol: String public let nativeName: String /// Ordered keyless endpoints: primary first, chain id verified live. public let rpcs: [URL] public let explorerBase: URL public let feeModel: FeeModel public let isTestnet: Bool } public var config: ChainConfig { switch self { case .baseSepolia: return ChainConfig( chainID: 84532, displayName: "Base Sepolia", nativeSymbol: "ETH", nativeName: "Ether", rpcs: [URL(string: "https://sepolia.base.org")!, URL(string: "https://base-sepolia-rpc.publicnode.com")!], explorerBase: URL(string: "https://sepolia.basescan.org")!, feeModel: .opStackL1Fee, isTestnet: true ) case .baseMainnet: return ChainConfig( chainID: 8453, displayName: "Base", nativeSymbol: "ETH", nativeName: "Ether", rpcs: [URL(string: "https://base-rpc.publicnode.com")!, URL(string: "https://mainnet.base.org")!], explorerBase: URL(string: "https://basescan.org")!, feeModel: .opStackL1Fee, isTestnet: false ) case .ethereum: return ChainConfig( chainID: 1, displayName: "Ethereum", nativeSymbol: "ETH", nativeName: "Ether", rpcs: [URL(string: "https://ethereum-rpc.publicnode.com")!, URL(string: "https://cloudflare-eth.com")!, URL(string: "https://eth.merkle.io")!], explorerBase: URL(string: "https://etherscan.io")!, feeModel: .eip1559, isTestnet: false ) case .arbitrum: return ChainConfig( chainID: 42161, displayName: "Arbitrum One", nativeSymbol: "ETH", nativeName: "Ether", rpcs: [URL(string: "https://arbitrum-one-rpc.publicnode.com")!, URL(string: "https://arb1.arbitrum.io/rpc")!], explorerBase: URL(string: "https://arbiscan.io")!, feeModel: .arbitrumInclusive, isTestnet: false ) case .optimism: return ChainConfig( chainID: 10, displayName: "OP Mainnet", nativeSymbol: "ETH", nativeName: "Ether", rpcs: [URL(string: "https://optimism-rpc.publicnode.com")!, URL(string: "https://mainnet.optimism.io")!], explorerBase: URL(string: "https://optimistic.etherscan.io")!, feeModel: .opStackL1Fee, isTestnet: false ) case .polygon: return ChainConfig( chainID: 137, displayName: "Polygon", nativeSymbol: "POL", nativeName: "Polygon Ecosystem Token", rpcs: [URL(string: "https://polygon-bor-rpc.publicnode.com")!, URL(string: "https://polygon.drpc.org")!], explorerBase: URL(string: "https://polygonscan.com")!, feeModel: .eip1559, isTestnet: false ) case .bnb: return ChainConfig( chainID: 56, displayName: "BNB Chain", nativeSymbol: "BNB", nativeName: "BNB", rpcs: [URL(string: "https://bsc-rpc.publicnode.com")!, URL(string: "https://bsc-dataseed.bnbchain.org")!], explorerBase: URL(string: "https://bscscan.com")!, feeModel: .zeroBaseFee, isTestnet: false ) case .avalanche: return ChainConfig( chainID: 43114, displayName: "Avalanche C-Chain", nativeSymbol: "AVAX", nativeName: "Avalanche", rpcs: [URL(string: "https://avalanche-c-chain-rpc.publicnode.com")!, URL(string: "https://api.avax.network/ext/bc/C/rpc")!], explorerBase: URL(string: "https://snowtrace.io")!, feeModel: .eip1559, isTestnet: false ) case .gnosis: return ChainConfig( chainID: 100, displayName: "Gnosis", nativeSymbol: "xDAI", nativeName: "xDAI (dollar-pegged)", rpcs: [URL(string: "https://gnosis-rpc.publicnode.com")!, URL(string: "https://rpc.gnosischain.com")!], explorerBase: URL(string: "https://gnosisscan.io")!, feeModel: .eip1559, isTestnet: false ) case .linea: return ChainConfig( chainID: 59144, displayName: "Linea", nativeSymbol: "ETH", nativeName: "Ether", rpcs: [URL(string: "https://linea-rpc.publicnode.com")!, URL(string: "https://rpc.linea.build")!], explorerBase: URL(string: "https://lineascan.build")!, feeModel: .lineaPinnedBase, isTestnet: false ) case .scroll: return ChainConfig( chainID: 534352, displayName: "Scroll", nativeSymbol: "ETH", nativeName: "Ether", rpcs: [URL(string: "https://scroll-rpc.publicnode.com")!, URL(string: "https://rpc.scroll.io")!], explorerBase: URL(string: "https://scrollscan.com")!, feeModel: .scrollL1Fee, isTestnet: false ) } } /// L1 data-fee oracle for rollups that charge one (`getL1Fee(bytes)`). public var l1FeeOracle: String? { switch config.feeModel { case .opStackL1Fee: return "0x420000000000000000000000000000000000000F" case .scrollL1Fee: return "0x5300000000000000000000000000000000000002" default: return nil } } /// Active RPC endpoints, user override (Settings) first. public var rpcURLs: [URL] { var urls = config.rpcs if let raw = UserDefaults.standard.string(forKey: rpcOverrideKey), !raw.isEmpty, let url = URL(string: raw), url.scheme?.hasPrefix("http") == true { urls.insert(url, at: 0) } return urls } public var rpcOverrideKey: String { "osvault.rpcOverride.\(rawValue)" } public func explorerTxURL(_ hash: String) -> URL { config.explorerBase.appendingPathComponent("tx/\(hash)") } public func explorerAddressURL(_ address: String) -> URL { config.explorerBase.appendingPathComponent("address/\(address)") } }