// // CoreTests.swift // OS Vault // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // import XCTest import BigInt @testable import OSVaultKit final class DerivationTests: XCTestCase { // Standard BIP-39/BIP-44 test vector (public, not a real wallet): // the all-"abandon" mnemonic derives this well-known address at // m/44'/60'/0'/0/0 across every major wallet implementation. let vectorMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" let vectorAddress = "0x9858EfFD232B4033E47d90003D41EC34EcaEda94" func testKnownVectorDerivesKnownAddress() throws { let wallet = try KeyManager.derive(mnemonic: vectorMnemonic) XCTAssertEqual(wallet.address, vectorAddress) XCTAssertEqual(wallet.privateKey.count, 32) } func testGeneratedMnemonicIsValidAndDerives() throws { let manager = KeyManager(vaultURL: tempVaultURL()) let mnemonic = try manager.generateMnemonic() XCTAssertEqual(mnemonic.split(separator: " ").count, 12) XCTAssertTrue(KeyManager.validate(mnemonic: mnemonic)) let wallet = try KeyManager.derive(mnemonic: mnemonic) XCTAssertTrue(wallet.address.hasPrefix("0x")) XCTAssertEqual(wallet.address.count, 42) } func testInvalidMnemonicRejected() { XCTAssertFalse(KeyManager.validate(mnemonic: "not a real phrase at all")) XCTAssertFalse(KeyManager.validate(mnemonic: "abandon abandon abandon")) // Right words, wrong checksum word. XCTAssertFalse(KeyManager.validate(mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon")) } func testNormalizationHandlesCaseAndWhitespace() throws { let messy = " Abandon ABANDON abandon\nabandon abandon abandon abandon abandon abandon abandon abandon about " let wallet = try KeyManager.derive(mnemonic: messy) XCTAssertEqual(wallet.address, vectorAddress) } } final class VaultCryptoTests: XCTestCase { func testSealOpenRoundtrip() throws { let secret = Data("twelve words of nothing sensitive here".utf8) // Low iteration count keeps the test fast; production uses 600k. let vault = try VaultCrypto.seal(secret: secret, password: "correct horse", iterations: 10_000) let opened = try VaultCrypto.open(vault, password: "correct horse") XCTAssertEqual(opened, secret) } func testWrongPasswordFails() throws { let vault = try VaultCrypto.seal(secret: Data("s".utf8), password: "right", iterations: 10_000) XCTAssertThrowsError(try VaultCrypto.open(vault, password: "wrong")) { error in XCTAssertEqual(error as? WalletError, .wrongPassword) } } func testTamperedCiphertextFails() throws { let vault = try VaultCrypto.seal(secret: Data("s".utf8), password: "pw", iterations: 10_000) var combined = Data(base64Encoded: vault.ciphertext)! combined[combined.count - 1] ^= 0xFF let tampered = VaultCrypto.VaultFile( version: vault.version, kdf: vault.kdf, cipher: vault.cipher, ciphertext: combined.base64EncodedString() ) XCTAssertThrowsError(try VaultCrypto.open(tampered, password: "pw")) } func testKeyManagerVaultLifecycle() throws { let url = tempVaultURL() let manager = KeyManager(vaultURL: url) XCTAssertFalse(manager.hasVault) let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" let saved = try manager.saveWallet(mnemonic: mnemonic, password: "test-password-123") XCTAssertTrue(manager.hasVault) let unlocked = try manager.unlock(password: "test-password-123") XCTAssertEqual(unlocked.address, saved.address) XCTAssertEqual(unlocked.mnemonic, mnemonic) XCTAssertThrowsError(try manager.unlock(password: "nope")) { error in XCTAssertEqual(error as? WalletError, .wrongPassword) } // Vault file on disk must never contain the mnemonic in clear. let raw = try String(contentsOf: url, encoding: .utf8) XCTAssertFalse(raw.contains("abandon")) try manager.deleteVault() XCTAssertFalse(manager.hasVault) } } final class TokenAmountTests: XCTestCase { func testUSDCSixDecimals() { XCTAssertEqual(TokenAmount.parse("1", decimals: 6), 1_000_000) XCTAssertEqual(TokenAmount.parse("12.5", decimals: 6), 12_500_000) XCTAssertEqual(TokenAmount.parse("0.000001", decimals: 6), 1) XCTAssertEqual(TokenAmount.parse("1,25", decimals: 6), 1_250_000) XCTAssertNil(TokenAmount.parse("0.0000001", decimals: 6)) // too many digits XCTAssertNil(TokenAmount.parse("", decimals: 6)) XCTAssertNil(TokenAmount.parse("1.2.3", decimals: 6)) XCTAssertNil(TokenAmount.parse("abc", decimals: 6)) XCTAssertNil(TokenAmount.parse("-1", decimals: 6)) } func testDAIEighteenDecimals() { XCTAssertEqual(TokenAmount.parse("1", decimals: 18), BigUInt(10).power(18)) XCTAssertEqual(TokenAmount.format(BigUInt(10).power(18), decimals: 18), "1") XCTAssertEqual(TokenAmount.format(BigUInt(1_500) * BigUInt(10).power(15), decimals: 18), "1.5") } func testFormatting() { XCTAssertEqual(TokenAmount.format(1_000_000, decimals: 6), "1") XCTAssertEqual(TokenAmount.format(12_500_000, decimals: 6), "12.5") XCTAssertEqual(TokenAmount.format(1, decimals: 6), "0.000001") XCTAssertEqual(TokenAmount.format(0, decimals: 6), "0") // Truncation, never rounding up. XCTAssertEqual(TokenAmount.format(1_999_999, decimals: 6, maxFractionDigits: 2), "1.99") } func testRoundtrip() { let cases = ["0.1", "123456.654321", "1", "0.000001"] for c in cases { let parsed = TokenAmount.parse(c, decimals: 6)! XCTAssertEqual(TokenAmount.format(parsed, decimals: 6), c) } } } final class AddressValidationTests: XCTestCase { func testChecksummedAddressValid() { // EIP-55 reference vectors. let valid = [ "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed", "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359", "0x52908400098527886E0F7030069857D2E4169EE7" ] for address in valid { guard case .valid(let checksummed) = AddressValidator.validate(address) else { return XCTFail("\(address) should be valid") } XCTAssertEqual(checksummed, address) } } func testBadChecksumInvalid() { // One flipped case letter breaks the checksum. XCTAssertEqual(AddressValidator.validate("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAeD"), .invalid) } func testLowercaseWarnsButPasses() { let result = AddressValidator.validate("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed") guard case .validNoChecksum(let checksummed) = result else { return XCTFail("lowercase should pass with warning") } XCTAssertEqual(checksummed, "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed") } func testGarbageInvalid() { XCTAssertEqual(AddressValidator.validate(""), .invalid) XCTAssertEqual(AddressValidator.validate("0x123"), .invalid) XCTAssertEqual(AddressValidator.validate("5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed"), .invalid) XCTAssertEqual(AddressValidator.validate("0xZZZeb6053F3E94C9b9A09f33669435E7Ef1BeAed"), .invalid) } } final class CalldataTests: XCTestCase { func testTransferCalldataShape() { let recipient = "0x9858EfFD232B4033E47d90003D41EC34EcaEda94" let calldata = Hex.erc20TransferData(to: recipient, amount: 1_000_000)! XCTAssertEqual(calldata.count, 4 + 32 + 32) XCTAssertEqual(calldata.prefix(4), Data([0xa9, 0x05, 0x9c, 0xbb])) XCTAssertEqual(Hex.string(calldata.suffix(32)), "0x00000000000000000000000000000000000000000000000000000000000f4240") } func testBalanceOfCalldata() { let owner = "0x9858EfFD232B4033E47d90003D41EC34EcaEda94" let calldata = Hex.erc20BalanceOfData(owner: owner)! XCTAssertEqual(calldata.count, 4 + 32) XCTAssertEqual(calldata.prefix(4), Data([0x70, 0xa0, 0x82, 0x31])) } func testQuantityHelpers() { XCTAssertEqual(Hex.quantity(0), "0x0") XCTAssertEqual(Hex.quantity(255), "0xff") XCTAssertEqual(Hex.toBigUInt("0xff"), 255) XCTAssertEqual(Hex.toBigUInt("0x"), 0) XCTAssertNil(Hex.toBigUInt("0xzz")) } } func tempVaultURL() -> URL { FileManager.default.temporaryDirectory .appendingPathComponent("osvault-tests-\(UUID().uuidString)/vault.json") }