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%
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