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// CoreTests.swift3// OS Vault4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//89import XCTest10import BigInt11@testable import OSVaultKit1213final class DerivationTests: XCTestCase {1415 // Standard BIP-39/BIP-44 test vector (public, not a real wallet):16 // the all-"abandon" mnemonic derives this well-known address at17 // m/44'/60'/0'/0/0 across every major wallet implementation.18 let vectorMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"19 let vectorAddress = "0x9858EfFD232B4033E47d90003D41EC34EcaEda94"2021 func testKnownVectorDerivesKnownAddress() throws {22 let wallet = try KeyManager.derive(mnemonic: vectorMnemonic)23 XCTAssertEqual(wallet.address, vectorAddress)24 XCTAssertEqual(wallet.privateKey.count, 32)25 }2627 func testGeneratedMnemonicIsValidAndDerives() throws {28 let manager = KeyManager(vaultURL: tempVaultURL())29 let mnemonic = try manager.generateMnemonic()30 XCTAssertEqual(mnemonic.split(separator: " ").count, 12)31 XCTAssertTrue(KeyManager.validate(mnemonic: mnemonic))32 let wallet = try KeyManager.derive(mnemonic: mnemonic)33 XCTAssertTrue(wallet.address.hasPrefix("0x"))34 XCTAssertEqual(wallet.address.count, 42)35 }3637 func testInvalidMnemonicRejected() {38 XCTAssertFalse(KeyManager.validate(mnemonic: "not a real phrase at all"))39 XCTAssertFalse(KeyManager.validate(mnemonic: "abandon abandon abandon"))40 // Right words, wrong checksum word.41 XCTAssertFalse(KeyManager.validate(mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon"))42 }4344 func testNormalizationHandlesCaseAndWhitespace() throws {45 let messy = " Abandon ABANDON abandon\nabandon abandon abandon abandon abandon abandon abandon abandon about "46 let wallet = try KeyManager.derive(mnemonic: messy)47 XCTAssertEqual(wallet.address, vectorAddress)48 }49}5051final class VaultCryptoTests: XCTestCase {5253 func testSealOpenRoundtrip() throws {54 let secret = Data("twelve words of nothing sensitive here".utf8)55 // Low iteration count keeps the test fast; production uses 600k.56 let vault = try VaultCrypto.seal(secret: secret, password: "correct horse", iterations: 10_000)57 let opened = try VaultCrypto.open(vault, password: "correct horse")58 XCTAssertEqual(opened, secret)59 }6061 func testWrongPasswordFails() throws {62 let vault = try VaultCrypto.seal(secret: Data("s".utf8), password: "right", iterations: 10_000)63 XCTAssertThrowsError(try VaultCrypto.open(vault, password: "wrong")) { error in64 XCTAssertEqual(error as? WalletError, .wrongPassword)65 }66 }6768 func testTamperedCiphertextFails() throws {69 let vault = try VaultCrypto.seal(secret: Data("s".utf8), password: "pw", iterations: 10_000)70 var combined = Data(base64Encoded: vault.ciphertext)!71 combined[combined.count - 1] ^= 0xFF72 let tampered = VaultCrypto.VaultFile(73 version: vault.version, kdf: vault.kdf,74 cipher: vault.cipher, ciphertext: combined.base64EncodedString()75 )76 XCTAssertThrowsError(try VaultCrypto.open(tampered, password: "pw"))77 }7879 func testKeyManagerVaultLifecycle() throws {80 let url = tempVaultURL()81 let manager = KeyManager(vaultURL: url)82 XCTAssertFalse(manager.hasVault)8384 let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"85 let saved = try manager.saveWallet(mnemonic: mnemonic, password: "test-password-123")86 XCTAssertTrue(manager.hasVault)8788 let unlocked = try manager.unlock(password: "test-password-123")89 XCTAssertEqual(unlocked.address, saved.address)90 XCTAssertEqual(unlocked.mnemonic, mnemonic)9192 XCTAssertThrowsError(try manager.unlock(password: "nope")) { error in93 XCTAssertEqual(error as? WalletError, .wrongPassword)94 }9596 // Vault file on disk must never contain the mnemonic in clear.97 let raw = try String(contentsOf: url, encoding: .utf8)98 XCTAssertFalse(raw.contains("abandon"))99100 try manager.deleteVault()101 XCTAssertFalse(manager.hasVault)102 }103}104105final class TokenAmountTests: XCTestCase {106107 func testUSDCSixDecimals() {108 XCTAssertEqual(TokenAmount.parse("1", decimals: 6), 1_000_000)109 XCTAssertEqual(TokenAmount.parse("12.5", decimals: 6), 12_500_000)110 XCTAssertEqual(TokenAmount.parse("0.000001", decimals: 6), 1)111 XCTAssertEqual(TokenAmount.parse("1,25", decimals: 6), 1_250_000)112 XCTAssertNil(TokenAmount.parse("0.0000001", decimals: 6)) // too many digits113 XCTAssertNil(TokenAmount.parse("", decimals: 6))114 XCTAssertNil(TokenAmount.parse("1.2.3", decimals: 6))115 XCTAssertNil(TokenAmount.parse("abc", decimals: 6))116 XCTAssertNil(TokenAmount.parse("-1", decimals: 6))117 }118119 func testDAIEighteenDecimals() {120 XCTAssertEqual(TokenAmount.parse("1", decimals: 18), BigUInt(10).power(18))121 XCTAssertEqual(TokenAmount.format(BigUInt(10).power(18), decimals: 18), "1")122 XCTAssertEqual(TokenAmount.format(BigUInt(1_500) * BigUInt(10).power(15), decimals: 18), "1.5")123 }124125 func testFormatting() {126 XCTAssertEqual(TokenAmount.format(1_000_000, decimals: 6), "1")127 XCTAssertEqual(TokenAmount.format(12_500_000, decimals: 6), "12.5")128 XCTAssertEqual(TokenAmount.format(1, decimals: 6), "0.000001")129 XCTAssertEqual(TokenAmount.format(0, decimals: 6), "0")130 // Truncation, never rounding up.131 XCTAssertEqual(TokenAmount.format(1_999_999, decimals: 6, maxFractionDigits: 2), "1.99")132 }133134 func testRoundtrip() {135 let cases = ["0.1", "123456.654321", "1", "0.000001"]136 for c in cases {137 let parsed = TokenAmount.parse(c, decimals: 6)!138 XCTAssertEqual(TokenAmount.format(parsed, decimals: 6), c)139 }140 }141}142143final class AddressValidationTests: XCTestCase {144145 func testChecksummedAddressValid() {146 // EIP-55 reference vectors.147 let valid = [148 "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",149 "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359",150 "0x52908400098527886E0F7030069857D2E4169EE7"151 ]152 for address in valid {153 guard case .valid(let checksummed) = AddressValidator.validate(address) else {154 return XCTFail("\(address) should be valid")155 }156 XCTAssertEqual(checksummed, address)157 }158 }159160 func testBadChecksumInvalid() {161 // One flipped case letter breaks the checksum.162 XCTAssertEqual(AddressValidator.validate("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAeD"), .invalid)163 }164165 func testLowercaseWarnsButPasses() {166 let result = AddressValidator.validate("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed")167 guard case .validNoChecksum(let checksummed) = result else {168 return XCTFail("lowercase should pass with warning")169 }170 XCTAssertEqual(checksummed, "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed")171 }172173 func testGarbageInvalid() {174 XCTAssertEqual(AddressValidator.validate(""), .invalid)175 XCTAssertEqual(AddressValidator.validate("0x123"), .invalid)176 XCTAssertEqual(AddressValidator.validate("5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"), .invalid)177 XCTAssertEqual(AddressValidator.validate("0xZZZeb6053F3E94C9b9A09f33669435E7Ef1BeAed"), .invalid)178 }179}180181final class CalldataTests: XCTestCase {182183 func testTransferCalldataShape() {184 let recipient = "0x9858EfFD232B4033E47d90003D41EC34EcaEda94"185 let calldata = Hex.erc20TransferData(to: recipient, amount: 1_000_000)!186 XCTAssertEqual(calldata.count, 4 + 32 + 32)187 XCTAssertEqual(calldata.prefix(4), Data([0xa9, 0x05, 0x9c, 0xbb]))188 XCTAssertEqual(Hex.string(calldata.suffix(32)),189 "0x00000000000000000000000000000000000000000000000000000000000f4240")190 }191192 func testBalanceOfCalldata() {193 let owner = "0x9858EfFD232B4033E47d90003D41EC34EcaEda94"194 let calldata = Hex.erc20BalanceOfData(owner: owner)!195 XCTAssertEqual(calldata.count, 4 + 32)196 XCTAssertEqual(calldata.prefix(4), Data([0x70, 0xa0, 0x82, 0x31]))197 }198199 func testQuantityHelpers() {200 XCTAssertEqual(Hex.quantity(0), "0x0")201 XCTAssertEqual(Hex.quantity(255), "0xff")202 XCTAssertEqual(Hex.toBigUInt("0xff"), 255)203 XCTAssertEqual(Hex.toBigUInt("0x"), 0)204 XCTAssertNil(Hex.toBigUInt("0xzz"))205 }206}207208func tempVaultURL() -> URL {209 FileManager.default.temporaryDirectory210 .appendingPathComponent("osvault-tests-\(UUID().uuidString)/vault.json")211}212