diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index efa1f8548..7b4bab26f 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -466,6 +466,11 @@ AA33BEB72D3041A30083E59C /* AddressConverterMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA33BEB42D303CBD0083E59C /* AddressConverterMock.swift */; }; AA33BEB92D3044760083E59C /* BtcApiServiceProtocolMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA33BEB82D30446F0083E59C /* BtcApiServiceProtocolMock.swift */; }; AAB01CAD2D3AE44B007D6BF4 /* BitcoinKitTransactionFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB01CAC2D3AE449007D6BF4 /* BitcoinKitTransactionFactory.swift */; }; + AAB01CAF2D3AECED007D6BF4 /* DogeWalletServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB01CAE2D3AECE6007D6BF4 /* DogeWalletServiceTests.swift */; }; + AAB01CB12D3AF01B007D6BF4 /* DogeApiServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB01CB02D3AF015007D6BF4 /* DogeApiServiceProtocol.swift */; }; + AAB01CB32D3AF0B4007D6BF4 /* DogeApiServiceProtocolMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB01CB22D3AF0AE007D6BF4 /* DogeApiServiceProtocolMock.swift */; }; + AAB01CB52D3AF27E007D6BF4 /* DogeInternalApiProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB01CB42D3AF278007D6BF4 /* DogeInternalApiProtocol.swift */; }; + AAC641332D3ED1BB00619DFE /* DogeWalletServiceIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC641322D3ED1B200619DFE /* DogeWalletServiceIntegrationTests.swift */; }; AAFB3C8B2D31C0DD000CCCE9 /* Actor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFB3C8A2D31C0D0000CCCE9 /* Actor+Extensions.swift */; }; AAFB3C8D2D31C0EE000CCCE9 /* Result+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFB3C8C2D31C0EA000CCCE9 /* Result+Extensions.swift */; }; AAFB3C8F2D31C119000CCCE9 /* WalletServiceError+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAFB3C8E2D31C110000CCCE9 /* WalletServiceError+Equatable.swift */; }; @@ -1111,6 +1116,11 @@ AA33BEB52D303DB60083E59C /* APICoreProtocolMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APICoreProtocolMock.swift; sourceTree = ""; }; AA33BEB82D30446F0083E59C /* BtcApiServiceProtocolMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcApiServiceProtocolMock.swift; sourceTree = ""; }; AAB01CAC2D3AE449007D6BF4 /* BitcoinKitTransactionFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitcoinKitTransactionFactory.swift; sourceTree = ""; }; + AAB01CAE2D3AECE6007D6BF4 /* DogeWalletServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeWalletServiceTests.swift; sourceTree = ""; }; + AAB01CB02D3AF015007D6BF4 /* DogeApiServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeApiServiceProtocol.swift; sourceTree = ""; }; + AAB01CB22D3AF0AE007D6BF4 /* DogeApiServiceProtocolMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeApiServiceProtocolMock.swift; sourceTree = ""; }; + AAB01CB42D3AF278007D6BF4 /* DogeInternalApiProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeInternalApiProtocol.swift; sourceTree = ""; }; + AAC641322D3ED1B200619DFE /* DogeWalletServiceIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeWalletServiceIntegrationTests.swift; sourceTree = ""; }; AAFB3C8A2D31C0D0000CCCE9 /* Actor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Actor+Extensions.swift"; sourceTree = ""; }; AAFB3C8C2D31C0EA000CCCE9 /* Result+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Extensions.swift"; sourceTree = ""; }; AAFB3C8E2D31C110000CCCE9 /* WalletServiceError+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WalletServiceError+Equatable.swift"; sourceTree = ""; }; @@ -1759,6 +1769,8 @@ 64E1C82E222E95F6006C4DA7 /* DogeWallet.swift */, 64E1C830222E9617006C4DA7 /* DogeWalletService.swift */, 93CCAE782B06D81D00EA5B94 /* DogeApiService.swift */, + AAB01CB02D3AF015007D6BF4 /* DogeApiServiceProtocol.swift */, + AAB01CB42D3AF278007D6BF4 /* DogeInternalApiProtocol.swift */, 4186B337294200E8006594A3 /* DogeWalletService+DynamicConstants.swift */, 648DD7A32237DB9E00B811FD /* DogeWalletService+Send.swift */, 648DD7A72239147800B811FD /* DogeWalletService+RichMessageProvider.swift */, @@ -2280,6 +2292,8 @@ AA33BEB02D303C470083E59C /* Wallets */ = { isa = PBXGroup; children = ( + AAC641322D3ED1B200619DFE /* DogeWalletServiceIntegrationTests.swift */, + AAB01CAE2D3AECE6007D6BF4 /* DogeWalletServiceTests.swift */, AAFB3C9A2D383BA9000CCCE9 /* EthWalletServiceTests.swift */, AAFB3C982D357E16000CCCE9 /* BtcWalletServiceIntegrationTests.swift */, AA33BEB12D303C5F0083E59C /* BtcWalletServiceTests.swift */, @@ -2298,6 +2312,7 @@ AAFB3C9C2D383F73000CCCE9 /* EthApiServiceProtocolMock.swift */, AAFB3C942D31C587000CCCE9 /* BitcoinKitTransactionFactoryProtocolMock.swift */, AA33BEB82D30446F0083E59C /* BtcApiServiceProtocolMock.swift */, + AAB01CB22D3AF0AE007D6BF4 /* DogeApiServiceProtocolMock.swift */, AA33BEB52D303DB60083E59C /* APICoreProtocolMock.swift */, AA33BEB42D303CBD0083E59C /* AddressConverterMock.swift */, ); @@ -3435,6 +3450,7 @@ 6403F5E222723F7500D58779 /* DashWallet.swift in Sources */, 26A975FF2B7E843E0095C367 /* SelectTextView.swift in Sources */, 93294B822AAD0BB400911109 /* BtcWalletFactory.swift in Sources */, + AAB01CB12D3AF01B007D6BF4 /* DogeApiServiceProtocol.swift in Sources */, 648DD7A42237DB9E00B811FD /* DogeWalletService+Send.swift in Sources */, 93294B7D2AAD067000911109 /* AppContainer.swift in Sources */, 93ED214C2CC3561800AA1FC8 /* TransactionsStatusServiceComposeProtocol.swift in Sources */, @@ -3820,6 +3836,7 @@ 64BD2B7720E2820300E2CD36 /* TransactionDetails.swift in Sources */, 3A9015A52A614A18002A2464 /* EmojiService.swift in Sources */, 9322E875297042F000B8357C /* ChatSender.swift in Sources */, + AAB01CB52D3AF27E007D6BF4 /* DogeInternalApiProtocol.swift in Sources */, 93ED214F2CC3567600AA1FC8 /* TransactionsStatusServiceCompose.swift in Sources */, E96E86B821679C120061F80A /* EthTransactionDetailsViewController.swift in Sources */, 3A26D9432C3C2E19003AD832 /* KlyWalletService+StatusCheck.swift in Sources */, @@ -3912,9 +3929,12 @@ AA33BEB92D3044760083E59C /* BtcApiServiceProtocolMock.swift in Sources */, AAFB3CA52D3854BB000CCCE9 /* MockURLProtocol.swift in Sources */, AAFB3C9B2D383BB1000CCCE9 /* EthWalletServiceTests.swift in Sources */, + AAC641332D3ED1BB00619DFE /* DogeWalletServiceIntegrationTests.swift in Sources */, AA33BEB22D303C730083E59C /* BtcWalletServiceTests.swift in Sources */, + AAB01CAF2D3AECED007D6BF4 /* DogeWalletServiceTests.swift in Sources */, AA33BEB72D3041A30083E59C /* AddressConverterMock.swift in Sources */, AAFB3C8D2D31C0EE000CCCE9 /* Result+Extensions.swift in Sources */, + AAB01CB32D3AF0B4007D6BF4 /* DogeApiServiceProtocolMock.swift in Sources */, AA33BEB62D303E240083E59C /* APICoreProtocolMock.swift in Sources */, AAFB3C912D31C14A000CCCE9 /* BitcoinKitTransaction+Equatable.swift in Sources */, AAFB3C8B2D31C0DD000CCCE9 /* Actor+Extensions.swift in Sources */, diff --git a/Adamant/Modules/Wallets/Doge/DogeApiService.swift b/Adamant/Modules/Wallets/Doge/DogeApiService.swift index 0ad51e300..4002bd869 100644 --- a/Adamant/Modules/Wallets/Doge/DogeApiService.swift +++ b/Adamant/Modules/Wallets/Doge/DogeApiService.swift @@ -45,19 +45,23 @@ final class DogeApiCore: BlockchainHealthCheckableService, Sendable { } } -final class DogeApiService: ApiServiceProtocol { - let api: BlockchainHealthCheckWrapper +final class DogeApiService: DogeApiServiceProtocol { + var api: DogeInternalApiProtocol { + _api + } + + let _api: BlockchainHealthCheckWrapper @MainActor - var nodesInfoPublisher: AnyObservable { api.nodesInfoPublisher } + var nodesInfoPublisher: AnyObservable { _api.nodesInfoPublisher } @MainActor - var nodesInfo: NodesListInfo { api.nodesInfo } + var nodesInfo: NodesListInfo { _api.nodesInfo } - func healthCheck() { api.healthCheck() } + func healthCheck() { _api.healthCheck() } init(api: BlockchainHealthCheckWrapper) { - self.api = api + self._api = api } func request( @@ -75,3 +79,5 @@ final class DogeApiService: ApiServiceProtocol { } } } + +extension BlockchainHealthCheckWrapper: DogeInternalApiProtocol where Service == DogeApiCore {} diff --git a/Adamant/Modules/Wallets/Doge/DogeApiServiceProtocol.swift b/Adamant/Modules/Wallets/Doge/DogeApiServiceProtocol.swift new file mode 100644 index 000000000..23b15c752 --- /dev/null +++ b/Adamant/Modules/Wallets/Doge/DogeApiServiceProtocol.swift @@ -0,0 +1,22 @@ +// +// DogeApiServiceProtocol.swift +// Adamant +// +// Created by Christian Benua on 17.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import CommonKit +import Foundation + +protocol DogeApiServiceProtocol: ApiServiceProtocol { + + var api: DogeInternalApiProtocol { get } + + func request( + waitsForConnectivity: Bool, + _ request: @Sendable @escaping (APICoreProtocol, NodeOrigin) async -> ApiServiceResult + ) async -> WalletServiceResult + + func getStatusInfo() async -> WalletServiceResult +} diff --git a/Adamant/Modules/Wallets/Doge/DogeInternalApiProtocol.swift b/Adamant/Modules/Wallets/Doge/DogeInternalApiProtocol.swift new file mode 100644 index 000000000..3e7096f9f --- /dev/null +++ b/Adamant/Modules/Wallets/Doge/DogeInternalApiProtocol.swift @@ -0,0 +1,16 @@ +// +// DogeInternalApiProtocol.swift +// Adamant +// +// Created by Christian Benua on 17.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import CommonKit + +protocol DogeInternalApiProtocol { + func request( + waitsForConnectivity: Bool, + _ requestAction: @Sendable (DogeApiCore, NodeOrigin) async -> WalletServiceResult + ) async -> WalletServiceResult +} diff --git a/Adamant/Modules/Wallets/Doge/DogeWallet.swift b/Adamant/Modules/Wallets/Doge/DogeWallet.swift index d83597d9d..f53549bfd 100644 --- a/Adamant/Modules/Wallets/Doge/DogeWallet.swift +++ b/Adamant/Modules/Wallets/Doge/DogeWallet.swift @@ -49,4 +49,18 @@ final class DogeWallet: WalletAccount, @unchecked Sendable { self.publicKey = privateKey.publicKey() self.addressEntity = try addressConverter.convert(publicKey: publicKey, type: .p2pkh) } + +#if DEBUG + @available(*, deprecated, message: "For testing purposes only") + init( + unicId: String, + privateKey: PrivateKey, + addressEntity: Address + ) { + self.unicId = unicId + self.privateKey = privateKey + self.publicKey = privateKey.publicKey() + self.addressEntity = addressEntity + } +#endif } diff --git a/Adamant/Modules/Wallets/Doge/DogeWalletService+Send.swift b/Adamant/Modules/Wallets/Doge/DogeWalletService+Send.swift index d81fa10c4..79ed58608 100644 --- a/Adamant/Modules/Wallets/Doge/DogeWalletService+Send.swift +++ b/Adamant/Modules/Wallets/Doge/DogeWalletService+Send.swift @@ -52,12 +52,13 @@ extension DogeWalletService: WalletServiceTwoStepSend { } // Create local transaction - let transaction = BitcoinKit.Transaction.createNewTransaction( + let transaction = btcTransactionFactory.createTransaction( toAddress: toAddress, amount: rawAmount, fee: fee, changeAddress: wallet.addressEntity, utxos: utxos, + lockTime: 0, keys: [key] ) return transaction diff --git a/Adamant/Modules/Wallets/Doge/DogeWalletService.swift b/Adamant/Modules/Wallets/Doge/DogeWalletService.swift index 40fd559cd..359550e87 100644 --- a/Adamant/Modules/Wallets/Doge/DogeWalletService.swift +++ b/Adamant/Modules/Wallets/Doge/DogeWalletService.swift @@ -55,7 +55,8 @@ final class DogeWalletService: WalletCoreProtocol, @unchecked Sendable { // MARK: - Dependencies var apiService: AdamantApiServiceProtocol! - var dogeApiService: DogeApiService! + var dogeApiService: DogeApiServiceProtocol! + var btcTransactionFactory: BitcoinKitTransactionFactoryProtocol! var accountService: AccountService! var dialogService: DialogService! var addressConverter: AddressConverter! @@ -390,6 +391,7 @@ extension DogeWalletService: SwinjectDependentService { accountService = container.resolve(AccountService.self) apiService = container.resolve(AdamantApiServiceProtocol.self) dialogService = container.resolve(DialogService.self) + btcTransactionFactory = container.resolve(BitcoinKitTransactionFactoryProtocol.self) addressConverter = container.resolve(AddressConverterFactory.self)? .make(network: network) dogeApiService = container.resolve(DogeApiService.self) @@ -689,6 +691,15 @@ extension DogeWalletService { } } +#if DEBUG +extension DogeWalletService { + @available(*, deprecated, message: "For testing purposes only") + func setWalletForTests(_ wallet: DogeWallet?) { + self.dogeWallet = wallet + } +} +#endif + // MARK: - PrivateKey generator extension DogeWalletService: PrivateKeyGenerator { var rowTitle: String { diff --git a/AdamantTests/Modules/Wallets/DogeWalletServiceIntegrationTests.swift b/AdamantTests/Modules/Wallets/DogeWalletServiceIntegrationTests.swift new file mode 100644 index 000000000..867ae71ee --- /dev/null +++ b/AdamantTests/Modules/Wallets/DogeWalletServiceIntegrationTests.swift @@ -0,0 +1,135 @@ +// +// DogeWalletServiceIntegrationTests.swift +// Adamant +// +// Created by Christian Benua on 20.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import XCTest +@testable import Adamant +import Swinject +import BitcoinKit +import CommonKit + +final class DogeWalletServiceIntegrationTests: XCTestCase { + + private var apiCoreMock: APICoreProtocolMock! + private var dogeApiServiceProtocolMock: DogeApiServiceProtocolMock! + private var sut: DogeWalletService! + + override func setUp() { + super.setUp() + apiCoreMock = APICoreProtocolMock() + dogeApiServiceProtocolMock = DogeApiServiceProtocolMock() + dogeApiServiceProtocolMock._api = DogeApiCore(apiCore: apiCoreMock) + + sut = DogeWalletService() + sut.addressConverter = AddressConverterFactory().make(network: DogeMainnet()) + sut.dogeApiService = dogeApiServiceProtocolMock + sut.btcTransactionFactory = BitcoinKitTransactionFactory() + } + + override func tearDown() { + apiCoreMock = nil + dogeApiServiceProtocolMock = 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: Constants.recipient, + amount: 9, + fee: 1, + 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 extension DogeWalletServiceIntegrationTests { + func makeWallet() throws -> DogeWallet { + let privateKeyData = Constants.passphrase + .data(using: .utf8)! + .sha256() + let privateKey = PrivateKey( + data: privateKeyData, + network: DogeMainnet(), + isPublicKeyCompressed: true + ) + return try DogeWallet( + unicId: Constants.tokenId, + privateKey: privateKey, + addressConverter: AddressConverterFactory().make(network: DogeMainnet()) + ) + } +} + +private enum Constants { + + static let passphrase = "village lunch say patrol glow first hurt shiver name method dolphin dead" + + static let recipient = "DPCnnvzngz9AcpToiM7Y8qLewEDtP7jN8T" + + static let tokenId = "DOGEDOGE" + + static let expectedTransactionID = "4f9700bca38cce8f442ba0ebf6b2c1b95d235854cabb797fb1178499a5403c7a" + + static let expectedTransactionHex = "0100000001010000006b483045022100c54ae687dfaa6e910eaf2d40ec755cc11eb1263de38cbe4a5b48b1a13c6d113c022043cce15981221cef35fbcfe0a35ad9a9a218257acb854d1dc0f6e0ebbe892c2d012102cd3dcbdfc1b77e54b3a8f273310806ab56b0c2463c2f1677c7694a89a713e0d0ffffffff0200e9a435000000001976a914c6251d0e16c0e1946b745b69caa3a7c36014381088ac00362634f28623001976a91457f6f900ac7a7e3ccab712326cd7b85638fc15a888ac00000000" + + static let unspentTranscationsData = unspentTranscationsRawJSON.data(using: .utf8)! + + static let unspentTranscationsRawJSON: String = """ +[ + { + "txid": "1", + "vout": 1, + "amount": 100000000, + "confirmations": 1 + }, + { + "txid": "1", + "vout": 2, + "amount": 100000000, + "confirmations": 1 + }, + { + "txid": "1", + "vout": 3, + "amount": 200000000, + "confirmations": 0 + } +] +""" +} diff --git a/AdamantTests/Modules/Wallets/DogeWalletServiceTests.swift b/AdamantTests/Modules/Wallets/DogeWalletServiceTests.swift new file mode 100644 index 000000000..db5bbf3fc --- /dev/null +++ b/AdamantTests/Modules/Wallets/DogeWalletServiceTests.swift @@ -0,0 +1,311 @@ +// +// DogeWalletServiceTests.swift +// Adamant +// +// Created by Christian Benua on 17.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import XCTest +@testable import Adamant +import BitcoinKit +import CommonKit + +final class DogeWalletServiceTests: XCTestCase { + private var addressConverterMock: AddressConverterMock! + private var apiCoreMock: APICoreProtocolMock! + private var dogeApiServiceProtocolMock: DogeApiServiceProtocolMock! + private var transactionFactoryMock: BitcoinKitTransactionFactoryProtocolMock! + private var sut: DogeWalletService! + + override func setUp() { + super.setUp() + addressConverterMock = AddressConverterMock() + apiCoreMock = APICoreProtocolMock() + dogeApiServiceProtocolMock = DogeApiServiceProtocolMock() + dogeApiServiceProtocolMock._api = DogeApiCore(apiCore: apiCoreMock) + + sut = DogeWalletService() + sut.addressConverter = addressConverterMock + sut.dogeApiService = dogeApiServiceProtocolMock + transactionFactoryMock = BitcoinKitTransactionFactoryProtocolMock() + transactionFactoryMock.stubbedTransactionFactory = { + BitcoinKit.Transaction.createNewTransaction( + toAddress: $0, + amount: $1, + fee: $2, + changeAddress: $3, + utxos: $4, + lockTime: $5, + keys: $6 + ) + } + sut.btcTransactionFactory = transactionFactoryMock + } + + override func tearDown() { + addressConverterMock = nil + apiCoreMock = nil + dogeApiServiceProtocolMock = nil + transactionFactoryMock = nil + sut = nil + super.tearDown() + } + + func test_createTransaction_noWalletThrowsError() async throws { + // given + sut.setWalletForTests(nil) + + // when + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: "recipient", + amount: 10, + fee: 0.1, + comment: nil + ) + }) + + // then + XCTAssertEqual(result.error as? WalletServiceError, .notLogged) + } + + func test_createTransaction_accountNotFoundThrowsError() async throws { + // given + sut.setWalletForTests(try makeWallet()) + addressConverterMock.stubbedInvokedConvertAddressResult = .failure(NSError()) + + // when + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: "recipient", + amount: 10, + fee: 1, + comment: nil + ) + }) + + // then + XCTAssertEqual(result.error as? WalletServiceError, .accountNotFound) + } + + func test_createTransaction_notEnoughMoneyThrowsError() 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 + ) + } + addressConverterMock.stubbedInvokedConvertAddressResult = .success(try makeDefaultAddress()) + + // when + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: "recipient", + amount: 30, + fee: 1, + comment: nil) + }) + + // then + XCTAssertEqual(result.error as? WalletServiceError, .notEnoughMoney) + } + + func test_createTransaction_badUnspentTransactionResponseDataThrowsError() async throws { + // given + sut.setWalletForTests(try makeWallet()) + let data = Constants.unspentTranscationsCorruptedData + await apiCoreMock.isolated { mock in + mock.stubbedSendRequestBasicGenericResult = APIResponseModel(result: .success(data), data: data, code: 200) + } + addressConverterMock.stubbedInvokedConvertAddressResult = .success(try makeDefaultAddress()) + + // when + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: "recipient", + amount: 30 / DogeWalletService.multiplier, + fee: 1 / DogeWalletService.multiplier, + comment: nil) + }) + + // then + switch result.error as? WalletServiceError { + case .remoteServiceError?: + break + default: + XCTFail("Expected `remoteServiceError`, but got \(String(describing: result.error))") + } + } + + func test_createTransaction_enoughMoneyReturnsRealTransaction() async throws { + // given + sut.setWalletForTests(try makeWallet(address: Constants.anotherDogeAddress)) + let data = Constants.unspentTranscationsData + await apiCoreMock.isolated { mock in + mock.stubbedSendRequestBasicGenericResult = APIResponseModel(result: .success(data), data: data, code: 200) + } + let expectedToAddress = try makeDefaultAddress() + addressConverterMock.stubbedInvokedConvertAddressResult = .success(expectedToAddress) + + // when + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: "recipient", + amount: 10 / DogeWalletService.multiplier, + fee: 1 / DogeWalletService.multiplier, + comment: nil + ) + }) + + // then + XCTAssertNil(result.error) + XCTAssertEqual(result.value, Constants.expectedTransaction) + XCTAssertEqual( + transactionFactoryMock.invokedCreateTransactionParameters?.amount, + 10 + ) + XCTAssertEqual( + transactionFactoryMock.invokedCreateTransactionParameters?.fee, + 1 + ) + XCTAssertEqual( + transactionFactoryMock.invokedCreateTransactionParameters?.utxos, + Constants.expectedUnspentTransactions + ) + assertAddressesEqual( + try XCTUnwrap(transactionFactoryMock.invokedCreateTransactionParameters?.toAddress), + expectedToAddress + ) + assertAddressesEqual( + try XCTUnwrap(transactionFactoryMock.invokedCreateTransactionParameters?.changeAddress), + try XCTUnwrap(sut.dogeWallet?.addressEntity) + ) + } + + 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) + } +} + +// MARK: Private + +private extension DogeWalletServiceTests { + func makeWallet(address: String = Constants.dogeAddress) throws -> DogeWallet { + let privateKeyData = "my long passphrase" + .data(using: .utf8)! + .sha256() + let privateKey = PrivateKey( + data: privateKeyData, + network: .testnet, + isPublicKeyCompressed: true + ) + + return try DogeWallet( + unicId: "unicId", + privateKey: privateKey, + addressEntity: makeDefaultAddress(address: address) + ) + } + + func makeDefaultAddress(address: String = Constants.dogeAddress) throws -> Address { + try AddressConverterFactory() + .make(network: DogeMainnet()) + .convert(address: address) + } + + func assertAddressesEqual(_ lhs: Address, _ rhs: Address, file: StaticString = #file, line: UInt = #line) { + XCTAssertEqual(lhs.lockingScript, rhs.lockingScript, file: file, line: line) + XCTAssertEqual(lhs.stringValue, rhs.stringValue, file: file, line: line) + XCTAssertEqual(lhs.lockingScriptPayload, rhs.lockingScriptPayload, file: file, line: line) + XCTAssertEqual(lhs.scriptType, rhs.scriptType, file: file, line: line) + } +} + +private enum Constants { + static let dogeAddress = "DJYzxc6Rd3tknUmED7KB83ZoXV9NzhZxss" + + static let anotherDogeAddress = "DPCnnvzngz9AcpToiM7Y8qLewEDtP7jN8T" + + static let transactionHex = "0100000001a0d73e3bd0aa2025d91eabd8512d5e19ad80752892f415480f75b97966b06f0e010000006a473044022072c8ecd3143e663520807c496dba3dc8010478f3cae09fcb65995be29737a55702206d23617cad2f88a3bd28757be956c731dbde06615fb9bb9fabf2d55e6a8f67ba0121037ec9f6126013088b3d1e8f844f3e755144756a4e9a7da6b0094c189f55031934ffffffff0228230000000000001976a914c6251d0e16c0e1946b745b69caa3a7c36014381088ac38560200000000001976a914931ef5cbdad28723ba9596de5da1145ae969a71888ac00000000" + + static let transactionId = "8b2654793f94539e5c66b87dee6d0908fb9728eb25c90396e25286c6d4b8a371" + + static let anotherTransactionId = String("8b2654793f94539e5c66b87dee6d0908fb9728eb25c90396e25286c6d4b8a371".reversed()) + + static let lockingScript = Data([118, 169, 20, 147, 30, 245, 203, 218, 210, 135, 35, 186, 149, 150, 222, 93, 161, 20, 90, 233, 105, 167, 24, 136, 172]) + + static let lockingScript2 = Data([118, 169, 20, 198, 37, 29, 14, 22, 192, 225, 148, 107, 116, 91, 105, 202, 163, 167, 195, 96, 20, 56, 16, 136, 172]) + + static let expectedTransaction = BitcoinKit.Transaction( + version: 1, + inputs: [ + TransactionInput(previousOutput: TransactionOutPoint(hash: Data(), index: 1), signatureScript: Data(), sequence: 4294967295) + ], + outputs: [ + TransactionOutput( + value: 10, + lockingScript: Constants.lockingScript + ), + TransactionOutput( + value: 999999989, + lockingScript: Constants.lockingScript2 + ) + ], + lockTime: 0 + ) + + static let expectedUnspentTransactions = [ + UnspentTransaction( + output: TransactionOutput(value: ((10 * DogeWalletService.multiplier) as NSDecimalNumber).uint64Value, lockingScript: Constants.lockingScript2), + outpoint: TransactionOutPoint(hash: Data(), index: 1) + ), + UnspentTransaction( + output: TransactionOutput(value: ((20 * DogeWalletService.multiplier) as NSDecimalNumber).uint64Value, lockingScript: Constants.lockingScript2), + outpoint: TransactionOutPoint(hash: Data(), index: 2) + ) + ] + + static let unspentTranscationsData = unspentTranscationsRawJSON.data(using: .utf8)! + + static let unspentTranscationsCorruptedData = Data(unspentTranscationsData.shuffled()) + + static let unspentTranscationsRawJSON: String = """ +[ + { + "txid": "1", + "vout": 1, + "amount": 10, + "confirmations": 1 + }, + { + "txid": "1", + "vout": 2, + "amount": 20, + "confirmations": 1 + }, + { + "txid": "1", + "vout": 3, + "amount": 30, + "confirmations": 0 + } +] +""" +} diff --git a/AdamantTests/Stubs/DogeApiServiceProtocolMock.swift b/AdamantTests/Stubs/DogeApiServiceProtocolMock.swift new file mode 100644 index 000000000..8f176f10e --- /dev/null +++ b/AdamantTests/Stubs/DogeApiServiceProtocolMock.swift @@ -0,0 +1,52 @@ +// +// DogeApiServiceProtocolMock.swift +// Adamant +// +// Created by Christian Benua on 17.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +@testable import Adamant +import CommonKit +import Foundation + +final class DogeApiServiceProtocolMock: DogeApiServiceProtocol, DogeInternalApiProtocol { + + var api: DogeInternalApiProtocol { + self + } + + var _api: DogeApiCore! + + func request( + waitsForConnectivity: Bool, + _ request: @Sendable @escaping (APICoreProtocol, NodeOrigin) async -> ApiServiceResult + ) async -> WalletServiceResult { + await _api.request(origin: NodeOrigin(url: URL(string: "http://samplenodeorigin.com")!)) { core, origin in + await request(core, origin) + } + } + + func request( + waitsForConnectivity: Bool, + _ requestAction: @Sendable (DogeApiCore, NodeOrigin) async -> WalletServiceResult + ) async -> WalletServiceResult { + await requestAction(_api, NodeOrigin(url: URL(string: "http://samplenodeorigin.com")!)) + } + + func getStatusInfo() async -> WalletServiceResult { + return .failure(.networkError) + } + + var nodesInfo: NodesListInfo { + fatalError("\(#file).\(#function) is not implemented") + } + + var nodesInfoPublisher: AnyObservable { + fatalError("\(#file).\(#function) is not implemented") + } + + func healthCheck() { + fatalError("\(#file).\(#function) is not implemented") + } +}