SPB Git

spb/os-vault Public

Self-custody, multi-chain crypto wallet for macOS. One recovery phrase, six chain families, zero API keys — nothing leaves your Mac.

Swift 96% Shell 3.4% Makefile 0.6%
8.1 KB · 196 lines swift
Raw Blame History
1//2//  Network.swift3//  OS Vault4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//89import Foundation10import BigInt1112/// The EVM chains OS Vault can talk to. Fully data-driven: adding a chain is13/// a new case + ChainConfig entry. Every chain id and RPC below was verified14/// live before being listed (see docs/RESEARCH-MULTICHAIN.md). Non-EVM chains15/// (Bitcoin, Solana, Tron, TON, XRPL…) use their own adapters.16public enum Network: String, CaseIterable, Codable, Identifiable, Sendable {17    case baseSepolia        // testnet — safe default for development18    case baseMainnet19    case ethereum20    case arbitrum21    case optimism22    case polygon23    case bnb24    case avalanche25    case gnosis26    case linea27    case scroll2829    public var id: String { rawValue }3031    /// How transaction fees actually work on this chain — the research32    /// finding is that "EIP-1559" hides four distinct realities.33    public enum FeeModel: Sendable {34        /// Standard: maxFee = 2×baseFee + tip.35        case eip155936        /// BNB Chain (BEP-226): baseFee is 0; price via eth_gasPrice.37        case zeroBaseFee38        /// OP-stack (Base, Optimism): L2 fee + L1 data fee from the39        /// GasPriceOracle predeploy at 0x42…0F, deducted silently.40        case opStackL1Fee41        /// Scroll: same idea, oracle at 0x5300…0002.42        case scrollL1Fee43        /// Arbitrum: estimateGas already folds the L1 buffer into gasLimit;44        /// suggested tip is 0.45        case arbitrumInclusive46        /// Linea: base fee pinned at 7 wei; real cost rides in the tip.47        case lineaPinnedBase48    }4950    public struct ChainConfig: Sendable {51        public let chainID: BigUInt52        public let displayName: String53        public let nativeSymbol: String54        public let nativeName: String55        /// Ordered keyless endpoints: primary first, chain id verified live.56        public let rpcs: [URL]57        public let explorerBase: URL58        public let feeModel: FeeModel59        public let isTestnet: Bool60    }6162    public var config: ChainConfig {63        switch self {64        case .baseSepolia:65            return ChainConfig(66                chainID: 84532, displayName: "Base Sepolia",67                nativeSymbol: "ETH", nativeName: "Ether",68                rpcs: [URL(string: "https://sepolia.base.org")!,69                       URL(string: "https://base-sepolia-rpc.publicnode.com")!],70                explorerBase: URL(string: "https://sepolia.basescan.org")!,71                feeModel: .opStackL1Fee, isTestnet: true72            )73        case .baseMainnet:74            return ChainConfig(75                chainID: 8453, displayName: "Base",76                nativeSymbol: "ETH", nativeName: "Ether",77                rpcs: [URL(string: "https://base-rpc.publicnode.com")!,78                       URL(string: "https://mainnet.base.org")!],79                explorerBase: URL(string: "https://basescan.org")!,80                feeModel: .opStackL1Fee, isTestnet: false81            )82        case .ethereum:83            return ChainConfig(84                chainID: 1, displayName: "Ethereum",85                nativeSymbol: "ETH", nativeName: "Ether",86                rpcs: [URL(string: "https://ethereum-rpc.publicnode.com")!,87                       URL(string: "https://cloudflare-eth.com")!,88                       URL(string: "https://eth.merkle.io")!],89                explorerBase: URL(string: "https://etherscan.io")!,90                feeModel: .eip1559, isTestnet: false91            )92        case .arbitrum:93            return ChainConfig(94                chainID: 42161, displayName: "Arbitrum One",95                nativeSymbol: "ETH", nativeName: "Ether",96                rpcs: [URL(string: "https://arbitrum-one-rpc.publicnode.com")!,97                       URL(string: "https://arb1.arbitrum.io/rpc")!],98                explorerBase: URL(string: "https://arbiscan.io")!,99                feeModel: .arbitrumInclusive, isTestnet: false100            )101        case .optimism:102            return ChainConfig(103                chainID: 10, displayName: "OP Mainnet",104                nativeSymbol: "ETH", nativeName: "Ether",105                rpcs: [URL(string: "https://optimism-rpc.publicnode.com")!,106                       URL(string: "https://mainnet.optimism.io")!],107                explorerBase: URL(string: "https://optimistic.etherscan.io")!,108                feeModel: .opStackL1Fee, isTestnet: false109            )110        case .polygon:111            return ChainConfig(112                chainID: 137, displayName: "Polygon",113                nativeSymbol: "POL", nativeName: "Polygon Ecosystem Token",114                rpcs: [URL(string: "https://polygon-bor-rpc.publicnode.com")!,115                       URL(string: "https://polygon.drpc.org")!],116                explorerBase: URL(string: "https://polygonscan.com")!,117                feeModel: .eip1559, isTestnet: false118            )119        case .bnb:120            return ChainConfig(121                chainID: 56, displayName: "BNB Chain",122                nativeSymbol: "BNB", nativeName: "BNB",123                rpcs: [URL(string: "https://bsc-rpc.publicnode.com")!,124                       URL(string: "https://bsc-dataseed.bnbchain.org")!],125                explorerBase: URL(string: "https://bscscan.com")!,126                feeModel: .zeroBaseFee, isTestnet: false127            )128        case .avalanche:129            return ChainConfig(130                chainID: 43114, displayName: "Avalanche C-Chain",131                nativeSymbol: "AVAX", nativeName: "Avalanche",132                rpcs: [URL(string: "https://avalanche-c-chain-rpc.publicnode.com")!,133                       URL(string: "https://api.avax.network/ext/bc/C/rpc")!],134                explorerBase: URL(string: "https://snowtrace.io")!,135                feeModel: .eip1559, isTestnet: false136            )137        case .gnosis:138            return ChainConfig(139                chainID: 100, displayName: "Gnosis",140                nativeSymbol: "xDAI", nativeName: "xDAI (dollar-pegged)",141                rpcs: [URL(string: "https://gnosis-rpc.publicnode.com")!,142                       URL(string: "https://rpc.gnosischain.com")!],143                explorerBase: URL(string: "https://gnosisscan.io")!,144                feeModel: .eip1559, isTestnet: false145            )146        case .linea:147            return ChainConfig(148                chainID: 59144, displayName: "Linea",149                nativeSymbol: "ETH", nativeName: "Ether",150                rpcs: [URL(string: "https://linea-rpc.publicnode.com")!,151                       URL(string: "https://rpc.linea.build")!],152                explorerBase: URL(string: "https://lineascan.build")!,153                feeModel: .lineaPinnedBase, isTestnet: false154            )155        case .scroll:156            return ChainConfig(157                chainID: 534352, displayName: "Scroll",158                nativeSymbol: "ETH", nativeName: "Ether",159                rpcs: [URL(string: "https://scroll-rpc.publicnode.com")!,160                       URL(string: "https://rpc.scroll.io")!],161                explorerBase: URL(string: "https://scrollscan.com")!,162                feeModel: .scrollL1Fee, isTestnet: false163            )164        }165    }166167    /// L1 data-fee oracle for rollups that charge one (`getL1Fee(bytes)`).168    public var l1FeeOracle: String? {169        switch config.feeModel {170        case .opStackL1Fee: return "0x420000000000000000000000000000000000000F"171        case .scrollL1Fee: return "0x5300000000000000000000000000000000000002"172        default: return nil173        }174    }175176    /// Active RPC endpoints, user override (Settings) first.177    public var rpcURLs: [URL] {178        var urls = config.rpcs179        if let raw = UserDefaults.standard.string(forKey: rpcOverrideKey),180           !raw.isEmpty, let url = URL(string: raw), url.scheme?.hasPrefix("http") == true {181            urls.insert(url, at: 0)182        }183        return urls184    }185186    public var rpcOverrideKey: String { "osvault.rpcOverride.\(rawValue)" }187188    public func explorerTxURL(_ hash: String) -> URL {189        config.explorerBase.appendingPathComponent("tx/\(hash)")190    }191192    public func explorerAddressURL(_ address: String) -> URL {193        config.explorerBase.appendingPathComponent("address/\(address)")194    }195}196