diff --git a/AlphaWallet/ActiveWalletCoordinator.swift b/AlphaWallet/ActiveWalletCoordinator.swift index 13ef9ffb8..22a20c4c4 100644 --- a/AlphaWallet/ActiveWalletCoordinator.swift +++ b/AlphaWallet/ActiveWalletCoordinator.swift @@ -564,11 +564,13 @@ class ActiveWalletCoordinator: NSObject, Coordinator, DappRequestHandlerDelegate } func addImported(contract: AlphaWallet.Address, forServer server: RPCServer) { - importToken.importToken(for: contract, server: server, onlyIfThereIsABalance: false) - .done { _ in } - .catch { error in + importToken + .importTokenPublisher(for: contract, server: server, onlyIfThereIsABalance: false) + .sink(receiveCompletion: { result in + guard case .failure(let error) = result else { return } debugLog("Error while adding imported token contract: \(contract.eip55String) server: \(server) wallet: \(self.wallet.address.eip55String) error: \(error)") - } + }, receiveValue: { _ in }) + .store(in: &cancelable) } func show(error: Error) { diff --git a/AlphaWallet/AppCoordinator.swift b/AlphaWallet/AppCoordinator.swift index cf12a94cf..85a579eef 100644 --- a/AlphaWallet/AppCoordinator.swift +++ b/AlphaWallet/AppCoordinator.swift @@ -604,6 +604,7 @@ class AppCoordinator: NSObject, Coordinator { let dependency = WalletDependencies( activitiesPipeLine: activitiesPipeLine, transactionsDataStore: transactionsDataStore, + tokensDataStore: tokensDataStore, importToken: importToken, tokensService: tokensService, pipeline: pipeline, @@ -711,16 +712,18 @@ extension AppCoordinator: UniversalLinkServiceDelegate { tokenScriptOverridesFileManager.importTokenScriptOverrides(url: url) case .eip681(let url): let paymentFlowResolver = Eip681UrlResolver(config: config, importToken: resolver.importToken, missingRPCServerStrategy: .fallbackToAnyMatching) - firstly { - paymentFlowResolver.resolve(url: url) - }.done { result in - switch result { - case .address: - break //Add handling address, maybe same action when scan qr code - case .transaction(let transactionType, let token): - resolver.showPaymentFlow(for: .send(type: .transaction(transactionType)), server: token.server, navigationController: resolver.presentationNavigationController) - } - }.cauterize() + paymentFlowResolver.resolve(url: url) + .sink(receiveCompletion: { result in + guard case .failure(let error) = result else { return } + verboseLog("[Eip681UrlResolver] failure to resolve value from: \(url) with error: \(error)") + }, receiveValue: { result in + switch result { + case .address: + break //Add handling address, maybe same action when scan qr code + case .transaction(let transactionType, let token): + resolver.showPaymentFlow(for: .send(type: .transaction(transactionType)), server: token.server, navigationController: resolver.presentationNavigationController) + } + }).store(in: &cancelable) case .walletConnect(let url, let source): switch source { case .safariExtension: @@ -786,6 +789,7 @@ extension AppCoordinator { struct WalletDependencies { let activitiesPipeLine: ActivitiesPipeLine let transactionsDataStore: TransactionDataStore + let tokensDataStore: TokensDataStore let importToken: ImportToken let tokensService: DetectedContractsProvideble & TokenProvidable & TokenAddable & TokensServiceTests let pipeline: TokensProcessingPipeline diff --git a/AlphaWallet/Browser/Coordinators/QRCodeResolutionCoordinator.swift b/AlphaWallet/Browser/Coordinators/QRCodeResolutionCoordinator.swift index 0ac801962..89739a66b 100644 --- a/AlphaWallet/Browser/Coordinators/QRCodeResolutionCoordinator.swift +++ b/AlphaWallet/Browser/Coordinators/QRCodeResolutionCoordinator.swift @@ -9,6 +9,7 @@ import Foundation import BigInt import PromiseKit import AlphaWalletFoundation +import Combine enum QrCodeResolution { case address(address: AlphaWallet.Address, action: ScanQRCodeAction) @@ -40,6 +41,8 @@ final class QRCodeResolutionCoordinator: Coordinator { } private let scanQRCodeCoordinator: ScanQRCodeCoordinator private let account: Wallet + private var cancellable = Set() + var coordinators: [Coordinator] = [] weak var delegate: QRCodeResolutionCoordinatorDelegate? @@ -108,16 +111,18 @@ extension QRCodeResolutionCoordinator: ScanQRCodeCoordinatorDelegate { switch usage { case .all(_, let importToken): let resolver = Eip681UrlResolver(config: config, importToken: importToken, missingRPCServerStrategy: .fallbackToFirstMatching) - firstly { - resolver.resolve(protocolName: protocolName, address: address, functionName: functionName, params: params) - }.done { result in - switch result { - case .transaction(let transactionType, let token): - delegate.coordinator(self, didResolve: .transactionType(transactionType: transactionType, token: token)) - case .address: - break // Not possible here - } - }.cauterize() + resolver.resolve(protocolName: protocolName, address: address, functionName: functionName, params: params) + .sink(receiveCompletion: { result in + guard case .failure(let error) = result else { return } + verboseLog("[Eip681UrlResolver] failure to resolve value from: \(qrCodeValue) with error: \(error)") + }, receiveValue: { result in + switch result { + case .transaction(let transactionType, let token): + delegate.coordinator(self, didResolve: .transactionType(transactionType: transactionType, token: token)) + case .address: + break // Not possible here + } + }).store(in: &cancellable) case .importWalletOnly: break } diff --git a/AlphaWallet/Tokens/ViewControllers/AddHideTokensViewController.swift b/AlphaWallet/Tokens/ViewControllers/AddHideTokensViewController.swift index 5641b573c..15d2f7764 100644 --- a/AlphaWallet/Tokens/ViewControllers/AddHideTokensViewController.swift +++ b/AlphaWallet/Tokens/ViewControllers/AddHideTokensViewController.swift @@ -182,17 +182,20 @@ extension AddHideTokensViewController: UITableViewDataSource { } else { tableView.reloadData() } - case .promise(let promise): + case .publisher(let publisher): self.displayLoading() - promise.done(on: .none, flags: .barrier) { _ in - //no-op - }.catch { _ in - self.displayError(message: R.string.localizable.walletsHideTokenErrorAddTokenFailure()) - }.finally { + + publisher.sink(receiveCompletion: { result in + if case .failure = result { + self.displayError(message: R.string.localizable.walletsHideTokenErrorAddTokenFailure()) + } + tableView.reloadData() self.hideLoading() - } + }, receiveValue: { _ in + //no-op + }).store(in: &cancelable) } } @@ -213,7 +216,7 @@ extension AddHideTokensViewController: UITableViewDataSource { completionHandler(false) } - case .promise: + case .publisher: break } } diff --git a/AlphaWallet/Tokens/ViewModels/AddHideTokensViewModel.swift b/AlphaWallet/Tokens/ViewModels/AddHideTokensViewModel.swift index cc49c21b6..c0be48371 100644 --- a/AlphaWallet/Tokens/ViewModels/AddHideTokensViewModel.swift +++ b/AlphaWallet/Tokens/ViewModels/AddHideTokensViewModel.swift @@ -140,23 +140,25 @@ final class AddHideTokensViewModel { } case .popularTokens: let token = popularTokens[indexPath.row] - let promise = importToken - .importToken(for: token.contractAddress, server: token.server, onlyIfThereIsABalance: false) - .then { [tokenCollection] _token -> Promise in - guard let token = tokenCollection.tokenViewModel(for: _token) else { return .init(error: PMKError.cancelled) } + let publisher = importToken + .importTokenPublisher(for: token.contractAddress, server: token.server, onlyIfThereIsABalance: false) + .flatMap { [tokenCollection] _token -> AnyPublisher in + guard let token = tokenCollection.tokenViewModel(for: _token) else { + return .fail(ImportToken.ImportTokenError.internal(error: PMKError.cancelled)) + } self.popularTokens.remove(at: indexPath.row) self.displayedTokens.append(token) if let sectionIndex = self.sections.firstIndex(of: .displayedTokens) { tokenCollection.mark(token: token, isHidden: false) - return .value((token, IndexPath(row: max(0, self.displayedTokens.count - 1), section: Int(sectionIndex)))) + return .just((token, IndexPath(row: max(0, self.displayedTokens.count - 1), section: Int(sectionIndex)))) } - return .value(nil) - } + return .just(nil) + }.eraseToAnyPublisher() - return .promise(promise) + return .publisher(publisher) } return .value(nil) @@ -285,7 +287,7 @@ extension AddHideTokensViewModel { enum ShowHideTokenResult { case value(TokenWithIndexToInsert?) - case promise(Promise) + case publisher(AnyPublisher) } } diff --git a/AlphaWallet/Transfer/ViewModels/SendViewModel.swift b/AlphaWallet/Transfer/ViewModels/SendViewModel.swift index 64da2156a..d58e03d98 100644 --- a/AlphaWallet/Transfer/ViewModels/SendViewModel.swift +++ b/AlphaWallet/Transfer/ViewModels/SendViewModel.swift @@ -434,7 +434,7 @@ final class TransactionTypeFromQrCode { } return eip681UrlResolver - .resolvePublisher(url: url) + .resolve(url: url) .flatMap { [session] result -> AnyPublisher in switch result { case .transaction(let transactionType, let token): diff --git a/AlphaWalletTests/Ens/EnsResolverTests.swift b/AlphaWalletTests/Ens/EnsResolverTests.swift index 5d47fa2fb..3589aa887 100644 --- a/AlphaWalletTests/Ens/EnsResolverTests.swift +++ b/AlphaWalletTests/Ens/EnsResolverTests.swift @@ -5,6 +5,8 @@ import Foundation import XCTest import Combine import AlphaWalletFoundation +import AlphaWalletCore +import AlphaWalletWeb3 class EnsResolverTests: XCTestCase { func testNameHash() { @@ -29,7 +31,16 @@ class EnsResolverTests: XCTestCase { case .finished: break case .failure(let error): - XCTFail("Unknown error: \(error)") + guard case .embeded(let e) = error, let pe = e as? PromiseError, let e = pe.embedded as? AlphaWalletWeb3.Web3Error else { + XCTFail("Unknown error: \(error)") + return + } + switch e { + case .rateLimited: + break + default: + XCTFail("Unknown error: \(error)") + } } expectation.fulfill() }, receiveValue: { address in @@ -50,7 +61,16 @@ class EnsResolverTests: XCTestCase { case .finished: break case .failure(let error): - XCTFail("Unknown error: \(error)") + guard case .embeded(let e) = error, let pe = e as? PromiseError, let e = pe.embedded as? AlphaWalletWeb3.Web3Error else { + XCTFail("Unknown error: \(error)") + return + } + switch e { + case .rateLimited: + break + default: + XCTFail("Unknown error: \(error)") + } } expectation.fulfill() }, receiveValue: { address in @@ -71,14 +91,23 @@ class EnsResolverTests: XCTestCase { case .finished: break case .failure(let error): - XCTFail("Unknown error: \(error)") + guard case .embeded(let e) = error, let pe = e as? PromiseError, let e = pe.embedded as? AlphaWalletWeb3.Web3Error else { + XCTFail("Unknown error: \(error)") + return + } + switch e { + case .rateLimited: + break + default: + XCTFail("Unknown error: \(error)") + } } expectation.fulfill() }, receiveValue: { address in XCTAssertTrue(address.sameContract(as: "41563129cdbbd0c5d3e1c86cf9563926b243834d"), "ENS name did not resolve correctly") }).store(in: &cancelable) - wait(for: expectations, timeout: 20) + wait(for: expectations, timeout: 100) } private func makeServerForMainnet() -> RPCServer { diff --git a/AlphaWalletTests/EtherClient/EtherKeystoreTests.swift b/AlphaWalletTests/EtherClient/EtherKeystoreTests.swift index 33d75fef9..db50a4b07 100644 --- a/AlphaWalletTests/EtherClient/EtherKeystoreTests.swift +++ b/AlphaWalletTests/EtherClient/EtherKeystoreTests.swift @@ -87,7 +87,7 @@ class EtherKeystoreTests: XCTestCase { XCTAssertEqual(seedPhrase.split(separator: " ").count, 12) }.store(in: &cancelable) - wait(for: [expectation], timeout: 0.01) + wait(for: [expectation], timeout: 600) } func testExportRawPrivateKeyToKeystoreFile() throws { @@ -107,8 +107,8 @@ class EtherKeystoreTests: XCTestCase { expectation.fulfill() }.store(in: &cancelable) - - wait(for: [expectation], timeout: 20) + //NOTE: increase waiting time, latest version of iOS takes more time to decode encrypted data + wait(for: [expectation], timeout: 600) } func testRecentlyUsedAccount() throws { diff --git a/AlphaWalletTests/Foundation/QRCodeValueParserTests.swift b/AlphaWalletTests/Foundation/QRCodeValueParserTests.swift index c8f0119ea..7b485a806 100644 --- a/AlphaWalletTests/Foundation/QRCodeValueParserTests.swift +++ b/AlphaWalletTests/Foundation/QRCodeValueParserTests.swift @@ -140,178 +140,139 @@ class QRCodeValueParserTests: XCTestCase { func testParseNativeCryptoSend() { guard let qrCodeValue = AddressOrEip681Parser.from(string: "ethereum:0xfb6916095ca1df60bb79Ce92ce3ea74c37c5d359?value=2.014e18") else { return XCTFail("Can't parse EIP 681") } - let expectation = self.expectation(description: "Promise resolves") switch qrCodeValue { case .address: XCTFail("Can't parse EIP 681") case .eip681(let protocolName, let address, let functionName, let params): - Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse().done { result in - expectation.fulfill() - switch result { - case .nativeCryptoSend(let chainId, let recipient, let amount): - XCTAssertNil(chainId) - XCTAssertTrue(recipient.sameContract(as: "0xfb6916095ca1df60bb79Ce92ce3ea74c37c5d359")) - XCTAssertEqual(amount.rawValue, "2014000000000000000") - case .erc20Send, .invalidOrNotSupported: - XCTFail("Parsed as wrong EIP 681 type") - } - }.cauterize() + switch Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse() { + case .nativeCryptoSend(let chainId, let recipient, let amount): + XCTAssertNil(chainId) + XCTAssertTrue(recipient.sameContract(as: "0xfb6916095ca1df60bb79Ce92ce3ea74c37c5d359")) + XCTAssertEqual(amount.rawValue, "2014000000000000000") + case .erc20Send, .invalidOrNotSupported: + XCTFail("Parsed as wrong EIP 681 type") + } } - wait(for: [expectation], timeout: 20) } func testParseNativeCryptoSendWithScientificNotation() { guard let qrCodeValue = AddressOrEip681Parser.from(string: "ethereum:0xfb6916095ca1df60bb79Ce92ce3ea74c37c5d359?value=2.014e18") else { return XCTFail("Can't parse EIP 681") } - let expectation = self.expectation(description: "Promise resolves") switch qrCodeValue { case .address: XCTFail("Can't parse EIP 681") case .eip681(let protocolName, let address, let functionName, let params): - Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse().done { result in - expectation.fulfill() - switch result { - case .nativeCryptoSend(let chainId, let recipient, let amount): - XCTAssertNil(chainId) - XCTAssertTrue(recipient.sameContract(as: "0xfb6916095ca1df60bb79Ce92ce3ea74c37c5d359")) - XCTAssertEqual(amount.rawValue, "2014000000000000000") - case .erc20Send, .invalidOrNotSupported: - XCTFail("Parsed as wrong EIP 681 type") - } - }.cauterize() + switch Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse() { + case .nativeCryptoSend(let chainId, let recipient, let amount): + XCTAssertNil(chainId) + XCTAssertTrue(recipient.sameContract(as: "0xfb6916095ca1df60bb79Ce92ce3ea74c37c5d359")) + XCTAssertEqual(amount.rawValue, "2014000000000000000") + case .erc20Send, .invalidOrNotSupported: + XCTFail("Parsed as wrong EIP 681 type") + } } - wait(for: [expectation], timeout: 20) } func testParseErc20Send() { guard let qrCodeValue = AddressOrEip681Parser.from(string: "ethereum:0x744d70fdbe2ba4cf95131626614a1763df805b9e/transfer?address=0x3d597789ea16054a084ac84ce87f50df9198f415&uint256=314e17") else { return XCTFail("Can't parse EIP 681") } - let expectation = self.expectation(description: "Promise resolves") switch qrCodeValue { case .address: XCTFail("Can't parse EIP 681") case .eip681(let protocolName, let address, let functionName, let params): - Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse().done { result in - expectation.fulfill() - switch result { - case .erc20Send(let contract, let chainId, let recipient, let amount): - XCTAssertEqual(contract, AlphaWallet.Address(string: "0x744d70fdbe2ba4cf95131626614a1763df805b9e")) - XCTAssertNil(chainId) - XCTAssertTrue(recipient?.sameContract(as: "0x3d597789ea16054a084ac84ce87f50df9198f415") ?? false) - XCTAssertEqual(amount.rawValue, "31400000000000000000") - case .nativeCryptoSend, .invalidOrNotSupported: - XCTFail("Parsed as wrong EIP 681 type") - } - }.cauterize() + switch Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse() { + case .erc20Send(let contract, let chainId, let recipient, let amount): + XCTAssertEqual(contract, AlphaWallet.Address(string: "0x744d70fdbe2ba4cf95131626614a1763df805b9e")) + XCTAssertNil(chainId) + XCTAssertTrue(recipient?.sameContract(as: "0x3d597789ea16054a084ac84ce87f50df9198f415") ?? false) + XCTAssertEqual(amount.rawValue, "31400000000000000000") + case .nativeCryptoSend, .invalidOrNotSupported: + XCTFail("Parsed as wrong EIP 681 type") + } } - wait(for: [expectation], timeout: 20) } func testParseErc20SendWithoutRecipient() { guard let qrCodeValue = AddressOrEip681Parser.from(string: "ethereum:0x60fa213f48cd0d83b54380108ccd03a6993247e0/transfer?uint256=1.5e18") else { return XCTFail("Can't parse EIP 681") } - let expectation = self.expectation(description: "Promise resolves") switch qrCodeValue { case .address: XCTFail("Can't parse EIP 681") case .eip681(let protocolName, let address, let functionName, let params): - Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse().done { result in - expectation.fulfill() - switch result { - case .erc20Send(let contract, let chainId, let recipient, let amount): - XCTAssertEqual(contract, AlphaWallet.Address(string: "0x60fa213f48cd0d83b54380108ccd03a6993247e0")) - XCTAssertNil(chainId) - XCTAssertNil(recipient) - XCTAssertEqual(amount.rawValue, "1500000000000000000") - case .nativeCryptoSend, .invalidOrNotSupported: - XCTFail("Parsed as wrong EIP 681 type") - } - }.cauterize() + switch Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse() { + case .erc20Send(let contract, let chainId, let recipient, let amount): + XCTAssertEqual(contract, AlphaWallet.Address(string: "0x60fa213f48cd0d83b54380108ccd03a6993247e0")) + XCTAssertNil(chainId) + XCTAssertNil(recipient) + XCTAssertEqual(amount.rawValue, "1500000000000000000") + case .nativeCryptoSend, .invalidOrNotSupported: + XCTFail("Parsed as wrong EIP 681 type") + } } - wait(for: [expectation], timeout: 20) } func testParseNativeCryptoSendWithoutValue() { guard let qrCodeValue = AddressOrEip681Parser.from(string: "ethereum:0xfb6916095ca1df60bb79Ce92ce3ea74c37c5d359") else { return XCTFail("Can't parse EIP 681") } - let expectation = self.expectation(description: "Promise resolves") switch qrCodeValue { case .address: XCTFail("Can't parse EIP 681") case .eip681(let protocolName, let address, let functionName, let params): - Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse().done { result in - expectation.fulfill() - switch result { - case .nativeCryptoSend(let chainId, let recipient, let amount): - XCTAssertNil(chainId) - XCTAssertTrue(recipient.sameContract(as: "0xfb6916095ca1df60bb79Ce92ce3ea74c37c5d359")) - XCTAssertEqual(amount.rawValue, "") - case .erc20Send, .invalidOrNotSupported: - XCTFail("Parsed as wrong EIP 681 type") - } - }.cauterize() + switch Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse() { + case .nativeCryptoSend(let chainId, let recipient, let amount): + XCTAssertNil(chainId) + XCTAssertTrue(recipient.sameContract(as: "0xfb6916095ca1df60bb79Ce92ce3ea74c37c5d359")) + XCTAssertEqual(amount.rawValue, "") + case .erc20Send, .invalidOrNotSupported: + XCTFail("Parsed as wrong EIP 681 type") + } } - wait(for: [expectation], timeout: 20) } func testParseErc20SendWithoutAmount() { guard let qrCodeValue = AddressOrEip681Parser.from(string: "ethereum:0x744d70fdbe2ba4cf95131626614a1763df805b9e/transfer?address=0x3d597789ea16054a084ac84ce87f50df9198f415") else { return XCTFail("Can't parse EIP 681") } - let expectation = self.expectation(description: "Promise resolves") switch qrCodeValue { case .address: XCTFail("Can't parse EIP 681") case .eip681(let protocolName, let address, let functionName, let params): - Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse().done { result in - expectation.fulfill() - switch result { - case .erc20Send(let contract, let chainId, let recipient, let amount): - XCTAssertEqual(contract, AlphaWallet.Address(string: "0x744d70fdbe2ba4cf95131626614a1763df805b9e")) - XCTAssertNil(chainId) - XCTAssertTrue(recipient?.sameContract(as: "0x3d597789ea16054a084ac84ce87f50df9198f415") ?? false) - XCTAssertEqual(amount.rawValue, "") - case .nativeCryptoSend, .invalidOrNotSupported: - XCTFail("Parsed as wrong EIP 681 type") - } - }.cauterize() + + switch Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse() { + case .erc20Send(let contract, let chainId, let recipient, let amount): + XCTAssertEqual(contract, AlphaWallet.Address(string: "0x744d70fdbe2ba4cf95131626614a1763df805b9e")) + XCTAssertNil(chainId) + XCTAssertTrue(recipient?.sameContract(as: "0x3d597789ea16054a084ac84ce87f50df9198f415") ?? false) + XCTAssertEqual(amount.rawValue, "") + case .nativeCryptoSend, .invalidOrNotSupported: + XCTFail("Parsed as wrong EIP 681 type") + } } - wait(for: [expectation], timeout: 20) } func testParseInvalidNativeCryptoSend() { guard let qrCodeValue = AddressOrEip681Parser.from(string: "ethereum:0xfb6916095ca1df60bb79Ce92ce3ea74c37c5d359/foo?value=2.014e18") else { return XCTFail("Can't parse EIP 681") } - let expectation = self.expectation(description: "Promise resolves") switch qrCodeValue { case .address: XCTFail("Can't parse EIP 681") case .eip681(let protocolName, let address, let functionName, let params): - Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse().done { result in - expectation.fulfill() - switch result { - case .nativeCryptoSend, .erc20Send: - XCTFail("Parsed as wrong EIP 681 type") - case .invalidOrNotSupported: - XCTAssert(true) - } - }.cauterize() + switch Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse() { + case .nativeCryptoSend, .erc20Send: + XCTFail("Parsed as wrong EIP 681 type") + case .invalidOrNotSupported: + XCTAssert(true) + } } - wait(for: [expectation], timeout: 20) } func testParseNativeCryptoSendWithoutValueWithEnsName() { guard let qrCodeValue = AddressOrEip681Parser.from(string: "ethereum:foo.eth") else { return XCTFail("Can't parse EIP 681") } - let expectation = self.expectation(description: "Promise resolves") switch qrCodeValue { case .address: XCTFail("Can't parse EIP 681") case .eip681(let protocolName, let address, let functionName, let params): - Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse().done { result in - expectation.fulfill() - switch result { - case .nativeCryptoSend(let chainId, let recipient, let amount): - XCTAssertNil(chainId) - XCTAssertTrue(recipient.stringValue == "foo.eth") - XCTAssertEqual(amount.rawValue, "") - case .erc20Send, .invalidOrNotSupported: - XCTFail("Parsed as wrong EIP 681 type") - } - }.cauterize() + switch Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse() { + case .nativeCryptoSend(let chainId, let recipient, let amount): + XCTAssertNil(chainId) + XCTAssertTrue(recipient.stringValue == "foo.eth") + XCTAssertEqual(amount.rawValue, "") + case .erc20Send, .invalidOrNotSupported: + XCTFail("Parsed as wrong EIP 681 type") + } } - wait(for: [expectation], timeout: 20) } } diff --git a/AlphaWalletTests/Transfer/Types/TransactionTypeFromQrCodeTests.swift b/AlphaWalletTests/Transfer/Types/TransactionTypeFromQrCodeTests.swift index 5f20ec0e2..b0cc1168f 100644 --- a/AlphaWalletTests/Transfer/Types/TransactionTypeFromQrCodeTests.swift +++ b/AlphaWalletTests/Transfer/Types/TransactionTypeFromQrCodeTests.swift @@ -28,8 +28,7 @@ class ImportTokenTests: XCTestCase { XCTAssertNil(tokensDataStore.token(forContract: address, server: server), "Initially token is nil") let expectation = self.expectation(description: "did resolve erc20 token") - importToken.importToken(for: address, server: server) - .publisher + importToken.importTokenPublisher(for: address, server: server) .sink(receiveCompletion: { _ in expectation.fulfill() }, receiveValue: { token in @@ -54,8 +53,7 @@ class ImportTokenTests: XCTestCase { let expectation = self.expectation(description: "did resolve erc20 token") expectation.isInverted = true - importToken.importToken(for: address, server: server) - .publisher + importToken.importTokenPublisher(for: address, server: server) .sink(receiveCompletion: { _ in expectation.fulfill() }, receiveValue: { _ in @@ -79,8 +77,7 @@ class ImportTokenTests: XCTestCase { let expectation = self.expectation(description: "did resolve erc20 token") contractDataFetcher.contractData[.init(address: address, server: server)] = .fungibleTokenComplete(name: "erc20", symbol: "erc20", decimals: 6, value: .init("1"), tokenType: .erc20) - importToken.importToken(for: address, server: server) - .publisher + importToken.importTokenPublisher(for: address, server: server) .sink(receiveCompletion: { _ in expectation.fulfill() }, receiveValue: { token in @@ -112,12 +109,22 @@ class TransactionTypeFromQrCodeTests: XCTestCase { let etherToken = Token(contract: Constants.nativeCryptoAddressInDatabase, server: .main, name: "Ether", symbol: "eth", decimals: 18, type: .nativeCryptocurrency) //NOTE: make sure we have a eth token, base impl resolves it automatically, for test does it manually - tokensDataStore.addOrUpdate(with: [.init(etherToken)]) + tokensDataStore.addOrUpdate(with: [ + .init(etherToken) + ]) provider.buildTransactionType(qrCode: qrCode) .sink { result in - guard case .success(let transactionType) = result else { fatalError() } - guard case .nativeCryptocurrency(let token, let destination, let amount) = transactionType else { fatalError() } + guard case .success(let transactionType) = result else { + XCTFail("failure as resolved: \(result)") + expectation.fulfill() + return + } + guard case .nativeCryptocurrency(let token, let destination, let amount) = transactionType else { + XCTFail("failure as resolved: \(transactionType)") + expectation.fulfill() + return + } XCTAssertEqual(token, etherToken) XCTAssertNotNil(destination) XCTAssertNotNil(amount) @@ -138,6 +145,7 @@ class TransactionTypeFromQrCodeTests: XCTestCase { //NOTE: make sure we have a eth token, base impl resolves it automatically, for test does it manually tokensDataStore.addOrUpdate(with: [.init(erc20Token)]) + transactionTypeSupportable.transactionType = .erc20Token(erc20Token, destination: nil, amount: .notSet) provider.buildTransactionType(qrCode: qrCode) diff --git a/AlphaWalletTests/Transfer/ViewControllers/SendViewControllerTests.swift b/AlphaWalletTests/Transfer/ViewControllers/SendViewControllerTests.swift index b5a801752..ed99e5551 100644 --- a/AlphaWalletTests/Transfer/ViewControllers/SendViewControllerTests.swift +++ b/AlphaWalletTests/Transfer/ViewControllers/SendViewControllerTests.swift @@ -18,6 +18,7 @@ class SendViewControllerTests: XCTestCase { return .nativeCryptocurrency(token, destination: nil, amount: .notSet) }() private let dep = WalletDataProcessingPipeline.make(wallet: .make(), server: .main) + let contractDataFetcher = FakeContractDataFetcher() func testNativeCryptocurrencyAllFundsValueSpanish() { let vc = createSendViewControllerAndSetLocale(locale: .spanish, transactionType: nativeCryptocurrencyTransactionType) @@ -160,20 +161,25 @@ class SendViewControllerTests: XCTestCase { func testScanEip681QrCodeEnglish() { let token = Token(contract: AlphaWallet.Address.make(), server: .main, decimals: 18, value: "0", type: .erc20) + dep.tokensService.addOrUpdateTokenTestsOnly(token: token) let vc = createSendViewControllerAndSetLocale(locale: .english, transactionType: .erc20Token(token, destination: .none, amount: .amount(1.34))) dep.tokensService.setBalanceTestsOnly(balance: .init(value: BigInt("2020224719101120")), for: token) XCTAssertEqual(vc.amountTextField.value, "1.34") + let address = AlphaWallet.Address(string: "0xbc8dafeaca658ae0857c80d8aa6de4d487577c63")! + let server = RPCServer.main + contractDataFetcher.contractData[.init(address: address, server: server)] = .fungibleTokenComplete(name: "erc20", symbol: "erc20", decimals: 18, value: .zero, tokenType: .erc20) + let qrCode = "aw.app/ethereum:0xbc8dafeaca658ae0857c80d8aa6de4d487577c63@1?value=1e19" vc.didScanQRCode(qrCode) let expectation = self.expectation(description: "did update token balance expectation") let destination = AlphaWallet.Address(string: "0xbc8dafeaca658ae0857c80d8aa6de4d487577c63").flatMap { AddressOrEnsName(address: $0) } - XCTAssertEqual(vc.viewModel.latestQrCode, qrCode) - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + XCTAssertEqual(vc.viewModel.latestQrCode, qrCode) + switch vc.viewModel.scanQrCodeLatest { case .success(let transactionType): XCTAssertEqual(transactionType.amount, .amount(10)) @@ -201,10 +207,23 @@ class SendViewControllerTests: XCTestCase { dep.tokensService.setBalanceTestsOnly(balance: .init(value: BigInt("2020224719101120")), for: token) XCTAssertEqual(vc.amountTextField.value, "1,34") + let address = AlphaWallet.Address(string: "0xbc8dafeaca658ae0857c80d8aa6de4d487577c63")! + let server = RPCServer.main + contractDataFetcher.contractData[.init(address: address, server: server)] = .fungibleTokenComplete(name: "erc20", symbol: "erc20", decimals: 18, value: .zero, tokenType: .erc20) + vc.didScanQRCode("aw.app/ethereum:0xbc8dafeaca658ae0857c80d8aa6de4d487577c63@1?value=1e17") let expectation = self.expectation(description: "did update token balance expectation") DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + switch vc.viewModel.scanQrCodeLatest { + case .success(let transactionType): + XCTAssertEqual(transactionType.amount, .amount(0.1)) + case .failure(let e): + XCTFail(e.description) + case .none: + XCTFail() + } + XCTAssertEqual(vc.amountTextField.value, "0,1") expectation.fulfill() } @@ -237,7 +256,8 @@ class SendViewControllerTests: XCTestCase { private func createSendViewControllerAndSetLocale(locale: AppLocale, transactionType: TransactionType) -> SendViewController { Config.setLocale(locale) - let viewModel = SendViewModel(transactionType: transactionType, session: dep.sessionsProvider.session(for: .main)!, tokensService: dep.pipeline, importToken: dep.importToken) + let importToken = ImportToken.make(tokensDataStore: dep.tokensDataStore, contractDataFetcher: contractDataFetcher) + let viewModel = SendViewModel(transactionType: transactionType, session: dep.sessionsProvider.session(for: .main)!, tokensService: dep.pipeline, importToken: importToken) return SendViewController(viewModel: viewModel, domainResolutionService: FakeDomainResolutionService()) } } diff --git a/AlphaWalletTests/ViewControllers/PaymentCoordinatorTests.swift b/AlphaWalletTests/ViewControllers/PaymentCoordinatorTests.swift index 6666fcfcd..7d44c5000 100644 --- a/AlphaWalletTests/ViewControllers/PaymentCoordinatorTests.swift +++ b/AlphaWalletTests/ViewControllers/PaymentCoordinatorTests.swift @@ -72,6 +72,7 @@ extension WalletDataProcessingPipeline { let dep = AppCoordinator.WalletDependencies( activitiesPipeLine: activitiesPipeLine, transactionsDataStore: transactionsDataStore, + tokensDataStore: tokensDataStore, importToken: importToken, tokensService: tokensService, pipeline: pipeline, diff --git a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Foundation/Eip681Parser.swift b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Foundation/Eip681Parser.swift index 4c68cfa29..9d63358f6 100644 --- a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Foundation/Eip681Parser.swift +++ b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Foundation/Eip681Parser.swift @@ -63,7 +63,7 @@ public struct Eip681Parser { private let decimalParser = DecimalParser() //https://github.com/ethereum/EIPs/blob/master/EIPS/eip-681.md - public func parse() -> Promise { + public func parse() -> Eip681Type { let chainId = params["chainId"].flatMap { Int($0) } if functionName == "transfer", let contract = address.contract { let recipient = params["address"].flatMap({ AddressOrEnsName(string: $0) }) @@ -76,7 +76,7 @@ public struct Eip681Parser { } else { amount = "" } - return .value(.erc20Send(contract: contract, server: chainId.flatMap { .init(chainID: $0) }, recipient: recipient, amount: .uint256(amount, eNotation: eNotation))) + return .erc20Send(contract: contract, server: chainId.flatMap { .init(chainID: $0) }, recipient: recipient, amount: .uint256(amount, eNotation: eNotation)) } else if functionName == nil { //TODO UNITS is either optional or "ETH" for native crypto sends. If it's not provided, we treat it as something like 3.14e18, but it also can be like 1 let amount: String @@ -85,9 +85,9 @@ public struct Eip681Parser { } else { amount = "" } - return .value(.nativeCryptoSend(server: chainId.flatMap { .init(chainID: $0) }, recipient: address, amount: .ether(amount))) + return .nativeCryptoSend(server: chainId.flatMap { .init(chainID: $0) }, recipient: address, amount: .ether(amount)) } else { - return .value(.invalidOrNotSupported) + return .invalidOrNotSupported } } diff --git a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/Eip681UrlResolver.swift b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/Eip681UrlResolver.swift index ae1b0c70f..9a57b01bd 100644 --- a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/Eip681UrlResolver.swift +++ b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/Eip681UrlResolver.swift @@ -28,47 +28,43 @@ public final class Eip681UrlResolver { self.missingRPCServerStrategy = missingRPCServerStrategy } - @discardableResult public func resolvePublisher(url: URL) -> AnyPublisher { - return resolve(url: url).publisher - .receive(on: RunLoop.main) - .mapError { return $0.embedded as? CheckEIP681Error ?? .embeded(error: $0.embedded) } - .eraseToAnyPublisher() - } - - @discardableResult public func resolve(url: URL) -> Promise { + @discardableResult public func resolve(url: URL) -> AnyPublisher { switch AddressOrEip681Parser.from(string: url.absoluteString) { case .address(let address): - return .value(.address(address)) + return .just(.address(address)) case .eip681(let protocolName, let address, let functionName, let params): return resolve(protocolName: protocolName, address: address, functionName: functionName, params: params) case .none: - return .init(error: CheckEIP681Error.notEIP681) + return .fail(CheckEIP681Error.notEIP681) } } - @discardableResult public func resolve(protocolName: String, address: AddressOrEnsName, functionName: String?, params: [String: String]) -> Promise { - Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params) - .parse() - .then { [importToken] result -> Promise in + @discardableResult public func resolve(protocolName: String, address: AddressOrEnsName, functionName: String?, params: [String: String]) -> AnyPublisher { + return Just(protocolName) + .setFailureType(to: CheckEIP681Error.self) + .map { protocolName in + Eip681Parser(protocolName: protocolName, address: address, functionName: functionName, params: params).parse() + }.flatMap { [importToken] result -> AnyPublisher in guard let (contract: contract, customServer, recipient, amount) = result.parameters else { - return .init(error: CheckEIP681Error.parameterInvalid) + return .fail(CheckEIP681Error.parameterInvalid) } guard let server = self.serverFromEip681LinkOrDefault(customServer) else { - return .init(error: CheckEIP681Error.missingRpcServer) + return .fail(CheckEIP681Error.missingRpcServer) } return importToken - .importToken(for: contract, server: server) - .map { token -> Eip681UrlResolver.Resolution in + .importTokenPublisher(for: contract, server: server) + .mapError { CheckEIP681Error.embeded(error: $0) } + .flatMap { token -> AnyPublisher in switch token.type { case .erc20, .nativeCryptocurrency: let transactionType = Eip681UrlResolver.buildFungibleTransactionType(token, recipient: recipient, amount: amount) - return .transaction(transactionType, token: token) + return .just(.transaction(transactionType, token: token)) case .erc1155, .erc721, .erc721ForTickets, .erc875: - throw CheckEIP681Error.tokenTypeNotSupported + return .fail(CheckEIP681Error.tokenTypeNotSupported) } - } - } + }.eraseToAnyPublisher() + }.eraseToAnyPublisher() } private func serverFromEip681LinkOrDefault(_ serverInLink: RPCServer?) -> RPCServer? { diff --git a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/TokensAutodetector/ImportToken.swift b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/TokensAutodetector/ImportToken.swift index f1ed820d8..8fb57ab05 100644 --- a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/TokensAutodetector/ImportToken.swift +++ b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/TokensAutodetector/ImportToken.swift @@ -13,10 +13,11 @@ import BigInt public protocol TokenImportable { func importToken(ercToken: ErcToken, shouldUpdateBalance: Bool) -> Token func importToken(for contract: AlphaWallet.Address, server: RPCServer, onlyIfThereIsABalance: Bool) -> Promise + func importTokenPublisher(for contract: AlphaWallet.Address, server: RPCServer, onlyIfThereIsABalance: Bool) -> AnyPublisher } public protocol TokenOrContractFetchable: ContractDataFetchable { - func fetchTokenOrContract(for contract: AlphaWallet.Address, server: RPCServer, onlyIfThereIsABalance: Bool) -> Promise + func fetchTokenOrContractPublisher(for contract: AlphaWallet.Address, server: RPCServer, onlyIfThereIsABalance: Bool) -> AnyPublisher } //NOTE: actually its internal, public for tests @@ -34,7 +35,11 @@ public final class ContractDataFetcher: ContractDataFetchable { private let reachability: ReachabilityManagerProtocol private let sessionProvider: SessionsProvider - public init(sessionProvider: SessionsProvider, assetDefinitionStore: AssetDefinitionStore, analytics: AnalyticsLogger, reachability: ReachabilityManagerProtocol) { + public init(sessionProvider: SessionsProvider, + assetDefinitionStore: AssetDefinitionStore, + analytics: AnalyticsLogger, + reachability: ReachabilityManagerProtocol) { + self.assetDefinitionStore = assetDefinitionStore self.sessionProvider = sessionProvider self.analytics = analytics @@ -52,7 +57,7 @@ public final class ContractDataFetcher: ContractDataFetchable { } final public class ImportToken: TokenImportable, TokenOrContractFetchable { - enum ImportTokenError: Error { + public enum ImportTokenError: Error { case nativeCryptoNotSupported case serverIsDisabled case zeroBalanceDetected @@ -65,6 +70,7 @@ final public class ImportToken: TokenImportable, TokenOrContractFetchable { private var cancelable = Set() private let queue = DispatchQueue(label: "org.alphawallet.swift.importToken") private var inFlightPromises: [String: Promise] = [:] + private var inFlightPublishers: [String: AnyPublisher] = [:] private let reachability = ReachabilityManager() public init(tokensDataStore: TokensDataStore, contractDataFetcher: ContractDataFetchable) { @@ -77,11 +83,6 @@ final public class ImportToken: TokenImportable, TokenOrContractFetchable { firstly { .value(server) }.then(on: queue, { [queue, tokensDataStore] server -> Promise in - //Useful to check because we are/might action-only TokenScripts for native crypto currency - guard contract != Constants.nativeCryptoAddressInDatabase else { - return .init(error: ImportTokenError.nativeCryptoNotSupported) - } - if let token = tokensDataStore.token(forContract: contract, server: server) { return .value(token) } else { @@ -98,6 +99,30 @@ final public class ImportToken: TokenImportable, TokenOrContractFetchable { }) } + public func importTokenPublisher(for contract: AlphaWallet.Address, server: RPCServer, onlyIfThereIsABalance: Bool = false) -> AnyPublisher { + return Just(server) + .receive(on: queue) + .setFailureType(to: ImportTokenError.self) + .flatMap { [tokensDataStore, queue] server -> AnyPublisher in + if let token = tokensDataStore.token(forContract: contract, server: server) { + return .just(token) + } else { + return self.fetchTokenOrContractPublisher(for: contract, server: server, onlyIfThereIsABalance: onlyIfThereIsABalance) + .flatMap { tokenOrContract -> AnyPublisher in + //FIXME: looks like blocking access to realm doesn't work well, after adding a new token and retrieving its value from bd, returns nil, adding delay in 1 sec to return a new token. + if let token = tokensDataStore.addOrUpdate(tokensOrContracts: [tokenOrContract]).first { + return .just(token) + .delay(for: .seconds(1), scheduler: queue) + .eraseToAnyPublisher() + } else { + return .fail(ImportTokenError.notContractOrFailed(tokenOrContract)) + } + }.eraseToAnyPublisher() + } + }.receive(on: RunLoop.main) + .eraseToAnyPublisher() + } + public func importToken(ercToken: ErcToken, shouldUpdateBalance: Bool = true) -> Token { let tokens = tokensDataStore.addOrUpdate(with: [.add(ercToken: ercToken, shouldUpdateBalance: shouldUpdateBalance)]) @@ -108,12 +133,17 @@ final public class ImportToken: TokenImportable, TokenOrContractFetchable { contractDataFetcher.fetchContractData(for: contract, server: server, completion: completion) } - public func fetchTokenOrContract(for contract: AlphaWallet.Address, server: RPCServer, onlyIfThereIsABalance: Bool = false) -> Promise { + private func fetchTokenOrContract(for contract: AlphaWallet.Address, server: RPCServer, onlyIfThereIsABalance: Bool = false) -> Promise { firstly { .value(contract) }.then(on: queue, { [queue] contract -> Promise in let key = "\(contract.hashValue)-\(onlyIfThereIsABalance)-\(server)" + //Useful to check because we are/might action-only TokenScripts for native crypto currency + guard contract != Constants.nativeCryptoAddressInDatabase else { + return .init(error: ImportTokenError.nativeCryptoNotSupported) + } + if let promise = self.inFlightPromises[key] { return promise } else { @@ -163,4 +193,66 @@ final public class ImportToken: TokenImportable, TokenOrContractFetchable { } }) } + + public func fetchTokenOrContractPublisher(for contract: AlphaWallet.Address, server: RPCServer, onlyIfThereIsABalance: Bool = false) -> AnyPublisher { + Just(contract) + .receive(on: queue) + .setFailureType(to: ImportTokenError.self) + .flatMap { [queue] contract -> AnyPublisher in + //Useful to check because we are/might action-only TokenScripts for native crypto currency + guard contract != Constants.nativeCryptoAddressInDatabase else { + return .fail(ImportTokenError.nativeCryptoNotSupported) + } + + let key = "\(contract.hashValue)-\(onlyIfThereIsABalance)-\(server)" + + if let publisher = self.inFlightPublishers[key] { + return publisher + } else { + let publisher = Future { seal in + self.fetchContractData(for: contract, server: server) { data in + switch data { + case .name, .symbol, .balance, .decimals: + break + case .nonFungibleTokenComplete(let name, let symbol, let balance, let tokenType): + guard !onlyIfThereIsABalance || (onlyIfThereIsABalance && !balance.isEmpty) else { + seal(.failure(ImportTokenError.zeroBalanceDetected)) + return + } + let ercToken = ErcToken(contract: contract, server: server, name: name, symbol: symbol, decimals: 0, type: tokenType, value: "0", balance: balance) + + seal(.success(.ercToken(ercToken))) + case .fungibleTokenComplete(let name, let symbol, let decimals, let value, let tokenType): + //NOTE: we want to make get balance for fungible token, fetching for token from data source might be unusefull as token hasn't created yes (when we fetch for a new contract) so we fetch tokens balance sync on `getFungibleBalanceQueue` and return result on `.main` queue + // one more additional network call, shouldn't be complex. + guard !onlyIfThereIsABalance || (onlyIfThereIsABalance && (value != .zero)) else { + seal(.failure(ImportTokenError.zeroBalanceDetected)) + return + } + + let ercToken = ErcToken(contract: contract, server: server, name: name, symbol: symbol, decimals: decimals, type: tokenType, value: value, balance: .balance(["0"])) + + seal(.success(.ercToken(ercToken))) + case .delegateTokenComplete: + seal(.success(.delegateContracts([AddressAndRPCServer(address: contract, server: server)]))) + case .failed(let networkReachable, let error): + //Receives first received error, e.g name, symbol, token type, decimals + //TODO: maybe its need to handle some cases of error here? + if networkReachable { + seal(.success(.deletedContracts([AddressAndRPCServer(address: contract, server: server)]))) + } else { + seal(.failure(ImportTokenError.internal(error: error))) + } + } + } + }.receive(on: queue) + .handleEvents(receiveCompletion: { _ in self.inFlightPublishers[key] = nil }) + .eraseToAnyPublisher() + + self.inFlightPublishers[key] = publisher + + return publisher + } + }.eraseToAnyPublisher() + } } diff --git a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/TokensAutodetector/TokensAutodetector.swift b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/TokensAutodetector/TokensAutodetector.swift index 21ae20197..f5b61aa5a 100644 --- a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/TokensAutodetector/TokensAutodetector.swift +++ b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/TokensAutodetector/TokensAutodetector.swift @@ -120,13 +120,12 @@ public class SingleChainTokensAutodetector: NSObject, TokensAutodetector { return autoDetectTransactedContractsImpl(wallet: wallet, erc20: erc20, server: server) .flatMap { [importToken, queue] detectedContracts -> AnyPublisher<[TokenOrContract], Never> in - let promises = self.contractsForTransactedTokens(detectedContracts: detectedContracts, forServer: server) - .map { importToken.fetchTokenOrContract(for: $0, server: server, onlyIfThereIsABalance: false) } + let publishers = self.contractsForTransactedTokens(detectedContracts: detectedContracts, forServer: server) + .map { importToken.fetchTokenOrContractPublisher(for: $0, server: server, onlyIfThereIsABalance: false).mapToResult() } - return when(resolved: promises) - .map(on: queue, { $0.compactMap { $0.optionalValue } }) - .publisher - .replaceError(with: []) + return Publishers.MergeMany(publishers).collect() + .map { $0.compactMap { try? $0.get() } } + .receive(on: queue) .eraseToAnyPublisher() }.eraseToAnyPublisher() } @@ -165,13 +164,12 @@ extension SingleChainTokensAutodetector: AutoDetectTransactedTokensOperationDele extension SingleChainTokensAutodetector: AutoDetectTokensOperationDelegate { func autoDetectTokensImpl(withContracts contractsToDetect: [ContractToImport]) -> AnyPublisher<[TokenOrContract], Never> { - let promises = contractsToAutodetectTokens(contractsToDetect: contractsToDetect) - .map { importToken.fetchTokenOrContract(for: $0.contract, server: $0.server, onlyIfThereIsABalance: $0.onlyIfThereIsABalance) } + let publishers = contractsToAutodetectTokens(contractsToDetect: contractsToDetect) + .map { importToken.fetchTokenOrContractPublisher(for: $0.contract, server: $0.server, onlyIfThereIsABalance: $0.onlyIfThereIsABalance).mapToResult() } - return when(resolved: promises) - .map(on: queue, { $0.compactMap { $0.optionalValue } }) - .publisher - .replaceError(with: []) + return Publishers.MergeMany(publishers).collect() + .map { $0.compactMap { try? $0.get() } } + .receive(on: queue) .eraseToAnyPublisher() }