diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index 56a0837ef..40e0a9baa 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -471,6 +471,7 @@ AAFB3C932D31C3DC000CCCE9 /* BitcoinKitTransactionFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFB3C922D31C3CD000CCCE9 /* BitcoinKitTransactionFactoryProtocol.swift */; }; AAFB3C952D31C58B000CCCE9 /* BitcoinKitTransactionFactoryProtocolMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFB3C942D31C587000CCCE9 /* BitcoinKitTransactionFactoryProtocolMock.swift */; }; AAFB3C972D31CB72000CCCE9 /* UnspentTransaction+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFB3C962D31CB6B000CCCE9 /* UnspentTransaction+Equatable.swift */; }; + AAFB3C992D357E1D000CCCE9 /* BtcWalletServiceIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFB3C982D357E16000CCCE9 /* BtcWalletServiceIntegrationTests.swift */; }; E90055F520EBF5DA00D0CB2D /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055F420EBF5DA00D0CB2D /* AboutViewController.swift */; }; E90055F720EC200900D0CB2D /* SecurityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055F620EC200900D0CB2D /* SecurityViewController.swift */; }; E90055F920ECD86800D0CB2D /* SecurityViewController+StayIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055F820ECD86800D0CB2D /* SecurityViewController+StayIn.swift */; }; @@ -1104,6 +1105,7 @@ AAFB3C922D31C3CD000CCCE9 /* BitcoinKitTransactionFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitcoinKitTransactionFactoryProtocol.swift; sourceTree = ""; }; AAFB3C942D31C587000CCCE9 /* BitcoinKitTransactionFactoryProtocolMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitcoinKitTransactionFactoryProtocolMock.swift; sourceTree = ""; }; AAFB3C962D31CB6B000CCCE9 /* UnspentTransaction+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UnspentTransaction+Equatable.swift"; sourceTree = ""; }; + AAFB3C982D357E16000CCCE9 /* BtcWalletServiceIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcWalletServiceIntegrationTests.swift; sourceTree = ""; }; AD258997F050B24C0051CC8D /* Pods-Adamant.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Adamant.release.xcconfig"; path = "Target Support Files/Pods-Adamant/Pods-Adamant.release.xcconfig"; sourceTree = ""; }; ADDFD2FA17E41CCBD11A1733 /* Pods-Adamant.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Adamant.debug.xcconfig"; path = "Target Support Files/Pods-Adamant/Pods-Adamant.debug.xcconfig"; sourceTree = ""; }; E90055F420EBF5DA00D0CB2D /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; @@ -2245,6 +2247,7 @@ AA33BEB02D303C470083E59C /* Wallets */ = { isa = PBXGroup; children = ( + AAFB3C982D357E16000CCCE9 /* BtcWalletServiceIntegrationTests.swift */, AA33BEB12D303C5F0083E59C /* BtcWalletServiceTests.swift */, ); path = Wallets; @@ -3856,6 +3859,7 @@ buildActionMask = 2147483647; files = ( AAFB3C972D31CB72000CCCE9 /* UnspentTransaction+Equatable.swift in Sources */, + AAFB3C992D357E1D000CCCE9 /* BtcWalletServiceIntegrationTests.swift in Sources */, AAFB3C8F2D31C119000CCCE9 /* WalletServiceError+Equatable.swift in Sources */, AA33BEB92D3044760083E59C /* BtcApiServiceProtocolMock.swift in Sources */, AA33BEB22D303C730083E59C /* BtcWalletServiceTests.swift in Sources */, diff --git a/AdamantTests/Modules/Wallets/BtcWalletServiceIntegrationTests.swift b/AdamantTests/Modules/Wallets/BtcWalletServiceIntegrationTests.swift new file mode 100644 index 000000000..f633d03f7 --- /dev/null +++ b/AdamantTests/Modules/Wallets/BtcWalletServiceIntegrationTests.swift @@ -0,0 +1,119 @@ +// +// BtcWalletServiceIntegrationTests.swift +// Adamant +// +// Created by Christian Benua on 13.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import XCTest +@testable import Adamant +import Swinject +import BitcoinKit +import CommonKit + +final class BtcWalletServiceIntegrationTests: XCTestCase { + + private var apiCoreMock: APICoreProtocolMock! + private var btcApiServiceProtocolMock: BtcApiServiceProtocolMock! + private var sut: BtcWalletService! + + override func setUp() { + super.setUp() + apiCoreMock = APICoreProtocolMock() + btcApiServiceProtocolMock = BtcApiServiceProtocolMock() + btcApiServiceProtocolMock.api = BtcApiCore(apiCore: apiCoreMock) + + sut = BtcWalletService() + sut.addressConverter = AddressConverterFactory().make(network: .mainnetBTC) + sut.btcApiService = btcApiServiceProtocolMock + sut.btcTransactionFactory = BitcoinKitTransactionFactory() + } + + override func tearDown() { + apiCoreMock = nil + btcApiServiceProtocolMock = nil + sut = nil + super.tearDown() + } + + func test_createAndSendTransaction_createsValidTxIdAndHash() async throws { + // given + sut.setWalletForTests(try makeWallet()) + let data = Constants.unspentTranscationsData + await apiCoreMock.isolated { mock in + mock.stubbedSendRequestBasicGenericResult = APIResponseModel(result: .success(data), data: data, code: 200) + } + + // when 1 + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: "1K4hFg49PaEt5pHCym7yb5B446Vb3roSMp", + amount: 0.00009, + fee: 0.00002159542, + comment: nil + ) + }) + + // then 1 + let transaction = try XCTUnwrap(result.value) + XCTAssertEqual(transaction.serialized().hex, Constants.expectedTransactionHex) + XCTAssertEqual(transaction.txID, Constants.expectedTransactionID) + + // given 2 + let txData = try XCTUnwrap(transaction.txID.data(using: .utf8)) + await apiCoreMock.isolated { mock in + mock.stubbedSendRequestBasicGenericResult = APIResponseModel(result: .success(txData), data: txData, code: 200) + } + + // when 2 + let result2 = await Result { + try await self.sut.sendTransaction(transaction) + } + // then 3 + XCTAssertNil(result2.error) + await apiCoreMock.isolated { mock in + XCTAssertEqual(mock.invokedSendRequestBasicGenericCount, 2) + } + } + + private func makeWallet() throws -> BtcWallet { + let privateKeyData = Constants.passphrase + .data(using: .utf8)! + .sha256() + let privateKey = PrivateKey( + data: privateKeyData, + network: .mainnetBTC, + isPublicKeyCompressed: true + ) + return try BtcWallet( + unicId: "BTCBTC", + privateKey: privateKey, + addressConverter: AddressConverterFactory().make(network: .mainnetBTC) + ) + } +} + +private enum Constants { + + static let passphrase = "village lunch say patrol glow first hurt shiver name method dolphin dead" + + static let expectedTransactionID = "e9e99b0d38e3b3fc362a3a9a2809807af179cbaaff59ae7f9ddb3ed30a4f9582" + + static let expectedTransactionHex = "0100000001a0d73e3bd0aa2025d91eabd8512d5e19ad80752892f415480f75b97966b06f0e010000006a47304402200f8908e3a4b1c3ab181fa875c15dc8816ec29298a74e78122000d4e08bced3a2022016767a16bb9ea315a9ea9a8536d39bd3e6e8dce3594cf5b17c4576f7bfc39140012102cd3dcbdfc1b77e54b3a8f273310806ab56b0c2463c2f1677c7694a89a713e0d0ffffffff0228230000000000001976a914c6251d0e16c0e1946b745b69caa3a7c36014381088ac38560200000000001976a91457f6f900ac7a7e3ccab712326cd7b85638fc15a888ac00000000" + + static let unspentTranscationsData = unspentTranscationsRawJSON.data(using: .utf8)! + + static let unspentTranscationsRawJSON: String = """ +[{ + "txid":"0e6fb06679b9750f4815f492287580ad195e2d51d8ab1ed92520aad03b3ed7a0", + "vout":1, + "status":{ + "confirmed":true, + "block_height":879091, + "block_hash":"00000000000000000001e0da09b0792ff69dcd98af264b1750cbf9ef2deab73d", + "block_time":1736786953}, + "value":164303 +}] +""" +} diff --git a/AdamantTests/Modules/Wallets/BtcWalletServiceTests.swift b/AdamantTests/Modules/Wallets/BtcWalletServiceTests.swift index 2a5ad098d..79589333c 100644 --- a/AdamantTests/Modules/Wallets/BtcWalletServiceTests.swift +++ b/AdamantTests/Modules/Wallets/BtcWalletServiceTests.swift @@ -189,6 +189,41 @@ final class BtcWalletServiceTests: XCTestCase { ) } + func test_sendTransaction_failIfTxIdCorrupted() async throws { + // given + let txData = try XCTUnwrap(Constants.anotherTransactionId.data(using: .utf8)) + await apiCoreMock.isolated { mock in + mock.stubbedSendRequestBasicGenericResult = APIResponseModel(result: .success(txData), data: txData, code: 200) + } + + // when + let result = await Result { + try await self.sut.sendTransaction(BitcoinKit.Transaction.deserialize(Data(hex: Constants.transactionHex)!)) + } + + // then + XCTAssertEqual( + result.error as? WalletServiceError, + WalletServiceError.remoteServiceError(message: Constants.anotherTransactionId) + ) + } + + func test_sendTransaction_successIfTxIdMatches() async throws { + // given + let txData = try XCTUnwrap(Constants.transactionId.data(using: .utf8)) + await apiCoreMock.isolated { mock in + mock.stubbedSendRequestBasicGenericResult = APIResponseModel(result: .success(txData), data: txData, code: 200) + } + + // when + let result = await Result { + try await self.sut.sendTransaction(BitcoinKit.Transaction.deserialize(Data(hex: Constants.transactionHex)!)) + } + + // then + XCTAssertNil(result.error) + } + private func makeWallet(address: String = Constants.btcAddress) throws -> BtcWallet { let privateKeyData = "my long passphrase" .data(using: .utf8)! @@ -230,6 +265,12 @@ private enum Constants { static let lockingScript2 = Data([118, 169, 20, 189, 45, 218, 220, 109, 190, 133, 34, 44, 61, 83, 31, 41, 204, 37, 209, 62, 168, 11, 45, 136, 172]) + static let transactionId = "8b2654793f94539e5c66b87dee6d0908fb9728eb25c90396e25286c6d4b8a371" + + static let anotherTransactionId = String("8b2654793f94539e5c66b87dee6d0908fb9728eb25c90396e25286c6d4b8a371".reversed()) + + static let transactionHex = "0100000001a0d73e3bd0aa2025d91eabd8512d5e19ad80752892f415480f75b97966b06f0e010000006a473044022072c8ecd3143e663520807c496dba3dc8010478f3cae09fcb65995be29737a55702206d23617cad2f88a3bd28757be956c731dbde06615fb9bb9fabf2d55e6a8f67ba0121037ec9f6126013088b3d1e8f844f3e755144756a4e9a7da6b0094c189f55031934ffffffff0228230000000000001976a914c6251d0e16c0e1946b745b69caa3a7c36014381088ac38560200000000001976a914931ef5cbdad28723ba9596de5da1145ae969a71888ac00000000" + static let expectedTransaction = BitcoinKit.Transaction( version: 1, inputs: [