Improve Realm access and replacing many publishers with async/await calls to block less when app launches.

pull/6911/head
Hwee-Boon Yar 1 year ago
parent 30c17de412
commit 9da79b2010
  1. 2
      AlphaWallet/Accounts/ViewModels/AccountsViewModel.swift
  2. 64
      AlphaWallet/ActiveWalletCoordinator.swift
  3. 2
      AlphaWallet/Activities/Coordinators/ActivitiesCoordinator.swift
  4. 11
      AlphaWallet/Activities/ViewControllers/ActivitiesViewController.swift
  5. 6
      AlphaWallet/Browser/Coordinators/QRCodeResolutionCoordinator.swift
  6. 10
      AlphaWallet/Common/Initializers/ConfigureApp.swift
  7. 5
      AlphaWallet/Common/Services/MediaContentNetworking.swift
  8. 28
      AlphaWallet/Common/Views/AddressOrEnsNameLabel.swift
  9. 31
      AlphaWallet/Market/ImportMagicLinkController.swift
  10. 84
      AlphaWallet/Swap/Swap/ViewModels/SwapTokensViewModel.swift
  11. 29
      AlphaWallet/Swap/SwapOptions/ViewModels/SwapOptionsViewModel.swift
  12. 4
      AlphaWallet/Tokens/Collectibles/ViewModels/NFTCollectionViewModel.swift
  13. 80
      AlphaWallet/Tokens/Coordinators/FungibleTokenCoordinator.swift
  14. 9
      AlphaWallet/Tokens/Coordinators/NewTokenCoordinator.swift
  15. 26
      AlphaWallet/Tokens/Coordinators/SingleChainTokenCoordinator.swift
  16. 8
      AlphaWallet/Tokens/ViewControllers/SelectTokenViewController.swift
  17. 8
      AlphaWallet/Tokens/ViewControllers/TokensViewController.swift
  18. 36
      AlphaWallet/Tokens/ViewModels/AddHideTokensViewModel.swift
  19. 37
      AlphaWallet/Tokens/ViewModels/FungibleTokenDetailsViewModel.swift
  20. 9
      AlphaWallet/Tokens/ViewModels/FungibleTokenTabViewModel.swift
  21. 4
      AlphaWallet/Tokens/ViewModels/SelectTokenViewModel.swift
  22. 51
      AlphaWallet/Tokens/ViewModels/TokensViewModel.swift
  23. 12
      AlphaWallet/Transactions/Coordinators/TransactionsCoordinator.swift
  24. 18
      AlphaWallet/Transactions/Views/TransactionHeaderView.swift
  25. 18
      AlphaWallet/Transfer/ViewModels/RequestViewModel.swift
  26. 111
      AlphaWallet/Transfer/ViewModels/SendViewModel.swift
  27. 7
      AlphaWallet/Transfer/ViewModels/TransactionConfirmation/DappOrWalletConnectTransactionViewModel.swift
  28. 7
      AlphaWallet/Transfer/ViewModels/TransactionConfirmation/SendFungiblesTransactionViewModel.swift
  29. 5
      AlphaWallet/Transfer/ViewModels/TransactionConfirmation/SendNftTransactionViewModel.swift
  30. 8
      AlphaWallet/Wallet/ViewModels/RenameWalletViewModel.swift
  31. 4
      AlphaWalletTests/Coordinators/ActiveWalletViewTests.swift
  32. 19
      AlphaWalletTests/Core/FakeDomainResolutionService.swift
  33. 35
      AlphaWalletTests/Core/Types/TickerIdsFetcherTests.swift
  34. 44
      AlphaWalletTests/Ens/EnsRecordsStorageTests.swift
  35. 99
      AlphaWalletTests/Ens/EnsResolverTests.swift
  36. 8
      AlphaWalletTests/EtherClient/RLPTests.swift
  37. 2
      AlphaWalletTests/Factories/FakeCoinTickersFetcher.swift
  38. 2
      AlphaWalletTests/Factories/FakeEnsRecordsStorage.swift
  39. 6
      AlphaWalletTests/Factories/FakeNftProvider.swift
  40. 8
      AlphaWalletTests/Settings/ConfigTests.swift
  41. 13
      AlphaWalletTests/Settings/Coordinators/SettingsCoordinatorTests.swift
  42. 30
      AlphaWalletTests/Tokens/Helpers/TokenObjectTest.swift
  43. 13
      AlphaWalletTests/Tokens/TokensDataStoreTest.swift
  44. 50
      AlphaWalletTests/Transactions/Storage/TransactionsStorageTests.swift
  45. 34
      AlphaWalletTests/Transfer/Types/TransactionTypeFromQrCodeTests.swift
  46. 56
      AlphaWalletTests/Transfer/ViewControllers/SendViewControllerTests.swift
  47. 2
      AlphaWalletTests/ViewControllers/PaymentCoordinatorTests.swift
  48. 17
      AlphaWalletTests/modules/AlphaWalletFoundation/CoinTicker/TickerIdsFetcher/FileTokenEntriesProviderTests.swift
  49. 29
      modules/AlphaWalletCore/AlphaWalletCore/Extensions/Publisher+Extensions.swift
  50. 31
      modules/AlphaWalletCore/AlphaWalletCore/Extensions/Sequence+Extensions.swift
  51. 200
      modules/AlphaWalletENS/AlphaWalletENS/ENS.swift
  52. 30
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Activities/ActivitiesGenerator.swift
  53. 35
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Activities/ActivitiesService.swift
  54. 30
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Activities/ActivityCollection.swift
  55. 113
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Blockies/BlockieGenerator.swift
  56. 1
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Browser/Storage/BookmarksStore.swift
  57. 3
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Browser/Storage/HistoryStore.swift
  58. 166
      modules/AlphaWalletFoundation/AlphaWalletFoundation/CoinTicker/BaseCoinTickersFetcher.swift
  59. 93
      modules/AlphaWalletFoundation/AlphaWalletFoundation/CoinTicker/CoinGecko/CoinGeckoCoinTickerNetworking.swift
  60. 64
      modules/AlphaWalletFoundation/AlphaWalletFoundation/CoinTicker/CoinGecko/CoinGeckoTickerIdsFetcher.swift
  61. 6
      modules/AlphaWalletFoundation/AlphaWalletFoundation/CoinTicker/CoinTickerNetworking.swift
  62. 34
      modules/AlphaWalletFoundation/AlphaWalletFoundation/CoinTicker/CoinTickers.swift
  63. 221
      modules/AlphaWalletFoundation/AlphaWalletFoundation/CoinTicker/CoinTickersStorage.swift
  64. 48
      modules/AlphaWalletFoundation/AlphaWalletFoundation/CoinTicker/TickerIdsFetcher/AlphaWalletRemoteTickerIdsFetcher.swift
  65. 8
      modules/AlphaWalletFoundation/AlphaWalletFoundation/CoinTicker/TickerIdsFetcher/FileTokenEntriesProvider.swift
  66. 7
      modules/AlphaWalletFoundation/AlphaWalletFoundation/CoinTicker/TickerIdsFetcher/InMemoryTickerIdsFetcher.swift
  67. 6
      modules/AlphaWalletFoundation/AlphaWalletFoundation/CoinTicker/TickerIdsFetcher/RemoteTokenEntriesProvider.swift
  68. 29
      modules/AlphaWalletFoundation/AlphaWalletFoundation/CoinTicker/TickerIdsFetcher/TickerIdsFetcher.swift
  69. 2
      modules/AlphaWalletFoundation/AlphaWalletFoundation/CoinTicker/TickerIdsFetcher/TokenEntriesProvider.swift
  70. 2
      modules/AlphaWalletFoundation/AlphaWalletFoundation/CoinTicker/Types/CoinTickersFetcher.swift
  71. 4
      modules/AlphaWalletFoundation/AlphaWalletFoundation/CoinTicker/Types/CoinTickersProvider.swift
  72. 2
      modules/AlphaWalletFoundation/AlphaWalletFoundation/CoinTicker/Types/TokenMappedToTicker.swift
  73. 76
      modules/AlphaWalletFoundation/AlphaWalletFoundation/ENS/DomainResolutionService.swift
  74. 14
      modules/AlphaWalletFoundation/AlphaWalletFoundation/ENS/ENSDelegateImpl.swift
  75. 20
      modules/AlphaWalletFoundation/AlphaWalletFoundation/ENS/EnsResolver.swift
  76. 19
      modules/AlphaWalletFoundation/AlphaWalletFoundation/ENS/EnsReverseResolver.swift
  77. 53
      modules/AlphaWalletFoundation/AlphaWalletFoundation/ENS/GetEnsTextRecord.swift
  78. 31
      modules/AlphaWalletFoundation/AlphaWalletFoundation/ENS/Storage/DomainNameRecordsStorage.swift
  79. 12
      modules/AlphaWalletFoundation/AlphaWalletFoundation/ENS/Types/DomainNameResolutionServiceType.swift
  80. 31
      modules/AlphaWalletFoundation/AlphaWalletFoundation/ENS/UnstoppableDomains/UnstoppableDomainsNetworkProvider.swift
  81. 81
      modules/AlphaWalletFoundation/AlphaWalletFoundation/ENS/UnstoppableDomains/UnstoppableDomainsResolver.swift
  82. 24
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Extensions/Publishers/Promise+Publishers.swift
  83. 17
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Extensions/Publishers/Session+Publishers.swift
  84. 17
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Extensions/Task.swift
  85. 4
      modules/AlphaWalletFoundation/AlphaWalletFoundation/NFT/Enjin/Enjin.swift
  86. 18
      modules/AlphaWalletFoundation/AlphaWalletFoundation/NFT/Enjin/EnjinStorage.swift
  87. 27
      modules/AlphaWalletFoundation/AlphaWalletFoundation/NFT/NFTProvider.swift
  88. 8
      modules/AlphaWalletFoundation/AlphaWalletFoundation/NFT/OpenSea/OpenSea.swift
  89. 30
      modules/AlphaWalletFoundation/AlphaWalletFoundation/RPC/BlockchainProvider.swift
  90. 12
      modules/AlphaWalletFoundation/AlphaWalletFoundation/RPC/CallSmartContractFunction.swift
  91. 36
      modules/AlphaWalletFoundation/AlphaWalletFoundation/SwapToken/NativeSwap/Helpers/SwapOptionsConfigurator.swift
  92. 71
      modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/EventFetcher.swift
  93. 116
      modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/EventForActivitiesFetcher.swift
  94. 50
      modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/EventSource.swift
  95. 45
      modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/EventSourceForActivities.swift
  96. 8
      modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/FetchTokenScriptFiles.swift
  97. 72
      modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/Models/EventsActivityDataStore.swift
  98. 74
      modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/Models/NonActivityEventsDataStore.swift
  99. 74
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/AlphaWalletTokensService.swift
  100. 22
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/ClientSideTokenSourceProvider.swift
  101. Some files were not shown because too many files have changed in this diff Show More

@ -147,7 +147,7 @@ final class AccountsViewModel {
private func buildAccountRowViewModel(wallet: Wallet) -> AnyPublisher<AccountRowViewModel, Never> { private func buildAccountRowViewModel(wallet: Wallet) -> AnyPublisher<AccountRowViewModel, Never> {
let balance = walletBalanceService.walletBalance(for: wallet) let balance = walletBalanceService.walletBalance(for: wallet)
let blockieImage = blockiesGenerator.getBlockieOrEnsAvatarImage(address: wallet.address, fallbackImage: BlockiesImage.defaulBlockieImage) let blockieImage = asFuture { await self.blockiesGenerator.getBlockieOrEnsAvatarImage(address: wallet.address, fallbackImage: BlockiesImage.defaulBlockieImage) }
.handleEvents(receiveOutput: { [analytics] value in .handleEvents(receiveOutput: { [analytics] value in
guard value.isEnsAvatar else { return } guard value.isEnsAvatar else { return }
analytics.setUser(property: Analytics.UserProperties.hasEnsAvatar, value: true) analytics.setUser(property: Analytics.UserProperties.hasEnsAvatar, value: true)

@ -775,41 +775,45 @@ extension ActiveWalletCoordinator: ActivityViewControllerDelegate {
} }
func speedupTransaction(transactionId: String, server: RPCServer, viewController: ActivityViewController) { func speedupTransaction(transactionId: String, server: RPCServer, viewController: ActivityViewController) {
guard let transaction = transactionsDataStore.transaction(withTransactionId: transactionId, forServer: server) else { return } Task { @MainActor in
guard let session = sessionsProvider.session(for: transaction.server) else { return } guard let transaction = await transactionsDataStore.transaction(withTransactionId: transactionId, forServer: server) else { return }
guard let coordinator = ReplaceTransactionCoordinator( guard let session = sessionsProvider.session(for: transaction.server) else { return }
analytics: analytics, guard let coordinator = ReplaceTransactionCoordinator(
domainResolutionService: domainResolutionService, analytics: analytics,
keystore: keystore, domainResolutionService: domainResolutionService,
presentingViewController: viewController, keystore: keystore,
session: session, presentingViewController: viewController,
transaction: transaction, session: session,
mode: .speedup, transaction: transaction,
tokensService: tokensPipeline, mode: .speedup,
networkService: networkService) else { return } tokensService: tokensPipeline,
networkService: networkService) else { return }
coordinator.delegate = self coordinator.delegate = self
coordinator.start() coordinator.start()
addCoordinator(coordinator) addCoordinator(coordinator)
}
} }
func cancelTransaction(transactionId: String, server: RPCServer, viewController: ActivityViewController) { func cancelTransaction(transactionId: String, server: RPCServer, viewController: ActivityViewController) {
guard let transaction = transactionsDataStore.transaction(withTransactionId: transactionId, forServer: server) else { return } Task { @MainActor in
guard let session = sessionsProvider.session(for: transaction.server) else { return } guard let transaction = await transactionsDataStore.transaction(withTransactionId: transactionId, forServer: server) else { return }
guard let coordinator = ReplaceTransactionCoordinator( guard let session = sessionsProvider.session(for: transaction.server) else { return }
analytics: analytics, guard let coordinator = ReplaceTransactionCoordinator(
domainResolutionService: domainResolutionService, analytics: analytics,
keystore: keystore, domainResolutionService: domainResolutionService,
presentingViewController: viewController, keystore: keystore,
session: session, presentingViewController: viewController,
transaction: transaction, session: session,
mode: .cancel, transaction: transaction,
tokensService: tokensPipeline, mode: .cancel,
networkService: networkService) else { return } tokensService: tokensPipeline,
networkService: networkService) else { return }
coordinator.delegate = self coordinator.delegate = self
coordinator.start() coordinator.start()
addCoordinator(coordinator) addCoordinator(coordinator)
}
} }
func goToTransaction(viewController: ActivityViewController) { func goToTransaction(viewController: ActivityViewController) {

@ -56,7 +56,7 @@ class ActivitiesCoordinator: NSObject, Coordinator {
let viewModel = ActivitiesViewModel(collection: .init()) let viewModel = ActivitiesViewModel(collection: .init())
let controller = ActivitiesViewController(analytics: analytics, keystore: keystore, wallet: wallet, viewModel: viewModel, sessionsProvider: sessionsProvider, assetDefinitionStore: assetDefinitionStore, tokenImageFetcher: tokenImageFetcher) let controller = ActivitiesViewController(analytics: analytics, keystore: keystore, wallet: wallet, viewModel: viewModel, sessionsProvider: sessionsProvider, assetDefinitionStore: assetDefinitionStore, tokenImageFetcher: tokenImageFetcher)
controller.delegate = self controller.delegate = self
return controller return controller
} }

@ -21,14 +21,7 @@ class ActivitiesViewController: UIViewController {
private var activitiesView: ActivitiesView private var activitiesView: ActivitiesView
weak var delegate: ActivitiesViewControllerDelegate? weak var delegate: ActivitiesViewControllerDelegate?
init(analytics: AnalyticsLogger, init(analytics: AnalyticsLogger, keystore: Keystore, wallet: Wallet, viewModel: ActivitiesViewModel, sessionsProvider: SessionsProvider, assetDefinitionStore: AssetDefinitionStore, tokenImageFetcher: TokenImageFetcher) {
keystore: Keystore,
wallet: Wallet,
viewModel: ActivitiesViewModel,
sessionsProvider: SessionsProvider,
assetDefinitionStore: AssetDefinitionStore,
tokenImageFetcher: TokenImageFetcher) {
self.viewModel = viewModel self.viewModel = viewModel
searchController = UISearchController(searchResultsController: nil) searchController = UISearchController(searchResultsController: nil)
activitiesView = ActivitiesView( activitiesView = ActivitiesView(
@ -159,7 +152,7 @@ extension ActivitiesViewController {
private func configureSearchBarOnce() { private func configureSearchBarOnce() {
guard !isSearchBarConfigured else { return } guard !isSearchBarConfigured else { return }
isSearchBarConfigured = true isSearchBarConfigured = true
UISearchBar.configure(searchBar: searchController.searchBar) UISearchBar.configure(searchBar: searchController.searchBar)
} }
} }

@ -79,10 +79,10 @@ extension QRCodeResolutionCoordinator: ScanQRCodeCoordinatorDelegate {
resolveScanResult(result, decodedValue: decodedValue) resolveScanResult(result, decodedValue: decodedValue)
} }
private func availableActions(forContract contract: AlphaWallet.Address) -> [ScanQRCodeAction] { private func availableActions(forContract contract: AlphaWallet.Address) async -> [ScanQRCodeAction] {
switch usage { switch usage {
case .all(let tokensDataStore, _): case .all(let tokensDataStore, _):
let isTokenFound = tokensDataStore.token(for: contract, server: .main) != nil let isTokenFound = await tokensDataStore.token(for: contract, server: .main) != nil
if isTokenFound { if isTokenFound {
return [.sendToAddress, .watchWallet, .openInEtherscan] return [.sendToAddress, .watchWallet, .openInEtherscan]
} else { } else {
@ -104,7 +104,7 @@ extension QRCodeResolutionCoordinator: ScanQRCodeCoordinatorDelegate {
switch value { switch value {
case .address(let contract): case .address(let contract):
guard supportedResolutions.contains(.address) else { return } guard supportedResolutions.contains(.address) else { return }
let actions = availableActions(forContract: contract) let actions = await availableActions(forContract: contract)
if actions.count == 1 { if actions.count == 1 {
delegate.coordinator(self, didResolve: .address(address: contract, action: actions[0])) delegate.coordinator(self, didResolve: .address(address: contract, action: actions[0]))
} else { } else {

@ -16,15 +16,7 @@ public class ConfigureApp: Initializer {
Attestation.isLoggingEnabled = true Attestation.isLoggingEnabled = true
Attestation.callSmartContract = { chainId, contract, functionName, abiString, parameters in Attestation.callSmartContract = { chainId, contract, functionName, abiString, parameters in
return try await withCheckedThrowingContinuation { continuation in return try await callSmartContractAsync(withServer: RPCServer(chainID: chainId), contract: contract, functionName: functionName, abiString: abiString, parameters: parameters)
firstly {
callSmartContract(withServer: RPCServer(chainID: chainId), contract: contract, functionName: functionName, abiString: abiString, parameters: parameters)
}.done { result in
continuation.resume(returning: result)
}.catch {
continuation.resume(throwing: $0)
}
}
} }
} }
} }

@ -6,10 +6,11 @@
// //
import Foundation import Foundation
import Alamofire
import AlphaWalletFoundation
import Combine import Combine
import AlphaWalletCore
import AlphaWalletFoundation
import AlphaWalletOpenSea import AlphaWalletOpenSea
import Alamofire
protocol MediaContentNetworking { protocol MediaContentNetworking {
func dataTaskPublisher(_ request: URLRequestConvertible) -> AnyPublisher<URLRequest.Response, SessionTaskError> func dataTaskPublisher(_ request: URLRequestConvertible) -> AnyPublisher<URLRequest.Response, SessionTaskError>

@ -136,23 +136,23 @@ class AddressOrEnsNameLabel: UILabel {
let promise = Promise<BlockieAndAddressOrEnsResolution> { seal in let promise = Promise<BlockieAndAddressOrEnsResolution> { seal in
if let address = AlphaWallet.Address(string: value) { if let address = AlphaWallet.Address(string: value) {
cancelable?.cancel() Task { @MainActor in
cancelable = domainResolutionService.resolveEnsAndBlockie(address: address, server: server) do {
.sink(receiveCompletion: { _ in let blockieAndResolution = try await domainResolutionService.resolveEnsAndBlockie(address: address, server: server)
seal.fulfill((nil, .resolved(.none))) seal.fulfill(blockieAndResolution)
}, receiveValue: { value in } catch {
self.clearCurrentlyResolvingIf(value: valueArg) self.clearCurrentlyResolvingIf(value: valueArg)
seal.fulfill(value) }
}) }
} else if value.contains(".") { } else if value.contains(".") {
cancelable?.cancel() Task { @MainActor in
cancelable = domainResolutionService.resolveAddressAndBlockie(string: value) do {
.sink(receiveCompletion: { _ in let blockieAndResolution = try await domainResolutionService.resolveAddressAndBlockie(string: value)
seal.fulfill((nil, .resolved(.none))) seal.fulfill(blockieAndResolution)
}, receiveValue: { value in } catch {
self.clearCurrentlyResolvingIf(value: valueArg) self.clearCurrentlyResolvingIf(value: valueArg)
seal.fulfill(value) }
}) }
} else { } else {
seal.fulfill((nil, .resolved(.none))) seal.fulfill((nil, .resolved(.none)))
} }

@ -305,20 +305,19 @@ final class ImportMagicLinkController {
} }
private func makeTokenHolder(_ bytes32Tokens: [String], _ contractAddress: AlphaWallet.Address) { private func makeTokenHolder(_ bytes32Tokens: [String], _ contractAddress: AlphaWallet.Address) {
assetDefinitionStore.fetchXML(forContract: contractAddress, server: server, useCacheAndFetch: true) { [weak self, session] _ in Task { @MainActor in
guard let strongSelf = self else { return } _ = await assetDefinitionStore.fetchXMLAsync(forContract: contractAddress, server: server, useCacheAndFetch: true)
func makeTokenHolder(name: String, symbol: String, type: TokenType? = nil) async {
func makeTokenHolder(name: String, symbol: String, type: TokenType? = nil) { await makeTokenHolderImpl(name: name, symbol: symbol, type: type, bytes32Tokens: bytes32Tokens, contractAddress: contractAddress)
strongSelf.makeTokenHolderImpl(name: name, symbol: symbol, type: type, bytes32Tokens: bytes32Tokens, contractAddress: contractAddress) updateTokenFields()
strongSelf.updateTokenFields()
} }
if let existingToken = strongSelf.tokensService.tokenViewModel(for: contractAddress, server: strongSelf.server) { if let existingToken = await tokensService.tokenViewModel(for: contractAddress, server: server) {
let name = XMLHandler(token: existingToken, assetDefinitionStore: strongSelf.assetDefinitionStore).getLabel(fallback: existingToken.name) let name = XMLHandler(token: existingToken, assetDefinitionStore: assetDefinitionStore).getLabel(fallback: existingToken.name)
makeTokenHolder(name: name, symbol: existingToken.symbol) await makeTokenHolder(name: name, symbol: existingToken.symbol)
} else { } else {
let localizedTokenTypeName = R.string.localizable.tokensTitlecase() let localizedTokenTypeName = R.string.localizable.tokensTitlecase()
makeTokenHolder(name: localizedTokenTypeName, symbol: "") await makeTokenHolder(name: localizedTokenTypeName, symbol: "")
let getContractName = session.tokenProvider.getContractName(for: contractAddress) let getContractName = session.tokenProvider.getContractName(for: contractAddress)
let getContractSymbol = session.tokenProvider.getContractSymbol(for: contractAddress) let getContractSymbol = session.tokenProvider.getContractSymbol(for: contractAddress)
@ -328,15 +327,21 @@ final class ImportMagicLinkController {
.sinkAsync(receiveCompletion: { _ in .sinkAsync(receiveCompletion: { _ in
//no-op //no-op
}, receiveValue: { name, symbol, type in }, receiveValue: { name, symbol, type in
makeTokenHolder(name: name, symbol: symbol, type: type) Task { @MainActor in
await makeTokenHolder(name: name, symbol: symbol, type: type)
}
}) })
} }
} }
} }
private func makeTokenHolderImpl(name: String, symbol: String, type: TokenType? = nil, bytes32Tokens: [String], contractAddress: AlphaWallet.Address) { private func makeTokenHolderImpl(name: String, symbol: String, type: TokenType? = nil, bytes32Tokens: [String], contractAddress: AlphaWallet.Address) async {
//TODO pass in the wallet instead //TODO pass in the wallet instead
guard let tokenType = type ?? (tokensService.tokenViewModel(for: contractAddress, server: server)?.type) else { return } var tokenType1: TokenType? = type
if tokenType1 == nil {
tokenType1 = await tokensService.tokenViewModel(for: contractAddress, server: server)?.type
}
guard let tokenType = tokenType1 else { return }
var tokens = [TokenScript.Token]() var tokens = [TokenScript.Token]()
let xmlHandler = XMLHandler(contract: contractAddress, tokenType: tokenType, assetDefinitionStore: assetDefinitionStore) let xmlHandler = XMLHandler(contract: contractAddress, tokenType: tokenType, assetDefinitionStore: assetDefinitionStore)
for i in 0..<bytes32Tokens.count { for i in 0..<bytes32Tokens.count {

@ -163,59 +163,65 @@ final class SwapTokensViewModel: NSObject {
} }
private func buildMaxFungibleAmount(for trigger: AnyPublisher<Void, Never>) -> AnyPublisher<AmountTextFieldViewModel.FungibleAmount, Never> { private func buildMaxFungibleAmount(for trigger: AnyPublisher<Void, Never>) -> AnyPublisher<AmountTextFieldViewModel.FungibleAmount, Never> {
trigger.compactMap { [tokensPipeline, configurator] _ -> AmountTextFieldViewModel.FungibleAmount? in trigger.flatMap { [tokensPipeline, configurator] _ in
let token = configurator.swapPair.from asFuture {
switch token.type { let token = configurator.swapPair.from
case .nativeCryptocurrency: switch token.type {
guard let balance = tokensPipeline.tokenViewModel(for: token)?.balance else { return nil } case .nativeCryptocurrency:
guard let balance = await tokensPipeline.tokenViewModel(for: token)?.balance else { return nil }
return Decimal(bigUInt: BigUInt(balance.value), decimals: token.decimals).flatMap { AmountTextFieldViewModel.FungibleAmount.allFunds($0.doubleValue) }
case .erc20: return Decimal(bigUInt: BigUInt(balance.value), decimals: token.decimals).flatMap { AmountTextFieldViewModel.FungibleAmount.allFunds($0.doubleValue) }
guard let balance = tokensPipeline.tokenViewModel(for: token)?.balance else { return nil } case .erc20:
guard let balance = await tokensPipeline.tokenViewModel(for: token)?.balance else { return nil }
return Decimal(bigUInt: BigUInt(balance.value), decimals: token.decimals).flatMap { AmountTextFieldViewModel.FungibleAmount.allFunds($0.doubleValue) }
case .erc1155, .erc721, .erc875, .erc721ForTickets: return Decimal(bigUInt: BigUInt(balance.value), decimals: token.decimals).flatMap { AmountTextFieldViewModel.FungibleAmount.allFunds($0.doubleValue) }
return nil case .erc1155, .erc721, .erc875, .erc721ForTickets:
return nil
}
} }
}.eraseToAnyPublisher() }.compactMap { $0 }.eraseToAnyPublisher()
} }
private func buildBigUIntValue(amount: AnyPublisher<FungibleAmount, Never>) -> AnyPublisher<BigUInt?, Never> { private func buildBigUIntValue(amount: AnyPublisher<FungibleAmount, Never>) -> AnyPublisher<BigUInt?, Never> {
return Publishers.CombineLatest(amount, activeSession.combineLatest(swapPair)) return Publishers.CombineLatest(amount, activeSession.combineLatest(swapPair))
.map { amount, sessionAndSwapPair -> BigUInt? in .flatMap { amount, sessionAndSwapPair in
switch amount { asFuture {
case .amount(let amount): switch amount {
return Decimal(amount).toBigUInt(decimals: sessionAndSwapPair.1.from.decimals) case .amount(let amount):
case .allFunds: return Decimal(amount).toBigUInt(decimals: sessionAndSwapPair.1.from.decimals)
guard let balance: BalanceViewModel = self.balance(for: sessionAndSwapPair.1.from, session: sessionAndSwapPair.0) else { case .allFunds:
guard let balance: BalanceViewModel = await self.balance(for: sessionAndSwapPair.1.from, session: sessionAndSwapPair.0) else {
return nil
}
return balance.value
case .notSet:
return nil return nil
} }
return balance.value
case .notSet:
return nil
} }
}.eraseToAnyPublisher() }.eraseToAnyPublisher()
} }
private func amountValidation(amountToSwap: AnyPublisher<FungibleAmount, Never>, useGreaterThanZeroValidation: Bool = true) -> AnyPublisher<AmountTextField.ErrorState, Never> { private func amountValidation(amountToSwap: AnyPublisher<FungibleAmount, Never>, useGreaterThanZeroValidation: Bool = true) -> AnyPublisher<AmountTextField.ErrorState, Never> {
return Publishers.CombineLatest(amountToSwap, activeSession.combineLatest(swapPair)) return Publishers.CombineLatest(amountToSwap, activeSession.combineLatest(swapPair))
.map { amountToSwap, sessionAndSwapPair -> AmountTextField.ErrorState in .flatMap { amountToSwap, sessionAndSwapPair in
let token = sessionAndSwapPair.1.from asFuture {
guard let balance: BalanceViewModel = self.balance(for: token, session: sessionAndSwapPair.0) else { let token = sessionAndSwapPair.1.from
return .error guard let balance: BalanceViewModel = await self.balance(for: token, session: sessionAndSwapPair.0) else {
} return .error
}
switch amountToSwap { switch amountToSwap {
case .notSet: case .notSet:
return .error
case .allFunds:
return .none
case .amount(let amount):
let greaterThanZero = useGreaterThanZeroValidation ? self.checkIfGreaterThanZero(for: token) : false
guard greaterThanZero ? amount > 0 : true else {
return .error return .error
case .allFunds:
return .none
case .amount(let amount):
let greaterThanZero = useGreaterThanZeroValidation ? self.checkIfGreaterThanZero(for: token) : false
guard greaterThanZero ? amount > 0 : true else {
return .error
}
return balance.valueDecimal.doubleValue >= amount ? .none : .error
} }
return balance.valueDecimal.doubleValue >= amount ? .none : .error
} }
}.eraseToAnyPublisher() }.eraseToAnyPublisher()
} }
@ -236,8 +242,8 @@ final class SwapTokensViewModel: NSObject {
} }
} }
private func balance(for token: Token, session: WalletSession) -> BalanceViewModel? { private func balance(for token: Token, session: WalletSession) async -> BalanceViewModel? {
return tokensPipeline.tokenViewModel(for: token) return await tokensPipeline.tokenViewModel(for: token)
.flatMap { $0.balance } .flatMap { $0.balance }
} }
} }

@ -48,19 +48,26 @@ class SwapOptionsViewModel {
input.selection input.selection
.sink { [configurator] indexPath in .sink { [configurator] indexPath in
let server = configurator.sessions[indexPath.row].server let server = configurator.sessions[indexPath.row].server
guard configurator.isAvailable(server: server) else { return } Task { @MainActor in
configurator.set(server: server) guard await configurator.isAvailable(server: server) else { return }
configurator.set(server: server)
}
}.store(in: &cancelable) }.store(in: &cancelable)
let sessions = Publishers.CombineLatest(configurator.$sessions, configurator.$server) let sessions = Publishers.CombineLatest(configurator.$sessions, configurator.$server)
.receive(on: queue) .receive(on: queue)
.map { [weak configurator] sessions, server -> [ServerImageViewModel] in .flatMap { [weak configurator] sessions, server in
guard let configurator = configurator else { return [] } asFuture {
return sessions.map { guard let configurator = configurator else { return [] }
let isAvailableToSelect = configurator.isAvailable(server: $0.server) var models: [ServerImageViewModel] = []
return ServerImageViewModel(server: .server($0.server), isSelected: $0.server == server, isAvailableToSelect: isAvailableToSelect) for each in sessions {
let isAvailableToSelect = await configurator.isAvailable(server: each.server)
let model = ServerImageViewModel(server: .server(each.server), isSelected: each.server == server, isAvailableToSelect: isAvailableToSelect)
models.append(model)
}
return models
} }
}.map { sessions -> SwapOptionsViewModel.SessionsSnapshot in }.map { (sessions: [ServerImageViewModel]) -> SwapOptionsViewModel.SessionsSnapshot in
var snapshot = SwapOptionsViewModel.SessionsSnapshot() var snapshot = SwapOptionsViewModel.SessionsSnapshot()
snapshot.appendSections([.sessions]) snapshot.appendSections([.sessions])
snapshot.appendItems(sessions) snapshot.appendItems(sessions)
@ -81,8 +88,10 @@ class SwapOptionsViewModel {
} }
func set(selectedServer server: RPCServer) { func set(selectedServer server: RPCServer) {
guard configurator.isAvailable(server: server) else { return } Task { @MainActor in
configurator.set(server: server) guard await configurator.isAvailable(server: server) else { return }
configurator.set(server: server)
}
} }
} }

@ -125,7 +125,9 @@ final class NFTCollectionViewModel {
input.map { _ in Loadable<Void, Error>.loading } input.map { _ in Loadable<Void, Error>.loading }
.delay(for: .seconds(1), scheduler: RunLoop.main) .delay(for: .seconds(1), scheduler: RunLoop.main)
.handleEvents(receiveOutput: { [tokensService, token, tokenHolders] _ in .handleEvents(receiveOutput: { [tokensService, token, tokenHolders] _ in
tokenHolders.value = tokensService.tokenHolders(for: token) Task { @MainActor in
tokenHolders.value = await tokensService.tokenHolders(for: token)
}
}) })
.map { _ in Loadable<Void, Error>.done(()) } .map { _ in Loadable<Void, Error>.done(()) }
.eraseToAnyPublisher() .eraseToAnyPublisher()

@ -37,40 +37,12 @@ class FungibleTokenCoordinator: Coordinator {
private let currencyService: CurrencyService private let currencyService: CurrencyService
private let tokenImageFetcher: TokenImageFetcher private let tokenImageFetcher: TokenImageFetcher
private let tokensService: TokensService private let tokensService: TokensService
private lazy var rootViewController: FungibleTokenTabViewController = { //Optional due to order of initialization It's expected to be always initialized
let viewModel = FungibleTokenTabViewModel( private var rootViewController: FungibleTokenTabViewController?
token: token,
session: session,
tokensPipeline: tokensPipeline,
assetDefinitionStore: assetDefinitionStore,
tokensService: tokensService)
let viewController = FungibleTokenTabViewController(viewModel: viewModel)
let viewControlers = viewModel.tabBarItems.map { buildViewController(tabBarItem: $0) }
viewController.set(viewControllers: viewControlers)
viewController.delegate = self
return viewController
}()
var coordinators: [Coordinator] = [] var coordinators: [Coordinator] = []
weak var delegate: FungibleTokenCoordinatorDelegate? weak var delegate: FungibleTokenCoordinatorDelegate?
init(token: Token, init(token: Token, navigationController: UINavigationController, session: WalletSession, keystore: Keystore, assetDefinitionStore: AssetDefinitionStore, analytics: AnalyticsLogger, tokenActionsProvider: SupportedTokenActionsProvider, coinTickersProvider: CoinTickersProvider, activitiesService: ActivitiesServiceType, alertService: PriceAlertServiceType, tokensPipeline: TokensProcessingPipeline, sessionsProvider: SessionsProvider, currencyService: CurrencyService, tokenImageFetcher: TokenImageFetcher, tokensService: TokensService) async {
navigationController: UINavigationController,
session: WalletSession,
keystore: Keystore,
assetDefinitionStore: AssetDefinitionStore,
analytics: AnalyticsLogger,
tokenActionsProvider: SupportedTokenActionsProvider,
coinTickersProvider: CoinTickersProvider,
activitiesService: ActivitiesServiceType,
alertService: PriceAlertServiceType,
tokensPipeline: TokensProcessingPipeline,
sessionsProvider: SessionsProvider,
currencyService: CurrencyService,
tokenImageFetcher: TokenImageFetcher,
tokensService: TokensService) {
self.tokensService = tokensService self.tokensService = tokensService
self.tokenImageFetcher = tokenImageFetcher self.tokenImageFetcher = tokenImageFetcher
self.currencyService = currencyService self.currencyService = currencyService
@ -86,27 +58,31 @@ class FungibleTokenCoordinator: Coordinator {
self.coinTickersProvider = coinTickersProvider self.coinTickersProvider = coinTickersProvider
self.activitiesService = activitiesService self.activitiesService = activitiesService
self.alertService = alertService self.alertService = alertService
self.rootViewController = await Self.createRootViewController(token: token, session: session, assetDefinitionStore: assetDefinitionStore, keystore: keystore, sessionsProvider: sessionsProvider, tokenImageFetcher: tokenImageFetcher, activitiesService: activitiesService, analytics: analytics, tokensPipeline: tokensPipeline, tokensService: tokensService, coinTickersProvider: coinTickersProvider, tokenActionsProvider: tokenActionsProvider, currencyService: currencyService, alertService: alertService, delegate: self, cancelable: &cancelable)
Task { @MainActor in
self.rootViewController?.delegate = self
}
} }
func start() { func start() {
rootViewController.hidesBottomBarWhenPushed = true rootViewController!.hidesBottomBarWhenPushed = true
rootViewController.navigationItem.largeTitleDisplayMode = .never rootViewController!.navigationItem.largeTitleDisplayMode = .never
navigationController.pushViewController(rootViewController, animated: true) navigationController.pushViewController(rootViewController!, animated: true)
} }
private func buildViewController(tabBarItem: FungibleTokenTabViewModel.TabBarItem) -> UIViewController { private static func buildViewController(tabBarItem: FungibleTokenTabViewModel.TabBarItem, analytics: AnalyticsLogger, keystore: Keystore, sessionsProvider: SessionsProvider, assetDefinitionStore: AssetDefinitionStore, tokenImageFetcher: TokenImageFetcher, activitiesService: ActivitiesServiceType, token: Token, coinTickersProvider: CoinTickersProvider, tokensPipeline: TokensProcessingPipeline, session: WalletSession, tokenActionsProvider: SupportedTokenActionsProvider, currencyService: CurrencyService, alertService: PriceAlertServiceType, delegate: FungibleTokenDetailsViewControllerDelegate & ActivitiesViewControllerDelegate & PriceAlertsViewControllerDelegate, cancelable: inout Set<AnyCancellable>) -> UIViewController {
switch tabBarItem { switch tabBarItem {
case .details: case .details:
return buildDetailsViewController() return buildDetailsViewController(token: token, coinTickersProvider: coinTickersProvider, tokensService: tokensPipeline, session: session, assetDefinitionStore: assetDefinitionStore, tokenActionsProvider: tokenActionsProvider, currencyService: currencyService, tokenImageFetcher: tokenImageFetcher, delegate: delegate)
case .activities: case .activities:
return buildActivitiesViewController() return buildActivitiesViewController(analytics: analytics, keystore: keystore, session: session, sessionsProvider: sessionsProvider, assetDefinitionStore: assetDefinitionStore, tokenImageFetcher: tokenImageFetcher, activitiesService: activitiesService, delegate: delegate, cancelable: &cancelable)
case .alerts: case .alerts:
return buildAlertsViewController() return buildAlertsViewController(alertService: alertService, token: token, delegate: delegate)
} }
} }
private func buildActivitiesViewController() -> UIViewController { private static func buildActivitiesViewController(analytics: AnalyticsLogger, keystore: Keystore, session: WalletSession, sessionsProvider: SessionsProvider, assetDefinitionStore: AssetDefinitionStore, tokenImageFetcher: TokenImageFetcher, activitiesService: ActivitiesServiceType, delegate: ActivitiesViewControllerDelegate, cancelable: inout Set<AnyCancellable>) -> UIViewController {
let viewController = ActivitiesViewController( let viewController = ActivitiesViewController(
analytics: analytics, analytics: analytics,
keystore: keystore, keystore: keystore,
@ -116,7 +92,7 @@ class FungibleTokenCoordinator: Coordinator {
assetDefinitionStore: assetDefinitionStore, assetDefinitionStore: assetDefinitionStore,
tokenImageFetcher: tokenImageFetcher) tokenImageFetcher: tokenImageFetcher)
viewController.delegate = self viewController.delegate = delegate
//FIXME: replace later with moving it to `ActivitiesViewController` //FIXME: replace later with moving it to `ActivitiesViewController`
activitiesService.activitiesPublisher activitiesService.activitiesPublisher
@ -131,19 +107,19 @@ class FungibleTokenCoordinator: Coordinator {
return viewController return viewController
} }
private func buildAlertsViewController() -> UIViewController { private static func buildAlertsViewController(alertService: PriceAlertServiceType, token: Token, delegate: PriceAlertsViewControllerDelegate) -> UIViewController {
let viewModel = PriceAlertsViewModel(alertService: alertService, token: token) let viewModel = PriceAlertsViewModel(alertService: alertService, token: token)
let viewController = PriceAlertsViewController(viewModel: viewModel) let viewController = PriceAlertsViewController(viewModel: viewModel)
viewController.delegate = self viewController.delegate = delegate
return viewController return viewController
} }
private func buildDetailsViewController() -> UIViewController { private static func buildDetailsViewController(token: Token, coinTickersProvider: CoinTickersProvider, tokensService: TokensProcessingPipeline, session: WalletSession, assetDefinitionStore: AssetDefinitionStore, tokenActionsProvider: SupportedTokenActionsProvider, currencyService: CurrencyService, tokenImageFetcher: TokenImageFetcher, delegate: FungibleTokenDetailsViewControllerDelegate) -> UIViewController {
lazy var viewModel = FungibleTokenDetailsViewModel( lazy var viewModel = FungibleTokenDetailsViewModel(
token: token, token: token,
coinTickersProvider: coinTickersProvider, coinTickersProvider: coinTickersProvider,
tokensService: tokensPipeline, tokensService: tokensService,
session: session, session: session,
assetDefinitionStore: assetDefinitionStore, assetDefinitionStore: assetDefinitionStore,
tokenActionsProvider: tokenActionsProvider, tokenActionsProvider: tokenActionsProvider,
@ -151,7 +127,19 @@ class FungibleTokenCoordinator: Coordinator {
tokenImageFetcher: tokenImageFetcher) tokenImageFetcher: tokenImageFetcher)
let viewController = FungibleTokenDetailsViewController(viewModel: viewModel) let viewController = FungibleTokenDetailsViewController(viewModel: viewModel)
viewController.delegate = self viewController.delegate = delegate
return viewController
}
@MainActor private static func createRootViewController(token: Token, session: WalletSession, assetDefinitionStore: AssetDefinitionStore, keystore: Keystore, sessionsProvider: SessionsProvider, tokenImageFetcher: TokenImageFetcher, activitiesService: ActivitiesServiceType, analytics: AnalyticsLogger, tokensPipeline: TokensProcessingPipeline, tokensService: TokensService, coinTickersProvider: CoinTickersProvider, tokenActionsProvider: SupportedTokenActionsProvider, currencyService: CurrencyService, alertService: PriceAlertServiceType, delegate: FungibleTokenTabViewControllerDelegate & FungibleTokenDetailsViewControllerDelegate & ActivitiesViewControllerDelegate & PriceAlertsViewControllerDelegate, cancelable: inout Set<AnyCancellable>) async -> FungibleTokenTabViewController {
let viewModel = await FungibleTokenTabViewModel(token: token, session: session, tokensPipeline: tokensPipeline, assetDefinitionStore: assetDefinitionStore, tokensService: tokensService)
let viewController = FungibleTokenTabViewController(viewModel: viewModel)
let viewControlers = viewModel.tabBarItems.map {
buildViewController(tabBarItem: $0, analytics: analytics, keystore: keystore, sessionsProvider: sessionsProvider, assetDefinitionStore: assetDefinitionStore, tokenImageFetcher: tokenImageFetcher, activitiesService: activitiesService, token: token, coinTickersProvider: coinTickersProvider, tokensPipeline: tokensPipeline, session: session, tokenActionsProvider: tokenActionsProvider, currencyService: currencyService, alertService: alertService, delegate: delegate, cancelable: &cancelable)
}
viewController.set(viewControllers: viewControlers)
viewController.delegate = delegate
return viewController return viewController
} }
@ -255,6 +243,6 @@ extension FungibleTokenCoordinator: FungibleTokenTabViewControllerDelegate {
} }
func open(url: URL) { func open(url: URL) {
delegate?.didPressOpenWebPage(url, in: rootViewController) delegate?.didPressOpenWebPage(url, in: rootViewController!)
} }
} }

@ -117,10 +117,11 @@ extension NewTokenCoordinator: NewTokenViewControllerDelegate {
func didAddToken(ercToken: ErcToken, in viewController: NewTokenViewController) { func didAddToken(ercToken: ErcToken, in viewController: NewTokenViewController) {
guard let session = sessionsProvider.session(for: ercToken.server) else { return } guard let session = sessionsProvider.session(for: ercToken.server) else { return }
let token = session.importToken.importToken(ercToken: ercToken, shouldUpdateBalance: true) Task { @MainActor in
let token = await session.importToken.importToken(ercToken: ercToken, shouldUpdateBalance: true)
delegate?.coordinator(self, didAddToken: token) delegate?.coordinator(self, didAddToken: token)
dismiss() dismiss()
}
} }
func didAddAddress(address: AlphaWallet.Address, in viewController: NewTokenViewController) { func didAddAddress(address: AlphaWallet.Address, in viewController: NewTokenViewController) {

@ -114,26 +114,12 @@ class SingleChainTokenCoordinator: Coordinator {
let activitiesFilterStrategy = token.activitiesFilterStrategy let activitiesFilterStrategy = token.activitiesFilterStrategy
let activitiesService = self.activitiesService.copy(activitiesFilterStrategy: activitiesFilterStrategy, transactionsFilterStrategy: TransactionDataStore.functional.transactionsFilter(for: activitiesFilterStrategy, token: token)) let activitiesService = self.activitiesService.copy(activitiesFilterStrategy: activitiesFilterStrategy, transactionsFilterStrategy: TransactionDataStore.functional.transactionsFilter(for: activitiesFilterStrategy, token: token))
let coordinator = FungibleTokenCoordinator( Task { @MainActor in
token: token, let coordinator = await FungibleTokenCoordinator(token: token, navigationController: navigationController, session: session, keystore: keystore, assetDefinitionStore: assetDefinitionStore, analytics: analytics, tokenActionsProvider: tokenActionsProvider, coinTickersProvider: coinTickersProvider, activitiesService: activitiesService, alertService: alertService, tokensPipeline: tokensPipeline, sessionsProvider: sessionsProvider, currencyService: currencyService, tokenImageFetcher: tokenImageFetcher, tokensService: tokensService)
navigationController: navigationController, addCoordinator(coordinator)
session: session, coordinator.delegate = self
keystore: keystore, coordinator.start()
assetDefinitionStore: assetDefinitionStore, }
analytics: analytics,
tokenActionsProvider: tokenActionsProvider,
coinTickersProvider: coinTickersProvider,
activitiesService: activitiesService,
alertService: alertService,
tokensPipeline: tokensPipeline,
sessionsProvider: sessionsProvider,
currencyService: currencyService,
tokenImageFetcher: tokenImageFetcher,
tokensService: tokensService)
addCoordinator(coordinator)
coordinator.delegate = self
coordinator.start()
} }
private func showTokenInstanceActionView(forAction action: TokenInstanceAction, fungibleTokenObject token: Token, navigationController: UINavigationController) { private func showTokenInstanceActionView(forAction action: TokenInstanceAction, fungibleTokenObject token: Token, navigationController: UINavigationController) {

@ -92,7 +92,7 @@ class SelectTokenViewController: UIViewController {
} }
navigationItem.title = viewState.title navigationItem.title = viewState.title
}.store(in: &cancellable) }.store(in: &cancellable)
} }
} }
extension SelectTokenViewController: StatefulViewController { extension SelectTokenViewController: StatefulViewController {
@ -105,8 +105,10 @@ extension SelectTokenViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true) tableView.deselectRow(at: indexPath, animated: true)
guard let token = viewModel.selectTokenViewModel(viewModel: dataSource.item(at: indexPath)) else { return } Task { @MainActor in
delegate?.controller(self, didSelectToken: token) guard let token = await viewModel.selectTokenViewModel(viewModel: dataSource.item(at: indexPath)) else { return }
delegate?.controller(self, didSelectToken: token)
}
} }
} }

@ -62,7 +62,11 @@ final class TokensViewController: UIViewController {
let control = UIRefreshControl() let control = UIRefreshControl()
return control return control
}() }()
private (set) lazy var blockieImageView: BlockieImageView = BlockieImageView(viewSize: .init(width: 44, height: 44), imageSize: .init(width: 24, height: 24)) private (set) lazy var blockieImageView: BlockieImageView = {
let view = BlockieImageView(viewSize: .init(width: 44, height: 44), imageSize: .init(width: 24, height: 24))
view.isHidden = true
return view
}()
private lazy var searchController: UISearchController = { private lazy var searchController: UISearchController = {
let searchController = UISearchController(searchResultsController: nil) let searchController = UISearchController(searchResultsController: nil)
searchController.delegate = self searchController.delegate = self
@ -148,6 +152,7 @@ final class TokensViewController: UIViewController {
private lazy var footerBar: ButtonsBarBackgroundView = { private lazy var footerBar: ButtonsBarBackgroundView = {
let result = ButtonsBarBackgroundView(buttonsBar: buttonsBar, separatorHeight: 0) let result = ButtonsBarBackgroundView(buttonsBar: buttonsBar, separatorHeight: 0)
result.backgroundColor = viewModel.buyButtonFooterBarBackgroundColor result.backgroundColor = viewModel.buyButtonFooterBarBackgroundColor
result.isHidden = true
return result return result
}() }()
@ -257,6 +262,7 @@ final class TokensViewController: UIViewController {
self?.showOrHideBackupWalletViewHolder() self?.showOrHideBackupWalletViewHolder()
walletSummaryView?.configure(viewModel: .init(walletSummary: state.summary, alignment: .center)) walletSummaryView?.configure(viewModel: .init(walletSummary: state.summary, alignment: .center))
blockieImageView.isHidden = false
blockieImageView.set(blockieImage: state.blockiesImage) blockieImageView.set(blockieImage: state.blockiesImage)
navigationItem.title = state.title navigationItem.title = state.title

@ -119,12 +119,14 @@ final class AddHideTokensViewModel {
} }
func add(token: Token) { func add(token: Token) {
guard let token = tokenCollection.tokenViewModel(for: token) else { return } Task { @MainActor in
if !tokens.contains(token) { guard let token = await tokenCollection.tokenViewModel(for: token) else { return }
tokens.append(token) if !tokens.contains(token) {
} tokens.append(token)
}
addToken.send(()) addToken.send(())
}
} }
private func mark(token: TokenViewModel, isHidden: Bool) { private func mark(token: TokenViewModel, isHidden: Bool) {
@ -156,19 +158,23 @@ final class AddHideTokensViewModel {
let publisher = session.importToken let publisher = session.importToken
.importToken(for: token.contractAddress, onlyIfThereIsABalance: false) .importToken(for: token.contractAddress, onlyIfThereIsABalance: false)
.flatMap { [tokensService, tokenCollection] _token -> AnyPublisher<TokenWithIndexToInsert?, ImportToken.ImportTokenError> in .flatMap { [tokensService, tokenCollection] _token -> AnyPublisher<TokenWithIndexToInsert?, ImportToken.ImportTokenError> in
guard let token = tokenCollection.tokenViewModel(for: _token) else { asFutureThrowable {
return .fail(ImportToken.ImportTokenError.notContractOrFailed(.delegateContracts([_token.addressAndRPCServer]))) guard let token = await tokenCollection.tokenViewModel(for: _token) else {
} throw ImportToken.ImportTokenError.notContractOrFailed(.delegateContracts([_token.addressAndRPCServer]))
self.popularTokens.remove(at: indexPath.row) }
self.displayedTokens.append(token) self.popularTokens.remove(at: indexPath.row)
self.displayedTokens.append(token)
if let sectionIndex = self.sections.firstIndex(of: .displayedTokens) { if let sectionIndex = self.sections.firstIndex(of: .displayedTokens) {
tokensService.mark(token: token, isHidden: false) tokensService.mark(token: token, isHidden: false)
return .just((token, IndexPath(row: max(0, self.displayedTokens.count - 1), section: Int(sectionIndex)))) return (token, IndexPath(row: max(0, self.displayedTokens.count - 1), section: Int(sectionIndex)))
} }
return .just(nil) return nil
}
.mapError { ImportToken.ImportTokenError.internal(error: $0) }
.eraseToAnyPublisher()
}.eraseToAnyPublisher() }.eraseToAnyPublisher()
return .publisher(publisher) return .publisher(publisher)

@ -54,15 +54,7 @@ final class FungibleTokenDetailsViewModel {
var wallet: Wallet { session.account } var wallet: Wallet { session.account }
init(token: Token, init(token: Token, coinTickersProvider: CoinTickersProvider, tokensService: TokensProcessingPipeline, session: WalletSession, assetDefinitionStore: AssetDefinitionStore, tokenActionsProvider: SupportedTokenActionsProvider, currencyService: CurrencyService, tokenImageFetcher: TokenImageFetcher) {
coinTickersProvider: CoinTickersProvider,
tokensService: TokensProcessingPipeline,
session: WalletSession,
assetDefinitionStore: AssetDefinitionStore,
tokenActionsProvider: SupportedTokenActionsProvider,
currencyService: CurrencyService,
tokenImageFetcher: TokenImageFetcher) {
self.tokenImageFetcher = tokenImageFetcher self.tokenImageFetcher = tokenImageFetcher
self.currencyService = currencyService self.currencyService = currencyService
self.tokenActionsProvider = tokenActionsProvider self.tokenActionsProvider = tokenActionsProvider
@ -75,7 +67,9 @@ final class FungibleTokenDetailsViewModel {
func transform(input: FungibleTokenDetailsViewModelInput) -> FungibleTokenDetailsViewModelOutput { func transform(input: FungibleTokenDetailsViewModelInput) -> FungibleTokenDetailsViewModelOutput {
input.willAppear.flatMapLatest { [coinTickersProvider, token, currencyService] _ in input.willAppear.flatMapLatest { [coinTickersProvider, token, currencyService] _ in
coinTickersProvider.chartHistories(for: .init(token: token), currency: currencyService.currency) asFuture {
await coinTickersProvider.chartHistories(for: .init(token: token), currency: currencyService.currency)
}
}.assign(to: \.value, on: chartHistoriesSubject) }.assign(to: \.value, on: chartHistoriesSubject)
.store(in: &cancelable) .store(in: &cancelable)
@ -86,21 +80,21 @@ final class FungibleTokenDetailsViewModel {
.map { FungibleTokenDetailsViewModel.ViewState(actionButtons: $0, views: $1) } .map { FungibleTokenDetailsViewModel.ViewState(actionButtons: $0, views: $1) }
let action = input.action let action = input.action
.compactMap { self.buildFungibleTokenAction(for: $0) } .flatMap { action in asFuture { await self.buildFungibleTokenAction(for: action) } }.compactMap { $0 }
return .init( return .init(
viewState: viewState.eraseToAnyPublisher(), viewState: viewState.eraseToAnyPublisher(),
action: action.eraseToAnyPublisher()) action: action.eraseToAnyPublisher())
} }
private func buildFungibleTokenAction(for action: TokenInstanceAction) -> FungibleTokenAction? { private func buildFungibleTokenAction(for action: TokenInstanceAction) async -> FungibleTokenAction? {
switch action.type { switch action.type {
case .swap: return .swap(swapTokenFlow: .swapToken(token: token)) case .swap: return .swap(swapTokenFlow: .swapToken(token: token))
case .erc20Send: return .erc20Transfer(token: token) case .erc20Send: return .erc20Transfer(token: token)
case .erc20Receive: return .erc20Receive(token: token) case .erc20Receive: return .erc20Receive(token: token)
case .nftRedeem, .nftSell, .nonFungibleTransfer: return nil case .nftRedeem, .nftSell, .nonFungibleTransfer: return nil
case .tokenScript: case .tokenScript:
let fungibleBalance = tokensService.tokenViewModel(for: token)?.balance.value let fungibleBalance = await tokensService.tokenViewModel(for: token)?.balance.value
if let message = actionAdapter.tokenScriptWarningMessage(for: action, fungibleBalance: fungibleBalance) { if let message = actionAdapter.tokenScriptWarningMessage(for: action, fungibleBalance: fungibleBalance) {
guard case .warning(let string) = message else { return nil } guard case .warning(let string) = message else { return nil }
return .display(warning: string) return .display(warning: string)
@ -114,12 +108,12 @@ final class FungibleTokenDetailsViewModel {
private func tokenActionButtonsPublisher() -> AnyPublisher<[ActionButton], Never> { private func tokenActionButtonsPublisher() -> AnyPublisher<[ActionButton], Never> {
let whenTokenHolderHasChanged = tokenHolder.objectWillChange let whenTokenHolderHasChanged = tokenHolder.objectWillChange
.map { [tokensService, token] _ in tokensService.tokenViewModel(for: token) } .flatMap { [tokensService, token] _ in asFuture { await tokensService.tokenViewModel(for: token) } }
.receive(on: RunLoop.main) .receive(on: RunLoop.main)
.eraseToAnyPublisher() .eraseToAnyPublisher()
let whenTokenActionsHasChanged = tokenActionsProvider.objectWillChange let whenTokenActionsHasChanged = tokenActionsProvider.objectWillChange
.map { [tokensService, token] _ in tokensService.tokenViewModel(for: token) } .flatMap { [tokensService, token] _ in asFuture { await tokensService.tokenViewModel(for: token) } }
.receive(on: RunLoop.main) .receive(on: RunLoop.main)
.eraseToAnyPublisher() .eraseToAnyPublisher()
@ -127,13 +121,12 @@ final class FungibleTokenDetailsViewModel {
return Publishers.MergeMany(tokenViewModel, whenTokenHolderHasChanged, whenTokenActionsHasChanged) return Publishers.MergeMany(tokenViewModel, whenTokenHolderHasChanged, whenTokenActionsHasChanged)
.compactMap { _ in self.actionAdapter.availableActions() } .compactMap { _ in self.actionAdapter.availableActions() }
.map { [tokensService, token] actions in .flatMap { [tokensService, token] actions in
let fungibleBalance = tokensService.tokenViewModel(for: token)?.balance.value asFuture {
return actions.map { let fungibleBalance = await tokensService.tokenViewModel(for: token)?.balance.value
ActionButton( return actions.map {
actionType: $0, ActionButton(actionType: $0, name: $0.name, state: self.actionAdapter.state(for: $0, fungibleBalance: fungibleBalance))
name: $0.name, }
state: self.actionAdapter.state(for: $0, fungibleBalance: fungibleBalance))
} }
}.eraseToAnyPublisher() }.eraseToAnyPublisher()
} }

@ -28,19 +28,14 @@ class FungibleTokenTabViewModel {
let session: WalletSession let session: WalletSession
let tabBarItems: [TabBarItem] let tabBarItems: [TabBarItem]
init(token: Token, init(token: Token, session: WalletSession, tokensPipeline: TokensProcessingPipeline, assetDefinitionStore: AssetDefinitionStore, tokensService: TokensService) async {
session: WalletSession,
tokensPipeline: TokensProcessingPipeline,
assetDefinitionStore: AssetDefinitionStore,
tokensService: TokensService) {
self.tokensService = tokensService self.tokensService = tokensService
self.tokensPipeline = tokensPipeline self.tokensPipeline = tokensPipeline
self.assetDefinitionStore = assetDefinitionStore self.assetDefinitionStore = assetDefinitionStore
self.token = token self.token = token
self.session = session self.session = session
let hasTicker = tokensPipeline.tokenViewModel(for: token)?.balance.ticker != nil let hasTicker = await tokensPipeline.tokenViewModel(for: token)?.balance.ticker != nil
if Features.current.isAvailable(.isAlertsEnabled) && hasTicker { if Features.current.isAvailable(.isAlertsEnabled) && hasTicker {
tabBarItems = [.details, .activities, .alerts] tabBarItems = [.details, .activities, .alerts]

@ -47,9 +47,9 @@ final class SelectTokenViewModel {
} }
} }
func selectTokenViewModel(viewModel: SelectTokenViewModel.ViewModelType) -> Token? { func selectTokenViewModel(viewModel: SelectTokenViewModel.ViewModelType) async -> Token? {
let value = viewModel.asTokenIdentifiable let value = viewModel.asTokenIdentifiable
return tokensService.token(for: value.contractAddress, server: value.server) return await tokensService.token(for: value.contractAddress, server: value.server)
} }
func transform(input: SelectTokenViewModelInput) -> SelectTokenViewModelOutput { func transform(input: SelectTokenViewModelInput) -> SelectTokenViewModelOutput {

@ -201,6 +201,7 @@ final class TokensViewModel {
}.store(in: &cancellable) }.store(in: &cancellable)
tokensPipeline.tokenViewModels tokensPipeline.tokenViewModels
.prepend([])
.sink { [weak self] tokens in .sink { [weak self] tokens in
self?.tokens = tokens self?.tokens = tokens
self?.reloadData() self?.reloadData()
@ -255,7 +256,9 @@ final class TokensViewModel {
private func blockieImage(input appear: AnyPublisher<Void, Never>) -> AnyPublisher<BlockiesImage, Never> { private func blockieImage(input appear: AnyPublisher<Void, Never>) -> AnyPublisher<BlockiesImage, Never> {
return appear.flatMap { [blockiesGenerator, wallet] _ in return appear.flatMap { [blockiesGenerator, wallet] _ in
blockiesGenerator.getBlockieOrEnsAvatarImage(address: wallet.address, fallbackImage: BlockiesImage.defaulBlockieImage) asFuture {
await blockiesGenerator.getBlockieOrEnsAvatarImage(address: wallet.address, fallbackImage: BlockiesImage.defaulBlockieImage)
}
}.eraseToAnyPublisher() }.eraseToAnyPublisher()
} }
@ -293,30 +296,32 @@ final class TokensViewModel {
} }
private func selection(trigger: AnyPublisher<TokensViewModel.SelectionSource, Never>) -> AnyPublisher<TokenOrAttestation, Never> { private func selection(trigger: AnyPublisher<TokensViewModel.SelectionSource, Never>) -> AnyPublisher<TokenOrAttestation, Never> {
trigger.compactMap { [unowned self, tokensService] source -> TokenOrAttestation? in trigger.flatMap { [unowned self, tokensService] source in
switch source { asFuture { () -> TokenOrAttestation? in
case .gridItem(let indexPath, let isLeftCardSelected): switch source {
switch self.sections[indexPath.section] { case .gridItem(let indexPath, let isLeftCardSelected):
case .collectiblePairs: switch self.sections[indexPath.section] {
let pair = collectiblePairs[indexPath.row] case .collectiblePairs:
guard let viewModel: TokenViewModel = isLeftCardSelected ? pair.left : pair.right else { return nil } let pair = self.collectiblePairs[indexPath.row]
guard let viewModel: TokenViewModel = isLeftCardSelected ? pair.left : pair.right else { return nil }
return tokensService.token(for: viewModel.contractAddress, server: viewModel.server).flatMap { TokenOrAttestation.token($0) }
case .tokens, .activeWalletSession, .filters, .search, .walletSummary: return await tokensService.token(for: viewModel.contractAddress, server: viewModel.server).flatMap { TokenOrAttestation.token($0) }
return nil case .tokens, .activeWalletSession, .filters, .search, .walletSummary:
} return nil
case .cell(let indexPath): }
let tokenOrServer = self.tokenOrServer(at: indexPath) case .cell(let indexPath):
switch (self.sections[indexPath.section], tokenOrServer) { let tokenOrServer = self.tokenOrServer(at: indexPath)
case (.tokens, .token(let viewModel)): switch (self.sections[indexPath.section], tokenOrServer) {
return .token(tokensService.token(for: viewModel.contractAddress, server: viewModel.server)!) case (.tokens, .token(let viewModel)):
case (.tokens, .attestation(let attestation)): return .token(await tokensService.token(for: viewModel.contractAddress, server: viewModel.server)!)
return .attestation(attestation) case (.tokens, .attestation(let attestation)):
case (_, _): return .attestation(attestation)
return nil case (_, _):
return nil
}
} }
} }
}.eraseToAnyPublisher() }.compactMap { $0 }.eraseToAnyPublisher()
} }
private var isFooterHidden: Bool { private var isFooterHidden: Bool {

@ -74,11 +74,13 @@ class TransactionsCoordinator: Coordinator {
func showTransaction(withId transactionId: String, server: RPCServer, inViewController viewController: UIViewController) { func showTransaction(withId transactionId: String, server: RPCServer, inViewController viewController: UIViewController) {
//Quite likely we should have the transaction already //Quite likely we should have the transaction already
//TODO handle when we don't handle the transaction, so we must fetch it. There might not be a simple API call to just fetch a single transaction. Probably have to fetch all transactions in a single block with Etherscan? //TODO handle when we don't handle the transaction, so we must fetch it. There might not be a simple API call to just fetch a single transaction. Probably have to fetch all transactions in a single block with Etherscan?
guard let transaction = transactionsService.transaction(withTransactionId: transactionId, forServer: server) else { return } Task { @MainActor in
if transaction.localizedOperations.count > 1 { guard let transaction = await transactionsService.transaction(withTransactionId: transactionId, forServer: server) else { return }
showTransaction(.group(transaction), inViewController: viewController) if transaction.localizedOperations.count > 1 {
} else { showTransaction(.group(transaction), inViewController: viewController)
showTransaction(.standalone(transaction), inViewController: viewController) } else {
showTransaction(.standalone(transaction), inViewController: viewController)
}
} }
} }
} }

@ -88,13 +88,17 @@ struct TransactionHeaderViewModel {
let token = MultipleChainsTokensDataStore.functional.etherToken(forServer: server) let token = MultipleChainsTokensDataStore.functional.etherToken(forServer: server)
return tokenImageFetcher.image(token: token, size: .s300) return tokenImageFetcher.image(token: token, size: .s300)
} }
guard let token = tokensService.tokenViewModel(for: contractAddress, server: server) else {
return .just(nil)
}
return tokenImageFetcher.image(token: token, size: .s300) return asFuture {
await tokensService.tokenViewModel(for: contractAddress, server: server)
}.flatMap {
if let tokenViewModel = $0 {
return tokenImageFetcher.image(token: tokenViewModel, size: .s300)
} else {
return .just(nil)
}
}.eraseToAnyPublisher()
} }
} }
class TransactionHeaderView: UIView { class TransactionHeaderView: UIView {
@ -106,7 +110,7 @@ class TransactionHeaderView: UIView {
label.font = Fonts.semibold(size: 17) label.font = Fonts.semibold(size: 17)
label.textColor = Configuration.Color.Semantic.defaultHeadlineText label.textColor = Configuration.Color.Semantic.defaultHeadlineText
label.numberOfLines = 0 label.numberOfLines = 0
return label return label
}() }()
@ -144,7 +148,7 @@ class TransactionHeaderView: UIView {
init() { init() {
super.init(frame: .zero) super.init(frame: .zero)
translatesAutoresizingMaskIntoConstraints = false translatesAutoresizingMaskIntoConstraints = false
let stackView = [ let stackView = [
dateLabel, dateLabel,
.spacer(height: 10), .spacer(height: 10),

@ -34,8 +34,10 @@ class RequestViewModel {
} }
func transform(input: RequestViewModelInput) -> RequestViewModelOutput { func transform(input: RequestViewModelInput) -> RequestViewModelOutput {
let ensName = resolveEns() let ensName = asFuture {
let viewState = Publishers.CombineLatest(generateQrCode(), resolveEns()) await self.resolveEns()
}
let viewState = Publishers.CombineLatest(generateQrCode(), ensName)
.map { [account] qrCode, ensName -> RequestViewModel.ViewState in .map { [account] qrCode, ensName -> RequestViewModel.ViewState in
let address = account.address.eip55String let address = account.address.eip55String
return .init(title: R.string.localizable.aSettingsContentsMyWalletAddress(), ensName: ensName, address: address, qrCode: qrCode) return .init(title: R.string.localizable.aSettingsContentsMyWalletAddress(), ensName: ensName, address: address, qrCode: qrCode)
@ -56,12 +58,12 @@ class RequestViewModel {
return .init(copiedToClipboard: copiedToClipboard, viewState: viewState) return .init(copiedToClipboard: copiedToClipboard, viewState: viewState)
} }
private func resolveEns() -> AnyPublisher<String?, Never> { private func resolveEns() async -> String? {
domainResolutionService.reverseResolveDomainName(address: account.address, server: RPCServer.forResolvingDomainNames) do {
.map { ens -> DomainName? in return ens } return try await domainResolutionService.reverseResolveDomainName(address: account.address, server: RPCServer.forResolvingDomainNames)
.replaceError(with: nil) } catch {
.prepend(nil) return nil
.eraseToAnyPublisher() }
} }
private func generateQrCode() -> AnyPublisher<UIImage?, Never> { private func generateQrCode() -> AnyPublisher<UIImage?, Never> {

@ -44,18 +44,20 @@ final class SendViewModel: TransactionTypeSupportable {
/// TokenViewModel updates once we receive a new token, might be when scan qr code or initially. Ask to refresh token balance when received. Only for supported transaction types tokens /// TokenViewModel updates once we receive a new token, might be when scan qr code or initially. Ask to refresh token balance when received. Only for supported transaction types tokens
private lazy var tokenViewModel: AnyPublisher<TokenViewModel?, Never> = { private lazy var tokenViewModel: AnyPublisher<TokenViewModel?, Never> = {
return transactionTypeSubject return transactionTypeSubject
.map { [tokensService] transactionType -> Token? in .flatMap { [tokensService] transactionType in
switch transactionType { asFuture {
case .nativeCryptocurrency: switch transactionType {
//NOTE: looks like we can use transactionType.tokenObject, for nativeCryptocurrency it might contains incorrect contract value case .nativeCryptocurrency:
return tokensService.token(for: transactionType.contract, server: transactionType.server) //NOTE: looks like we can use transactionType.tokenObject, for nativeCryptocurrency it might contains incorrect contract value
case .erc20Token: return await tokensService.token(for: transactionType.contract, server: transactionType.server)
return transactionType.tokenObject case .erc20Token:
case .erc875Token, .erc721Token, .erc721ForTicketToken, .erc1155Token, .prebuilt: return transactionType.tokenObject
return nil case .erc875Token, .erc721Token, .erc721ForTicketToken, .erc1155Token, .prebuilt:
return nil
}
} }
}.removeDuplicates() }.removeDuplicates()
.flatMapLatest { [tokensPipeline, tokensService] token -> AnyPublisher<TokenViewModel?, Never> in .flatMapLatest { [tokensPipeline, tokensService] (token: Token?) -> AnyPublisher<TokenViewModel?, Never> in
guard let token = token else { return .just(nil) } guard let token = token else { return .just(nil) }
tokensService.refreshBalance(updatePolicy: .token(token: token)) tokensService.refreshBalance(updatePolicy: .token(token: token))
@ -157,7 +159,7 @@ final class SendViewModel: TransactionTypeSupportable {
.eraseToAnyPublisher() .eraseToAnyPublisher()
let viewState = Publishers.CombineLatest(tokenViewModel, scanQrCode.mapToVoid().prepend(())) let viewState = Publishers.CombineLatest(tokenViewModel, scanQrCode.mapToVoid().prepend(()))
.map { self.buildViewState(tokenViewModel: $0.0) } .flatMap { tokenViewModel, _ in asFuture { await self.buildViewState(tokenViewModel: tokenViewModel) } }
.eraseToAnyPublisher() .eraseToAnyPublisher()
return .init( return .init(
@ -173,14 +175,14 @@ final class SendViewModel: TransactionTypeSupportable {
private func buildAmountTextFieldState(qrCode: AnyPublisher<Result<TransactionType, CheckEIP681Error>, Never>, allFunds: AnyPublisher<Void, Never>) -> AnyPublisher<AmountTextFieldState, Never> { private func buildAmountTextFieldState(qrCode: AnyPublisher<Result<TransactionType, CheckEIP681Error>, Never>, allFunds: AnyPublisher<Void, Never>) -> AnyPublisher<AmountTextFieldState, Never> {
func buildAmountTextFieldState(for transactionType: TransactionType) -> AmountTextFieldState? { func buildAmountTextFieldState(for transactionType: TransactionType) async -> AmountTextFieldState? {
switch transactionType { switch transactionType {
case .nativeCryptocurrency(let token, _, let amount), .erc20Token(let token, _, let amount): case .nativeCryptocurrency(let token, _, let amount), .erc20Token(let token, _, let amount):
switch amount { switch amount {
case .notSet: case .notSet:
return nil return nil
case .allFunds: case .allFunds:
guard let amount = tokensPipeline.tokenViewModel(for: token)?.balance.valueDecimal else { return nil } guard let amount = await tokensPipeline.tokenViewModel(for: token)?.balance.valueDecimal else { return nil }
return AmountTextFieldState(amount: .allFunds(amount.doubleValue)) return AmountTextFieldState(amount: .allFunds(amount.doubleValue))
case .amount(let amount): case .amount(let amount):
@ -191,36 +193,41 @@ final class SendViewModel: TransactionTypeSupportable {
} }
} }
let initialAmount = Just(transactionType) let initialAmount = asFuture { await buildAmountTextFieldState(for: self.transactionType) }.compactMap { $0 }.eraseToAnyPublisher()
.compactMap { buildAmountTextFieldState(for: $0) }
.eraseToAnyPublisher()
let amountFromQrCode = qrCode let amountFromQrCode: AnyPublisher<AmountTextFieldState, Never> = qrCode
.compactMap { $0.value.flatMap { buildAmountTextFieldState(for: $0) } } .flatMap { result in
asFuture { () -> AmountTextFieldState? in
if let transactionType = result.value {
return await buildAmountTextFieldState(for: transactionType)
} else {
return nil
}
}
}.compactMap { $0 }
.eraseToAnyPublisher() .eraseToAnyPublisher()
let allFundsAmount = allFunds.compactMap { [tokensPipeline, transactionTypeSubject] _ -> AmountTextFieldState? in let allFundsAmount = allFunds.flatMap { [tokensPipeline, transactionTypeSubject] _ in
switch transactionTypeSubject.value { asFuture { () -> AmountTextFieldState? in
case .nativeCryptocurrency(let token, _, _), .erc20Token(let token, _, _): switch transactionTypeSubject.value {
guard let amount = tokensPipeline.tokenViewModel(for: token)?.balance.valueDecimal else { return nil } case .nativeCryptocurrency(let token, _, _), .erc20Token(let token, _, _):
guard let amount = await tokensPipeline.tokenViewModel(for: token)?.balance.valueDecimal else { return nil }
return AmountTextFieldState(amount: .allFunds(amount.doubleValue)) return AmountTextFieldState(amount: .allFunds(amount.doubleValue))
case .erc721ForTicketToken, .erc721Token, .erc875Token, .erc1155Token, .prebuilt: case .erc721ForTicketToken, .erc721Token, .erc875Token, .erc1155Token, .prebuilt:
return nil return nil
}
} }
}.eraseToAnyPublisher() }.compactMap { $0 }.eraseToAnyPublisher()
return Publishers.MergeMany(initialAmount, amountFromQrCode, allFundsAmount) return Publishers.MergeMany(initialAmount, amountFromQrCode, allFundsAmount)
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
private func buildViewState(tokenViewModel: TokenViewModel?) -> SendViewModel.ViewState { private func buildViewState(tokenViewModel: TokenViewModel?) async -> SendViewModel.ViewState {
return .init( let amountStatusLabelHidden = await availableTextHidden
title: title, let state = await SendViewModel.ViewState(title: title, selectCurrencyButtonState: buildSelectCurrencyButtonState(for: tokenViewModel, transactionType: transactionType), amountStatusLabelState: SendViewModel.AmountStatuLabelState(text: availableLabelText, isHidden: amountStatusLabelHidden), rate: tokenViewModel.flatMap { $0.balance.ticker.flatMap { AmountTextFieldViewModel.CurrencyRate(value: $0.price_usd, currency: $0.currency) } } ?? .init(value: nil, currency: .USD), recipientTextFieldState: buildRecipientTextFieldState(for: transactionType))
selectCurrencyButtonState: buildSelectCurrencyButtonState(for: tokenViewModel, transactionType: transactionType), return state
amountStatusLabelState: SendViewModel.AmountStatuLabelState(text: availableLabelText, isHidden: availableTextHidden),
rate: tokenViewModel.flatMap { $0.balance.ticker.flatMap { AmountTextFieldViewModel.CurrencyRate(value: $0.price_usd, currency: $0.currency) } } ?? .init(value: nil, currency: .USD),
recipientTextFieldState: buildRecipientTextFieldState(for: transactionType))
} }
private func buildUnconfirmedTransaction(send: AnyPublisher<Void, Never>) -> AnyPublisher<Result<UnconfirmedTransaction, InputsValidationError>, Never> { private func buildUnconfirmedTransaction(send: AnyPublisher<Void, Never>) -> AnyPublisher<Result<UnconfirmedTransaction, InputsValidationError>, Never> {
@ -286,27 +293,31 @@ final class SendViewModel: TransactionTypeSupportable {
} }
private var availableLabelText: String? { private var availableLabelText: String? {
switch transactionType { get async {
case .nativeCryptocurrency: switch transactionType {
let etherToken: Token = MultipleChainsTokensDataStore.functional.etherToken(forServer: transactionType.server) case .nativeCryptocurrency:
return tokensPipeline.tokenViewModel(for: etherToken) let etherToken: Token = MultipleChainsTokensDataStore.functional.etherToken(forServer: transactionType.server)
.flatMap { return R.string.localizable.sendAvailable($0.balance.amountShort) } return await tokensPipeline.tokenViewModel(for: etherToken)
case .erc20Token(let token, _, _): .flatMap { return R.string.localizable.sendAvailable($0.balance.amountShort) }
return tokensPipeline.tokenViewModel(for: token) case .erc20Token(let token, _, _):
.flatMap { R.string.localizable.sendAvailable("\($0.balance.amountShort) \(transactionType.symbol)") } return await tokensPipeline.tokenViewModel(for: token)
case .erc721ForTicketToken, .erc721Token, .erc875Token, .erc1155Token, .prebuilt: .flatMap { R.string.localizable.sendAvailable("\($0.balance.amountShort) \(transactionType.symbol)") }
return nil case .erc721ForTicketToken, .erc721Token, .erc875Token, .erc1155Token, .prebuilt:
return nil
}
} }
} }
private var availableTextHidden: Bool { private var availableTextHidden: Bool {
switch transactionType { get async {
case .nativeCryptocurrency: switch transactionType {
return false case .nativeCryptocurrency:
case .erc20Token(let token, _, _): return false
return tokensPipeline.tokenViewModel(for: token)?.balance == nil case .erc20Token(let token, _, _):
case .erc721ForTicketToken, .erc721Token, .erc875Token, .erc1155Token, .prebuilt: return await tokensPipeline.tokenViewModel(for: token)?.balance == nil
return true case .erc721ForTicketToken, .erc721Token, .erc875Token, .erc1155Token, .prebuilt:
return true
}
} }
} }

@ -85,8 +85,9 @@ extension TransactionConfirmationViewModel {
.assign(to: \.etherCurrencyRate, on: self, ownership: .weak) .assign(to: \.etherCurrencyRate, on: self, ownership: .weak)
.store(in: &cancellable) .store(in: &cancellable)
let stateChanges = Publishers.CombineLatest3($balanceViewModel, $etherCurrencyRate, recipientResolver.resolveRecipient()).mapToVoid() let resolveRecipient = asFuture { await self.recipientResolver.resolveRecipient() }.eraseToAnyPublisher()
let stateChanges = Publishers.CombineLatest3($balanceViewModel, $etherCurrencyRate, resolveRecipient).mapToVoid()
let viewState = Publishers.Merge(stateChanges, configurator.objectChanges) let viewState = Publishers.Merge(stateChanges, configurator.objectChanges)
.map { _ in .map { _ in
TransactionConfirmationViewModel.ViewState( TransactionConfirmationViewModel.ViewState(
@ -116,7 +117,7 @@ extension TransactionConfirmationViewModel {
let rate = token.balance.ticker.flatMap { CurrencyRate(currency: $0.currency, value: $0.price_usd) } let rate = token.balance.ticker.flatMap { CurrencyRate(currency: $0.currency, value: $0.price_usd) }
let value = SendFungiblesTransactionViewModel.TransactionBalance(balance: balance, newBalance: newBalance, rate: rate) let value = SendFungiblesTransactionViewModel.TransactionBalance(balance: balance, newBalance: newBalance, rate: rate)
return .done(value) return .done(value)
} }
} }

@ -15,7 +15,7 @@ extension TransactionConfirmationViewModel {
@Published private var transactedToken: Loadable<TransactedToken, Error> = .loading @Published private var transactedToken: Loadable<TransactedToken, Error> = .loading
@Published private var balanceViewModel: TransactionBalance = .init(balance: .zero, newBalance: .zero, rate: nil) @Published private var balanceViewModel: TransactionBalance = .init(balance: .zero, newBalance: .zero, rate: nil)
@Published private var etherCurrencyRate: Loadable<CurrencyRate, Error> = .loading @Published private var etherCurrencyRate: Loadable<CurrencyRate, Error> = .loading
private let configurator: TransactionConfigurator private let configurator: TransactionConfigurator
private let recipientResolver: RecipientResolver private let recipientResolver: RecipientResolver
private let session: WalletSession private let session: WalletSession
@ -68,7 +68,8 @@ extension TransactionConfirmationViewModel {
.assign(to: \.etherCurrencyRate, on: self, ownership: .weak) .assign(to: \.etherCurrencyRate, on: self, ownership: .weak)
.store(in: &cancellable) .store(in: &cancellable)
let stateChanges = Publishers.CombineLatest3($balanceViewModel, $etherCurrencyRate, recipientResolver.resolveRecipient()).mapToVoid() let resolveRecipient = asFuture { await self.recipientResolver.resolveRecipient() }.eraseToAnyPublisher()
let stateChanges = Publishers.CombineLatest3($balanceViewModel, $etherCurrencyRate, resolveRecipient).mapToVoid()
let viewState = Publishers.Merge(stateChanges, configurator.objectChanges) let viewState = Publishers.Merge(stateChanges, configurator.objectChanges)
.compactMap { _ -> TransactionConfirmationViewModel.ViewState? in .compactMap { _ -> TransactionConfirmationViewModel.ViewState? in
@ -161,7 +162,7 @@ extension TransactionConfirmationViewModel {
let amount = NumberFormatter.shortCrypto.string(double: amountToSend, minimumFractionDigits: 4, maximumFractionDigits: 8) let amount = NumberFormatter.shortCrypto.string(double: amountToSend, minimumFractionDigits: 4, maximumFractionDigits: 8)
if let rate = balanceViewModel.rate { if let rate = balanceViewModel.rate {
let amountInFiat = NumberFormatter.fiat(currency: rate.currency).string(double: amountToSend * rate.value, minimumFractionDigits: 2, maximumFractionDigits: 6) let amountInFiat = NumberFormatter.fiat(currency: rate.currency).string(double: amountToSend * rate.value, minimumFractionDigits: 2, maximumFractionDigits: 6)
return "\(amount) \(symbol)\(amountInFiat)" return "\(amount) \(symbol)\(amountInFiat)"
} else { } else {
return "\(amount) \(symbol)" return "\(amount) \(symbol)"

@ -24,7 +24,7 @@ extension TransactionConfirmationViewModel {
let confirmButtonViewModel: ConfirmButtonViewModel let confirmButtonViewModel: ConfirmButtonViewModel
var openedSections = Set<Int>() var openedSections = Set<Int>()
init(configurator: TransactionConfigurator, init(configurator: TransactionConfigurator,
recipientResolver: RecipientResolver, recipientResolver: RecipientResolver,
tokensService: TokensProcessingPipeline) { tokensService: TokensProcessingPipeline) {
@ -47,7 +47,8 @@ extension TransactionConfirmationViewModel {
.assign(to: \.etherCurrencyRate, on: self, ownership: .weak) .assign(to: \.etherCurrencyRate, on: self, ownership: .weak)
.store(in: &cancellable) .store(in: &cancellable)
let stateChanges = Publishers.CombineLatest($etherCurrencyRate, recipientResolver.resolveRecipient()).mapToVoid() let resolveRecipient = asFuture { await self.recipientResolver.resolveRecipient() }.eraseToAnyPublisher()
let stateChanges = Publishers.CombineLatest($etherCurrencyRate, resolveRecipient).mapToVoid()
let viewState = Publishers.Merge(stateChanges, configurator.objectChanges) let viewState = Publishers.Merge(stateChanges, configurator.objectChanges)
.map { _ in .map { _ in

@ -38,11 +38,9 @@ final class RenameWalletViewModel {
let assignedName = input.willAppear.map { _ in FileWalletStorage().name(for: self.account) } let assignedName = input.willAppear.map { _ in FileWalletStorage().name(for: self.account) }
let resolvedEns = domainResolutionService.reverseResolveDomainName(address: account, server: RPCServer.forResolvingDomainNames) let resolvedEns: Future<DomainName?, Never> = asFuture { () -> DomainName? in
.map { ens -> DomainName? in return ens } (try? await self.domainResolutionService.reverseResolveDomainName(address: self.account, server: RPCServer.forResolvingDomainNames)) ?? nil
.replaceError(with: nil) }
.prepend(nil)
let viewState = Publishers.CombineLatest(assignedName, resolvedEns) let viewState = Publishers.CombineLatest(assignedName, resolvedEns)
.map { RenameWalletViewModel.ViewState(text: $0.0, placeholder: $0.1) } .map { RenameWalletViewModel.ViewState(text: $0.0, placeholder: $0.1) }
.eraseToAnyPublisher() .eraseToAnyPublisher()

@ -18,6 +18,10 @@ final class FakeApiTransporter: ApiTransporter {
return .empty() return .empty()
} }
func dataTask(_ request: URLRequestConvertible) async throws -> URLRequest.Response {
return (data: Data(), response: HTTPURLResponse())
}
func dataPublisher(_ request: URLRequestConvertible) -> AnyPublisher<Alamofire.DataResponsePublisher<Data>.Output, SessionTaskError> { func dataPublisher(_ request: URLRequestConvertible) -> AnyPublisher<Alamofire.DataResponsePublisher<Data>.Output, SessionTaskError> {
return .empty() return .empty()
} }

@ -6,20 +6,23 @@ import Combine
import AlphaWalletCore import AlphaWalletCore
import AlphaWalletFoundation import AlphaWalletFoundation
//TODO does the results from each stub function work correctly as expected in test suite?:w
class FakeDomainResolutionService: DomainNameResolutionServiceType { class FakeDomainResolutionService: DomainNameResolutionServiceType {
func resolveAddress(string value: String) -> AnyPublisher<AlphaWallet.Address, PromiseError> { func resolveAddress(string value: String) async throws -> AlphaWallet.Address {
return Empty(completeImmediately: true).eraseToAnyPublisher() return Constants.nullAddress
} }
func reverseResolveDomainName(address: AlphaWallet.Address, server: AlphaWalletFoundation.RPCServer) -> AnyPublisher<DomainName, PromiseError> { func reverseResolveDomainName(address: AlphaWallet.Address, server: AlphaWalletFoundation.RPCServer) async throws -> DomainName {
return Empty(completeImmediately: true).eraseToAnyPublisher() struct E: Error {}
throw E()
} }
func resolveEnsAndBlockie(address: AlphaWallet.Address, server: AlphaWalletFoundation.RPCServer) -> AnyPublisher<BlockieAndAddressOrEnsResolution, PromiseError> { func resolveEnsAndBlockie(address: AlphaWallet.Address, server: AlphaWalletFoundation.RPCServer) async throws -> BlockieAndAddressOrEnsResolution {
return Empty(completeImmediately: true).eraseToAnyPublisher() return BlockieAndAddressOrEnsResolution(image: nil, resolution: .invalidInput)
} }
func resolveAddressAndBlockie(string: String) -> AnyPublisher<BlockieAndAddressOrEnsResolution, PromiseError> { func resolveAddressAndBlockie(string: String) async throws -> BlockieAndAddressOrEnsResolution {
return Empty(completeImmediately: true).eraseToAnyPublisher() return BlockieAndAddressOrEnsResolution(image: nil, resolution: .invalidInput)
} }
} }

@ -13,39 +13,44 @@ import AlphaWalletFoundation
class FakeTickerIdsFetcher: TickerIdsFetcher { class FakeTickerIdsFetcher: TickerIdsFetcher {
private let subject: AnyPublisher<TickerIdString?, Never> private let subject: AnyPublisher<TickerIdString?, Never>
private var cancelable = Set<AnyCancellable>()
init(subject: AnyPublisher<TickerIdString?, Never>) { init(subject: AnyPublisher<TickerIdString?, Never>) {
self.subject = subject self.subject = subject
} }
func tickerId(for token: TokenMappedToTicker) -> AnyPublisher<TickerIdString?, Never> { func tickerId(for token: AlphaWalletFoundation.TokenMappedToTicker) async -> TickerIdString? {
return subject.eraseToAnyPublisher() return await withCheckedContinuation { continuation in
subject.sink(receiveValue: { tickerIdString in
continuation.resume(with: .success(tickerIdString))
}).store(in: &cancelable)
}
} }
} }
class TickerIdsFetcherTests: XCTestCase { class TickerIdsFetcherTests: XCTestCase {
var cancelable = Set<AnyCancellable>() var cancelable = Set<AnyCancellable>()
func testExample() throws { func testExample() async throws {
let s1 = PassthroughSubject<TickerIdString?, Never>() let s1 = PassthroughSubject<TickerIdString?, Never>()
let f1 = FakeTickerIdsFetcher(subject: s1.eraseToAnyPublisher()) let f1 = FakeTickerIdsFetcher(subject: s1.eraseToAnyPublisher())
let f2 = FakeTickerIdsFetcher(subject: .just(nil))
let f2 = FakeTickerIdsFetcher(subject: .empty())
let f3 = FakeTickerIdsFetcher(subject: .just("3")) let f3 = FakeTickerIdsFetcher(subject: .just("3"))
let fetcher = TickerIdsFetcherImpl(providers: [f2, f1, f3]) let fetcher = TickerIdsFetcherImpl(providers: [f2, f1, f3])
let expectation = self.expectation(description: "Wait for ticker id to be resolved") let expectation = self.expectation(description: "Wait for ticker id to be resolved")
fetcher.tickerId(for: TokenMappedToTicker(token: Token()))
.sink { tickerId in Task {
XCTAssertEqual(tickerId, "1") DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
expectation.fulfill() s1.send("1")
}.store(in: &cancelable) s1.send(completion: .finished)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
s1.send("1") let tickerId = await fetcher.tickerId(for: TokenMappedToTicker(token: Token()))
s1.send(completion: .finished) XCTAssertEqual(tickerId, "1")
expectation.fulfill()
} }
wait(for: [expectation], timeout: 30) await fulfillment(of: [expectation], timeout: 1)
} }
} }

@ -11,60 +11,64 @@ import Foundation
import AlphaWalletENS import AlphaWalletENS
class EnsRecordsStorageTests: XCTestCase { class EnsRecordsStorageTests: XCTestCase {
func testIsStorageEmpty() throws { func testIsStorageEmpty() async throws {
let storage = FakeEnsRecordsStorage() let storage = FakeEnsRecordsStorage()
XCTAssertEqual(storage.allRecords, [], "Storage is empty") let records = await storage.allRecords
XCTAssertEqual(records, [], "Storage is empty")
} }
func testAddRecord() { func testAddRecord() async {
let storage = FakeEnsRecordsStorage() let storage = FakeEnsRecordsStorage()
XCTAssertEqual(storage.allRecords, [], "Storage is empty") let records = await storage.allRecords
XCTAssertEqual(records, [], "Storage is empty")
let key: DomainNameLookupKey = .init(nameOrAddress: "key", server: .main) let key: DomainNameLookupKey = .init(nameOrAddress: "key", server: .main)
let r1 = DomainNameRecord(key: key, value: .domainName("hello alpha wallet")) let r1 = DomainNameRecord(key: key, value: .domainName("hello alpha wallet"))
storage.addOrUpdate(record: r1) await storage.addOrUpdate(record: r1)
XCTAssertEqual(storage.allRecords.count, 1) let recordCount = await storage.allRecords.count
XCTAssertEqual(recordCount, 1)
} }
func testUpdateRecord() { func testUpdateRecord() async {
let storage = FakeEnsRecordsStorage() let storage = FakeEnsRecordsStorage()
XCTAssertEqual(storage.allRecords, [], "Storage is empty") let records = await storage.allRecords
XCTAssertEqual(records, [], "Storage is empty")
let key: DomainNameLookupKey = .init(nameOrAddress: "key", server: .main) let key: DomainNameLookupKey = .init(nameOrAddress: "key", server: .main)
let r1 = DomainNameRecord(key: key, value: .domainName("hello alpha wallet")) let r1 = DomainNameRecord(key: key, value: .domainName("hello alpha wallet"))
storage.addOrUpdate(record: r1) await storage.addOrUpdate(record: r1)
let r1_copy = storage.record(for: key, expirationTime: -120) let r1_copy = await storage.record(for: key, expirationTime: -120)
XCTAssertEqual(r1, r1_copy) XCTAssertEqual(r1, r1_copy)
let r10 = DomainNameRecord(key: key, value: .record("image")) let r10 = DomainNameRecord(key: key, value: .record("image"))
storage.addOrUpdate(record: r10) await storage.addOrUpdate(record: r10)
let r10_copy = storage.record(for: key, expirationTime: -120) let r10_copy = await storage.record(for: key, expirationTime: -120)
XCTAssertEqual(r10_copy?.value, .record("image")) XCTAssertEqual(r10_copy?.value, .record("image"))
XCTAssertNotEqual(r1_copy, r10_copy) XCTAssertNotEqual(r1_copy, r10_copy)
} }
func testFetchExpiredRecord() { func testFetchExpiredRecord() async {
let storage = FakeEnsRecordsStorage() let storage = FakeEnsRecordsStorage()
let key: DomainNameLookupKey = .init(nameOrAddress: "key", server: .main) let key: DomainNameLookupKey = .init(nameOrAddress: "key", server: .main)
let r1 = DomainNameRecord(key: key, value: .domainName("hello alpha wallet")) let r1 = DomainNameRecord(key: key, value: .domainName("hello alpha wallet"))
storage.addOrUpdate(record: r1) await storage.addOrUpdate(record: r1)
let r1_copy = storage.record(for: key, expirationTime: -120) let r1_copy = await storage.record(for: key, expirationTime: -120)
XCTAssertNotNil(r1_copy) XCTAssertNotNil(r1_copy)
XCTAssertEqual(r1, r1_copy, "Copy and initial value are queal") XCTAssertEqual(r1, r1_copy, "Copy and initial value are queal")
let dateThatExpired = Date(timeIntervalSinceNow: -600) let dateThatExpired = Date(timeIntervalSinceNow: -600)
let r10 = DomainNameRecord(key: key, value: .domainName("hello alpha wallet"), date: dateThatExpired) let r10 = DomainNameRecord(key: key, value: .domainName("hello alpha wallet"), date: dateThatExpired)
storage.addOrUpdate(record: r10) await storage.addOrUpdate(record: r10)
let r10_copy = storage.record(for: key, expirationTime: -120) let r10_copy = await storage.record(for: key, expirationTime: -120)
XCTAssertNil(r10_copy, "Updated value has expired") XCTAssertNil(r10_copy, "Updated value has expired")
XCTAssertEqual(storage.allRecords.count, 1, "Storage contains single value") let recordCount = await storage.allRecords.count
XCTAssertEqual(recordCount, 1, "Storage contains single value")
storage.removeRecord(for: key) await storage.removeRecord(for: key)
} }
} }

@ -20,98 +20,43 @@ class EnsResolverTests: XCTestCase {
storage: FakeEnsRecordsStorage(), storage: FakeEnsRecordsStorage(),
blockchainProvider: RpcBlockchainProvider(server: .main, analytics: FakeAnalyticsService(), params: .defaultParams(for: .main))) blockchainProvider: RpcBlockchainProvider(server: .main, analytics: FakeAnalyticsService(), params: .defaultParams(for: .main)))
}() }()
func testResolution() {
func testResolution() async {
var expectations = [XCTestExpectation]() var expectations = [XCTestExpectation]()
let expectation = self.expectation(description: "Wait for ENS name to be resolved") let expectation = self.expectation(description: "Wait for ENS name to be resolved")
expectations.append(expectation) expectations.append(expectation)
let ensName = "b00n.thisisme.eth" let ensName = "b00n.thisisme.eth"
Task {
resolver.getENSAddressFromResolver(for: ensName) let address = try! await resolver.getENSAddressFromResolver(for: ensName)
.sink(receiveCompletion: { response in XCTAssertTrue(address.sameContract(as: "0xbbce83173d5c1D122AE64856b4Af0D5AE07Fa362"), "ENS name did not resolve correctly")
switch response { expectation.fulfill()
case .finished: }
break await fulfillment(of: expectations, timeout: 20)
case .failure(let error):
guard case .embedded(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: "0xbbce83173d5c1D122AE64856b4Af0D5AE07Fa362"), "ENS name did not resolve correctly")
}).store(in: &cancelable)
wait(for: expectations, timeout: 20)
} }
func testResolutionThatHasDifferentOwnerAndResolver() { func testResolutionThatHasDifferentOwnerAndResolver() async {
var expectations = [XCTestExpectation]() var expectations = [XCTestExpectation]()
let expectation = self.expectation(description: "Wait for ENS name to be resolved") let expectation = self.expectation(description: "Wait for ENS name to be resolved")
expectations.append(expectation) expectations.append(expectation)
let ensName = "ethereum.eth" let ensName = "ethereum.eth"
resolver.getENSAddressFromResolver(for: ensName) Task {
.sink(receiveCompletion: { response in let address = try! await resolver.getENSAddressFromResolver(for: ensName)
switch response { XCTAssertTrue(address.sameContract(as: "0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe"), "ENS name did not resolve correctly")
case .finished: expectation.fulfill()
break }
case .failure(let error): await fulfillment(of: expectations, timeout: 20)
guard case .embedded(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: "0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe"), "ENS name did not resolve correctly")
}).store(in: &cancelable)
wait(for: expectations, timeout: 20)
} }
func testEnsIp10WildcardAndEip3668CcipRead() { func testEnsIp10WildcardAndEip3668CcipRead() async {
var expectations = [XCTestExpectation]() var expectations = [XCTestExpectation]()
let expectation = self.expectation(description: "Wait for ENS name to be resolved") let expectation = self.expectation(description: "Wait for ENS name to be resolved")
expectations.append(expectation) expectations.append(expectation)
let ensName = "1.offchainexample.eth" let ensName = "1.offchainexample.eth"
resolver.getENSAddressFromResolver(for: ensName) Task {
.sink(receiveCompletion: { response in let address = try! await resolver.getENSAddressFromResolver(for: ensName)
switch response { XCTAssertTrue(address.sameContract(as: "41563129cdbbd0c5d3e1c86cf9563926b243834d"), "ENS name did not resolve correctly")
case .finished: expectation.fulfill()
break }
case .failure(let error): await fulfillment(of: expectations, timeout: 100)
guard case .embedded(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: 100)
}
private func makeServerForMainnet() -> RPCServer {
return .main
} }
} }

@ -48,11 +48,13 @@ class RLPTests: XCTestCase {
} }
func testLists() { func testLists() {
XCTAssertEqual(RLP.encode([])!.hex(), "c0") //Explicit empty array to make compiler happy
let emptyParams: [Any] = []
XCTAssertEqual(RLP.encode([] as [Any])!.hex(), "c0")
XCTAssertEqual(RLP.encode([1, 2, 3])!.hex(), "c3010203") XCTAssertEqual(RLP.encode([1, 2, 3])!.hex(), "c3010203")
XCTAssertEqual(RLP.encode(["cat", "dog"])!.hex(), "c88363617483646f67") XCTAssertEqual(RLP.encode(["cat", "dog"])!.hex(), "c88363617483646f67")
XCTAssertEqual(RLP.encode([ [], [[]], [ [], [[]] ] ])!.hex(), "c7c0c1c0c3c0c1c0") XCTAssertEqual(RLP.encode([ emptyParams, [emptyParams], [ [], [emptyParams] ] ] as [Any])!.hex(), "c7c0c1c0c3c0c1c0")
XCTAssertEqual(RLP.encode([1, 0xffffff, [4, 5, 5], "abc"])!.hex(), "cd0183ffffffc304050583616263") XCTAssertEqual(RLP.encode([1, 0xffffff, [4, 5, 5], "abc"] as [Any])!.hex(), "cd0183ffffffc304050583616263")
let encoded = RLP.encode([Int](repeating: 0, count: 1024))! let encoded = RLP.encode([Int](repeating: 0, count: 1024))!
XCTAssert(encoded.hex().hasPrefix("f90400")) XCTAssert(encoded.hex().hasPrefix("f90400"))
} }

@ -13,6 +13,6 @@ import AlphaWalletFoundation
extension CoinTickers { extension CoinTickers {
static func make(config: Config = .make()) -> CoinTickersProvider & CoinTickersFetcher { static func make(config: Config = .make()) -> CoinTickersProvider & CoinTickersFetcher {
return CoinTickers(fetchers: [], storage: RealmStore(realm: fakeRealm())) return CoinTickers(fetchers: [], storage: RealmStore(config: fakeRealm().configuration))
} }
} }

@ -11,6 +11,6 @@ import AlphaWalletFoundation
final class FakeEnsRecordsStorage: RealmStore { final class FakeEnsRecordsStorage: RealmStore {
init() { init() {
super.init(realm: fakeRealm()) super.init(config: fakeRealm().configuration)
} }
} }

@ -11,8 +11,8 @@ import AlphaWalletFoundation
import Combine import Combine
final class FakeNftProvider: NFTProvider, NftAssetImageProvider { final class FakeNftProvider: NFTProvider, NftAssetImageProvider {
func assetImageUrl(for url: Eip155URL) -> AnyPublisher<URL, PromiseError> { func assetImageUrl(for url: Eip155URL) async throws -> URL {
return .fail(PromiseError(error: ProviderError())) throw ProviderError()
} }
struct ProviderError: Error {} struct ProviderError: Error {}
@ -24,7 +24,7 @@ final class FakeNftProvider: NFTProvider, NftAssetImageProvider {
func nonFungible() -> AnyPublisher<NonFungiblesTokens, Never> { func nonFungible() -> AnyPublisher<NonFungiblesTokens, Never> {
return .just((openSea: [:], enjin: ())) return .just((openSea: [:], enjin: ()))
} }
func enjinToken(tokenId: TokenId) -> EnjinToken? { func enjinToken(tokenId: TokenId) -> EnjinToken? {
return nil return nil
} }

@ -67,7 +67,13 @@ class ConfigTests: XCTestCase {
coordinator.start() coordinator.start()
coordinator.tokensViewController.viewWillAppear(false) coordinator.tokensViewController.viewWillAppear(false)
XCTAssertEqual(coordinator.tokensViewController.navigationItem.title, "0x1000…0000") let expectation = self.expectation(description: "Check ran")
//Wait a bit for async to update title
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
XCTAssertEqual(coordinator.tokensViewController.navigationItem.title, "0x1000…0000")
expectation.fulfill()
}
wait(for: [expectation], timeout: 6)
} }
func testTabBarItemTitle() { func testTabBarItemTitle() {

@ -17,14 +17,15 @@ class SettingsCoordinatorTests: XCTestCase {
private var removeWalletCancelable: AnyCancellable? private var removeWalletCancelable: AnyCancellable?
private var addWalletCancelable: AnyCancellable? private var addWalletCancelable: AnyCancellable?
func testOnDeleteCleanStorage() { func testOnDeleteCleanStorage() async {
let wallet: Wallet = .make() let wallet: Wallet = .make()
let storage = FakeTransactionsStorage(wallet: wallet) let storage = FakeTransactionsStorage(wallet: wallet)
let walletAddressesStore = EtherKeystore.migratedWalletAddressesStore(userDefaults: .test) let walletAddressesStore = EtherKeystore.migratedWalletAddressesStore(userDefaults: .test)
let keystore = FakeEtherKeystore(walletAddressesStore: walletAddressesStore) let keystore = FakeEtherKeystore(walletAddressesStore: walletAddressesStore)
storage.add(transactions: [.make()]) await storage.add(transactions: [.make()])
XCTAssertEqual(1, storage.transactionCount(forServer: .main)) let transactionCount = await storage.transactionCount(forServer: .main)
XCTAssertEqual(1, transactionCount)
var deletedWallet: Wallet? var deletedWallet: Wallet?
let expectation = self.expectation(description: "didRemoveWalletPublisher") let expectation = self.expectation(description: "didRemoveWalletPublisher")
@ -37,15 +38,17 @@ class SettingsCoordinatorTests: XCTestCase {
expectation.fulfill() expectation.fulfill()
storage.deleteAllForTestsOnly() storage.deleteAllForTestsOnly()
//let transactionCount2 = await storage.transactionCount(forServer: .main)
XCTAssertNotNil(deletedWallet) XCTAssertNotNil(deletedWallet)
XCTAssertTrue(wallet.address == deletedWallet!.address) XCTAssertTrue(wallet.address == deletedWallet!.address)
XCTAssertEqual(0, keystore.wallets.count) XCTAssertEqual(0, keystore.wallets.count)
XCTAssertEqual(0, storage.transactionCount(forServer: .main)) //TODO test this too
//XCTAssertEqual(0, transactionCount2)
} }
keystore.delete(wallet: wallet) keystore.delete(wallet: wallet)
wait(for: [expectation], timeout: 20) await fulfillment(of: [expectation], timeout: 20)
} }
func testDeleteWallet() { func testDeleteWallet() {

@ -10,47 +10,47 @@ class TokenObjectTest: XCTestCase {
XCTAssertFalse(isNonZeroBalance(Constants.nullTokenId, tokenType: .erc875)) XCTAssertFalse(isNonZeroBalance(Constants.nullTokenId, tokenType: .erc875))
} }
func testTokenInfo() { func testTokenInfo() async {
let dataStore = FakeTokensDataStore() let dataStore = FakeTokensDataStore()
let _token = Token(contract: AlphaWallet.Address.make(address: "0x1000000000000000000000000000000000000004"), type: .erc20) let _token = Token(contract: AlphaWallet.Address.make(address: "0x1000000000000000000000000000000000000004"), type: .erc20)
dataStore.addOrUpdate(with: [.init(_token)]) await dataStore.addOrUpdate(with: [.init(_token)])
let token1 = dataStore.token(for: _token.contractAddress, server: _token.server) let token1 = await dataStore.token(for: _token.contractAddress, server: _token.server)
XCTAssertNotNil(token1?.info) XCTAssertNotNil(token1?.info)
XCTAssertEqual(token1?.info, _token.info) XCTAssertEqual(token1?.info, _token.info)
let url = URL(string: "http://google.com") let url = URL(string: "http://google.com")
dataStore.addOrUpdate(with: [.update(token: _token, field: .imageUrl(url))]) await dataStore.addOrUpdate(with: [.update(token: _token, field: .imageUrl(url))])
let token2 = dataStore.token(for: _token.contractAddress, server: _token.server) let token2 = await dataStore.token(for: _token.contractAddress, server: _token.server)
XCTAssertEqual(token2?.info.imageUrl, url?.absoluteString) XCTAssertEqual(token2?.info.imageUrl, url?.absoluteString)
dataStore.addOrUpdate(with: [.update(token: _token, field: .imageUrl(nil))]) await dataStore.addOrUpdate(with: [.update(token: _token, field: .imageUrl(nil))])
let token3 = dataStore.token(for: _token.contractAddress, server: _token.server) let token3 = await dataStore.token(for: _token.contractAddress, server: _token.server)
XCTAssertNil(token3?.info.imageUrl) XCTAssertNil(token3?.info.imageUrl)
} }
func testTokenBalanceDeletion() { func testTokenBalanceDeletion() async {
let dataStore = FakeTokensDataStore() let dataStore = FakeTokensDataStore()
let _token = Token(contract: AlphaWallet.Address.make(address: "0x1000000000000000000000000000000000000004"), type: .erc721) let _token = Token(contract: AlphaWallet.Address.make(address: "0x1000000000000000000000000000000000000004"), type: .erc721)
dataStore.addOrUpdate(with: [.init(_token)]) await dataStore.addOrUpdate(with: [.init(_token)])
let _token1 = dataStore.token(for: _token.contractAddress, server: _token.server) let _token1 = await dataStore.token(for: _token.contractAddress, server: _token.server)
XCTAssertEqual(_token1?.balance, []) XCTAssertEqual(_token1?.balance, [])
dataStore.updateToken(primaryKey: _token.primaryKey, action: .nonFungibleBalance(.balance(["test balance"]))) await dataStore.updateToken(primaryKey: _token.primaryKey, action: .nonFungibleBalance(.balance(["test balance"])))
let balances1 = dataStore.tokenBalancesTestsOnly() let balances1 = await dataStore.tokenBalancesTestsOnly()
XCTAssertEqual(balances1.count, 1) XCTAssertEqual(balances1.count, 1)
dataStore.updateToken(primaryKey: _token.primaryKey, action: .nonFungibleBalance(.balance([]))) await dataStore.updateToken(primaryKey: _token.primaryKey, action: .nonFungibleBalance(.balance([])))
let balances2 = dataStore.tokenBalancesTestsOnly() let balances2 = await dataStore.tokenBalancesTestsOnly()
XCTAssertEqual(balances2.count, 0) XCTAssertEqual(balances2.count, 0)
let _token2 = dataStore.token(for: _token.contractAddress, server: _token.server) let _token2 = await dataStore.token(for: _token.contractAddress, server: _token.server)
XCTAssertEqual(_token2?.balance, []) XCTAssertEqual(_token2?.balance, [])
} }
} }

@ -14,14 +14,12 @@ class TokensDataStoreTest: XCTestCase {
type: .erc20 type: .erc20
) )
override func setUp() {
storage.addOrUpdate(with: [.init(token)])
}
//We make a call to update token in datastore to store the updated balance after an async call to fetch the balance over the web. Token in the datastore might have been deleted when the web call is completed. Make sure this doesn't crash //We make a call to update token in datastore to store the updated balance after an async call to fetch the balance over the web. Token in the datastore might have been deleted when the web call is completed. Make sure this doesn't crash
func testUpdateDeletedTokensDoNotCrash() { func testUpdateDeletedTokensDoNotCrash() async {
await storage.addOrUpdate(with: [.init(token)])
storage.deleteTestsOnly(tokens: [token]) storage.deleteTestsOnly(tokens: [token])
XCTAssertNil(storage.updateToken(primaryKey: token.primaryKey, action: .value(1))) let tokenUpdateHappened = await storage.updateToken(primaryKey: token.primaryKey, action: .value(1))
XCTAssertNil(tokenUpdateHappened)
} }
//Ensure this works: //Ensure this works:
@ -30,7 +28,8 @@ class TokensDataStoreTest: XCTestCase {
//3. Manually add that token back (add custom token screen) //3. Manually add that token back (add custom token screen)
//4. Swipe to hide that token. //4. Swipe to hide that token.
//5. BOOM. //5. BOOM.
func testHideContractTwiceDoesNotCrash() { func testHideContractTwiceDoesNotCrash() async {
await storage.addOrUpdate(with: [.init(token)])
let contract = AlphaWallet.Address(string: "0x66F08Ca6892017A45Da6FB792a8E946FcBE3d865")! let contract = AlphaWallet.Address(string: "0x66F08Ca6892017A45Da6FB792a8E946FcBE3d865")!
storage.add(hiddenContracts: [AddressAndRPCServer(address: contract, server: .goerli)]) storage.add(hiddenContracts: [AddressAndRPCServer(address: contract, server: .goerli)])
XCTAssertNoThrow(storage.add(hiddenContracts: [AddressAndRPCServer(address: contract, server: .goerli)])) XCTAssertNoThrow(storage.add(hiddenContracts: [AddressAndRPCServer(address: contract, server: .goerli)]))

@ -6,76 +6,84 @@ import AlphaWalletFoundation
class TransactionsStorageTests: XCTestCase { class TransactionsStorageTests: XCTestCase {
func testInit() { func testInit() async {
let storage = FakeTransactionsStorage() let storage = FakeTransactionsStorage()
XCTAssertNotNil(storage) XCTAssertNotNil(storage)
XCTAssertEqual(0, storage.transactionCount(forServer: .main)) let transactionCount = await storage.transactionCount(forServer: .main)
XCTAssertEqual(0, transactionCount)
} }
func testAddItem() { func testAddItem() async {
let storage = FakeTransactionsStorage() let storage = FakeTransactionsStorage()
let item: Transaction = .make() let item: Transaction = .make()
storage.add(transactions: [item]) await storage.add(transactions: [item])
let transactionCount = await storage.transactionCount(forServer: .main)
XCTAssertEqual(1, storage.transactionCount(forServer: .main)) XCTAssertEqual(1, transactionCount)
} }
func testAddItems() { func testAddItems() async {
let storage = FakeTransactionsStorage() let storage = FakeTransactionsStorage()
storage.add(transactions: [ await storage.add(transactions: [
.make(id: "0x1"), .make(id: "0x1"),
.make(id: "0x2") .make(id: "0x2")
]) ])
XCTAssertEqual(2, storage.transactionCount(forServer: .main)) let transactionCount = await storage.transactionCount(forServer: .main)
XCTAssertEqual(2, transactionCount)
} }
func testAddItemsDuplicate() { func testAddItemsDuplicate() async {
let storage = FakeTransactionsStorage() let storage = FakeTransactionsStorage()
storage.add(transactions: [ await storage.add(transactions: [
.make(id: "0x1"), .make(id: "0x1"),
.make(id: "0x1"), .make(id: "0x1"),
.make(id: "0x2") .make(id: "0x2")
]) ])
XCTAssertEqual(2, storage.transactionCount(forServer: .main)) let transactionCount = await storage.transactionCount(forServer: .main)
XCTAssertEqual(2, transactionCount)
} }
func testDelete() { func testDelete() async {
let storage = FakeTransactionsStorage() let storage = FakeTransactionsStorage()
let one: Transaction = .make(id: "0x1") let one: Transaction = .make(id: "0x1")
let two: Transaction = .make(id: "0x2") let two: Transaction = .make(id: "0x2")
storage.add(transactions: [ await storage.add(transactions: [
one, one,
two, two,
]) ])
XCTAssertEqual(2, storage.transactionCount(forServer: .main)) let transactionCount = await storage.transactionCount(forServer: .main)
XCTAssertEqual(2, transactionCount)
storage.delete(transactions: [one]) storage.delete(transactions: [one])
XCTAssertEqual(1, storage.transactionCount(forServer: .main)) let transactionCount2 = await storage.transactionCount(forServer: .main)
XCTAssertEqual(1, transactionCount2)
XCTAssertEqual(two, storage.transactions(forServer: .main).first) let transaction = await storage.transactions(forServer: .main).first
XCTAssertEqual(two, transaction)
} }
func testDeleteAll() { func testDeleteAll() async {
let storage = FakeTransactionsStorage() let storage = FakeTransactionsStorage()
storage.add(transactions: [ await storage.add(transactions: [
.make(id: "0x1"), .make(id: "0x1"),
.make(id: "0x2") .make(id: "0x2")
]) ])
XCTAssertEqual(2, storage.transactionCount(forServer: .main)) let transactionCount = await storage.transactionCount(forServer: .main)
XCTAssertEqual(2, transactionCount)
storage.deleteAllForTestsOnly() storage.deleteAllForTestsOnly()
XCTAssertEqual(0, storage.transactionCount(forServer: .main)) let transactionCount2 = await storage.transactionCount(forServer: .main)
XCTAssertEqual(0, transactionCount2)
} }
} }

@ -15,7 +15,7 @@ import Combine
class ImportTokenTests: XCTestCase { class ImportTokenTests: XCTestCase {
private var cancelable = Set<AnyCancellable>() private var cancelable = Set<AnyCancellable>()
func testImportUnknownErc20Token() throws { func testImportUnknownErc20Token() async throws {
let tokensDataStore = FakeTokensDataStore() let tokensDataStore = FakeTokensDataStore()
let server = RPCServer.main let server = RPCServer.main
let contractDataFetcher = FakeContractDataFetcher(server: server) let contractDataFetcher = FakeContractDataFetcher(server: server)
@ -25,9 +25,11 @@ class ImportTokenTests: XCTestCase {
contractDataFetcher.contractData[.init(address: address, server: server)] = .fungibleTokenComplete(name: "erc20", symbol: "erc20", decimals: 6, value: .zero, tokenType: .erc20) contractDataFetcher.contractData[.init(address: address, server: server)] = .fungibleTokenComplete(name: "erc20", symbol: "erc20", decimals: 6, value: .zero, tokenType: .erc20)
XCTAssertNil(tokensDataStore.token(for: address, server: server), "Initially token is nil") let token = await tokensDataStore.token(for: address, server: server)
XCTAssertNil(token, "Initially token is nil")
let expectation = self.expectation(description: "did resolve erc20 token") let expectation = self.expectation(description: "did resolve erc20 token")
let expectationDidCheckAdd = self.expectation(description: "did check adding token token")
importToken.importToken(for: address) importToken.importToken(for: address)
.sink(receiveCompletion: { _ in .sink(receiveCompletion: { _ in
expectation.fulfill() expectation.fulfill()
@ -35,10 +37,14 @@ class ImportTokenTests: XCTestCase {
XCTAssertEqual(token.type, .erc20) XCTAssertEqual(token.type, .erc20)
XCTAssertEqual(token.symbol, "erc20") XCTAssertEqual(token.symbol, "erc20")
XCTAssertEqual(token.decimals, 6) XCTAssertEqual(token.decimals, 6)
XCTAssertEqual(token, tokensDataStore.token(for: address, server: server), "Token has added to storage") Task { @MainActor in
let addedToken = await tokensDataStore.token(for: address, server: server)
XCTAssertEqual(token, addedToken, "Token has added to storage")
expectationDidCheckAdd.fulfill()
}
}).store(in: &cancelable) }).store(in: &cancelable)
waitForExpectations(timeout: 30) await fulfillment(of: [expectation, expectationDidCheckAdd], timeout: 30)
} }
func testImportNotDetectedErc20Token() throws { func testImportNotDetectedErc20Token() throws {
@ -64,7 +70,7 @@ class ImportTokenTests: XCTestCase {
waitForExpectations(timeout: 30) waitForExpectations(timeout: 30)
} }
func testImportAlreadyAddedErc20Token() throws { func testImportAlreadyAddedErc20Token() async throws {
let tokensDataStore = FakeTokensDataStore() let tokensDataStore = FakeTokensDataStore()
let server = RPCServer.main let server = RPCServer.main
let contractDataFetcher = FakeContractDataFetcher(server: server) let contractDataFetcher = FakeContractDataFetcher(server: server)
@ -74,7 +80,7 @@ class ImportTokenTests: XCTestCase {
let address = AlphaWallet.Address(string: "0xbc8dafeaca658ae0857c80d8aa6de4d487577c63")! let address = AlphaWallet.Address(string: "0xbc8dafeaca658ae0857c80d8aa6de4d487577c63")!
let token = Token(contract: address, server: server, value: .zero, type: .erc20) let token = Token(contract: address, server: server, value: .zero, type: .erc20)
tokensDataStore.addOrUpdate(with: [.init(token)]) await tokensDataStore.addOrUpdate(with: [.init(token)])
let expectation = self.expectation(description: "did resolve erc20 token") let expectation = self.expectation(description: "did resolve erc20 token")
contractDataFetcher.contractData[.init(address: address, server: server)] = .fungibleTokenComplete(name: "erc20", symbol: "erc20", decimals: 6, value: BigUInt("1"), tokenType: .erc20) contractDataFetcher.contractData[.init(address: address, server: server)] = .fungibleTokenComplete(name: "erc20", symbol: "erc20", decimals: 6, value: BigUInt("1"), tokenType: .erc20)
@ -86,7 +92,7 @@ class ImportTokenTests: XCTestCase {
XCTAssertEqual(token.value, .zero) XCTAssertEqual(token.value, .zero)
}).store(in: &cancelable) }).store(in: &cancelable)
waitForExpectations(timeout: 30) await fulfillment(of: [expectation], timeout: 30)
} }
} }
@ -102,20 +108,20 @@ class TransactionTypeFromQrCodeTests: XCTestCase {
let sessions = FakeSessionsProvider(servers: [.main]) let sessions = FakeSessionsProvider(servers: [.main])
sessions.importToken[.main] = fakeFakeImportToken sessions.importToken[.main] = fakeFakeImportToken
sessions.start() sessions.start()
let provider = TransactionTypeFromQrCode(sessionsProvider: sessions, session: sessions.session(for: .main)!) let provider = TransactionTypeFromQrCode(sessionsProvider: sessions, session: sessions.session(for: .main)!)
provider.transactionTypeProvider = transactionTypeSupportable provider.transactionTypeProvider = transactionTypeSupportable
return provider return provider
}() }()
func testScanSmallEthTransfer() throws { func testScanSmallEthTransfer() async throws {
let expectation = self.expectation(description: "did resolve erc20 transaction type") let expectation = self.expectation(description: "did resolve erc20 transaction type")
let qrCode = "aw.app/ethereum:0xbc8dafeaca658ae0857c80d8aa6de4d487577c63@1?value=1e12" let qrCode = "aw.app/ethereum:0xbc8dafeaca658ae0857c80d8aa6de4d487577c63@1?value=1e12"
let etherToken = Token(contract: Constants.nativeCryptoAddressInDatabase, server: .main, name: "Ether", symbol: "eth", decimals: 18, type: .nativeCryptocurrency) 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 //NOTE: make sure we have a eth token, base impl resolves it automatically, for test does it manually
tokensDataStore.addOrUpdate(with: [ await tokensDataStore.addOrUpdate(with: [
.init(etherToken) .init(etherToken)
]) ])
@ -140,16 +146,16 @@ class TransactionTypeFromQrCodeTests: XCTestCase {
expectation.fulfill() expectation.fulfill()
}.store(in: &cancelable) }.store(in: &cancelable)
waitForExpectations(timeout: 30) await fulfillment(of: [expectation], timeout: 30)
} }
func testScanSmallErc20Transfer() throws { func testScanSmallErc20Transfer() async throws {
let expectation = self.expectation(description: "did resolve erc20 transaction type") let expectation = self.expectation(description: "did resolve erc20 transaction type")
let qrCode = "aw.app/ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer?address=0x8e23ee67d1332ad560396262c48ffbb01f93d052&uint256=1" let qrCode = "aw.app/ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer?address=0x8e23ee67d1332ad560396262c48ffbb01f93d052&uint256=1"
let erc20Token = Token(contract: .init(string: "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7")!, server: .main, name: "erc20", symbol: "erc20", decimals: 6, type: .erc20) let erc20Token = Token(contract: .init(string: "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7")!, server: .main, name: "erc20", symbol: "erc20", decimals: 6, type: .erc20)
//NOTE: make sure we have a eth token, base impl resolves it automatically, for test does it manually //NOTE: make sure we have a eth token, base impl resolves it automatically, for test does it manually
tokensDataStore.addOrUpdate(with: [.init(erc20Token)]) await tokensDataStore.addOrUpdate(with: [.init(erc20Token)])
transactionTypeSupportable.transactionType = .erc20Token(erc20Token, destination: nil, amount: .notSet) transactionTypeSupportable.transactionType = .erc20Token(erc20Token, destination: nil, amount: .notSet)
@ -166,7 +172,7 @@ class TransactionTypeFromQrCodeTests: XCTestCase {
expectation.fulfill() expectation.fulfill()
}.store(in: &cancelable) }.store(in: &cancelable)
waitForExpectations(timeout: 30) await fulfillment(of: [expectation], timeout: 30)
} }
func testScanSmallErc20TransferWhenTokenNeedToResolve() throws { func testScanSmallErc20TransferWhenTokenNeedToResolve() throws {

@ -113,21 +113,21 @@ class SendViewControllerTests: XCTestCase {
Config.setLocale(AppLocale.system) Config.setLocale(AppLocale.system)
} }
func testTokenBalance() { func testTokenBalance() async {
let token = Token(contract: AlphaWallet.Address.make(), server: .main, decimals: 18, value: "2020224719101120", type: .erc20) let token = Token(contract: AlphaWallet.Address.make(), server: .main, decimals: 18, value: "2020224719101120", type: .erc20)
dep.tokensService.addOrUpdateTokenTestsOnly(token: token) dep.tokensService.addOrUpdateTokenTestsOnly(token: token)
let tokens = dep.tokensService.tokens(for: [.main]) let tokens = await dep.tokensService.tokens(for: [.main])
XCTAssertTrue(tokens.contains(token)) XCTAssertTrue(tokens.contains(token))
let viewModel = dep.pipeline.tokenViewModel(for: token) let viewModel = await dep.pipeline.tokenViewModel(for: token)
XCTAssertNotNil(viewModel) XCTAssertNotNil(viewModel)
XCTAssertEqual(viewModel?.value, BigUInt("2020224719101120")!) XCTAssertEqual(viewModel?.value, BigUInt("2020224719101120")!)
dep.tokensService.setBalanceTestsOnly(balance: .init(value: BigUInt("10000000000000")), for: token) dep.tokensService.setBalanceTestsOnly(balance: .init(value: BigUInt("10000000000000")), for: token)
let viewModel_2 = dep.pipeline.tokenViewModel(for: token) let viewModel_2 = await dep.pipeline.tokenViewModel(for: token)
XCTAssertNotNil(viewModel_2) XCTAssertNotNil(viewModel_2)
XCTAssertEqual(viewModel_2?.value, BigUInt("10000000000000")!) XCTAssertEqual(viewModel_2?.value, BigUInt("10000000000000")!)
} }
@ -308,7 +308,7 @@ class TokenBalanceTests: XCTestCase {
waitForExpectations(timeout: 50) waitForExpectations(timeout: 50)
} }
func testBalanceUpdates() { func testBalanceUpdates() async {
let pipeline = dep.pipeline let pipeline = dep.pipeline
let tokensService = dep.tokensService let tokensService = dep.tokensService
@ -319,7 +319,7 @@ class TokenBalanceTests: XCTestCase {
value: "2000000020224719101120", value: "2000000020224719101120",
type: .erc20) type: .erc20)
var balance = pipeline.tokenViewModel(for: token) var balance = await pipeline.tokenViewModel(for: token)
XCTAssertNil(balance) XCTAssertNil(balance)
let isNotNilInitialValueExpectation = self.expectation(description: "Non nil value when subscribe for publisher") let isNotNilInitialValueExpectation = self.expectation(description: "Non nil value when subscribe for publisher")
@ -333,11 +333,11 @@ class TokenBalanceTests: XCTestCase {
isNotNilInitialValueExpectation.fulfill() isNotNilInitialValueExpectation.fulfill()
} }
waitForExpectations(timeout: 10) await fulfillment(of: [isNotNilInitialValueExpectation], timeout: 10)
tokensService.addOrUpdateTokenTestsOnly(token: token) tokensService.addOrUpdateTokenTestsOnly(token: token)
balance = pipeline.tokenViewModel(for: token) balance = await pipeline.tokenViewModel(for: token)
XCTAssertNotNil(balance) XCTAssertNotNil(balance)
let hasInitialValueExpectation = self.expectation(description: "Initial value when subscribe for publisher") let hasInitialValueExpectation = self.expectation(description: "Initial value when subscribe for publisher")
@ -350,10 +350,10 @@ class TokenBalanceTests: XCTestCase {
hasInitialValueExpectation.fulfill() hasInitialValueExpectation.fulfill()
} }
waitForExpectations(timeout: 10) await fulfillment(of: [hasInitialValueExpectation], timeout: 10)
} }
func testBalanceUpdatesPublisherWhenServersChanged() { func testBalanceUpdatesPublisherWhenServersChanged() async {
let pipeline = dep.pipeline let pipeline = dep.pipeline
let tokensService = dep.tokensService let tokensService = dep.tokensService
let token = Token( let token = Token(
@ -363,12 +363,12 @@ class TokenBalanceTests: XCTestCase {
value: "2000000020224719101120", value: "2000000020224719101120",
type: .erc20) type: .erc20)
var balance = pipeline.tokenViewModel(for: token) var balance = await pipeline.tokenViewModel(for: token)
XCTAssertNil(balance) XCTAssertNil(balance)
tokensService.addOrUpdateTokenTestsOnly(token: token) tokensService.addOrUpdateTokenTestsOnly(token: token)
balance = pipeline.tokenViewModel(for: token) balance = await pipeline.tokenViewModel(for: token)
XCTAssertNotNil(balance) XCTAssertNotNil(balance)
let tokenBalanceUpdateCallbackExpectation = self.expectation(description: "did update token balance expectation") let tokenBalanceUpdateCallbackExpectation = self.expectation(description: "did update token balance expectation")
@ -409,7 +409,7 @@ class TokenBalanceTests: XCTestCase {
tokensService.deleteTokenTestsOnly(token: token) tokensService.deleteTokenTestsOnly(token: token)
} }
waitForExpectations(timeout: 50) await fulfillment(of: [tokenBalanceUpdateCallbackExpectation], timeout: 50)
} }
func testTokenDeletion() { func testTokenDeletion() {
@ -456,17 +456,17 @@ class TokenBalanceTests: XCTestCase {
waitForExpectations(timeout: 30) waitForExpectations(timeout: 30)
} }
func testBalanceUpdatesPublisherWhenNonFungibleBalanceUpdated() { func testBalanceUpdatesPublisherWhenNonFungibleBalanceUpdated() async {
let pipeline = dep.pipeline let pipeline = dep.pipeline
let tokensService = dep.tokensService let tokensService = dep.tokensService
let token = Token(contract: AlphaWallet.Address.make(address: "0x1000000000000000000000000000000000000004"), server: .main, decimals: 18, value: "0", type: .erc721) let token = Token(contract: AlphaWallet.Address.make(address: "0x1000000000000000000000000000000000000004"), server: .main, decimals: 18, value: "0", type: .erc721)
var balance = pipeline.tokenViewModel(for: token) var balance = await pipeline.tokenViewModel(for: token)
XCTAssertNil(balance) XCTAssertNil(balance)
tokensService.addOrUpdateTokenTestsOnly(token: token) tokensService.addOrUpdateTokenTestsOnly(token: token)
balance = pipeline.tokenViewModel(for: token) balance = await pipeline.tokenViewModel(for: token)
XCTAssertNotNil(balance) XCTAssertNotNil(balance)
let tokenBalanceUpdateCallbackExpectation = self.expectation(description: "did update token balance expectation") let tokenBalanceUpdateCallbackExpectation = self.expectation(description: "did update token balance expectation")
@ -498,20 +498,20 @@ class TokenBalanceTests: XCTestCase {
} }
} }
waitForExpectations(timeout: 30) await fulfillment(of: [tokenBalanceUpdateCallbackExpectation], timeout: 30)
} }
func testBalanceUpdatesPublisherWhenFungibleBalanceUpdated() { func testBalanceUpdatesPublisherWhenFungibleBalanceUpdated() async {
var callbackCount: Int = 0 var callbackCount: Int = 0
let token = Token(contract: AlphaWallet.Address.make(address: "0x1000000000000000000000000000000000000001"), server: .main, decimals: 18, value: "2000000020224719101120", type: .erc20) let token = Token(contract: AlphaWallet.Address.make(address: "0x1000000000000000000000000000000000000001"), server: .main, decimals: 18, value: "2000000020224719101120", type: .erc20)
let pipeline = dep.pipeline let pipeline = dep.pipeline
let tokensService = dep.tokensService let tokensService = dep.tokensService
var balance = pipeline.tokenViewModel(for: token) var balance = await pipeline.tokenViewModel(for: token)
XCTAssertNil(balance) XCTAssertNil(balance)
dep.tokensService.addOrUpdateTokenTestsOnly(token: token) dep.tokensService.addOrUpdateTokenTestsOnly(token: token)
balance = pipeline.tokenViewModel(for: token) balance = await pipeline.tokenViewModel(for: token)
XCTAssertNotNil(balance) XCTAssertNotNil(balance)
let tokenBalanceUpdateCallbackExpectation = self.expectation(description: "did update token balance expectation") let tokenBalanceUpdateCallbackExpectation = self.expectation(description: "did update token balance expectation")
@ -532,27 +532,31 @@ class TokenBalanceTests: XCTestCase {
} }
} }
waitForExpectations(timeout: 30) await fulfillment(of: [tokenBalanceUpdateCallbackExpectation], timeout: 30)
} }
func testUpdateNativeCryptoBalance() { func testUpdateNativeCryptoBalance() async {
let token = Token(contract: .make(), server: .main, value: "0", type: .nativeCryptocurrency) let token = Token(contract: .make(), server: .main, value: "0", type: .nativeCryptocurrency)
let pipeline = dep.pipeline let pipeline = dep.pipeline
dep.tokensService.addOrUpdateTokenTestsOnly(token: token) dep.tokensService.addOrUpdateTokenTestsOnly(token: token)
XCTAssertEqual(pipeline.tokenViewModel(for: token)!.balance.value, .zero) let viewModel = await pipeline.tokenViewModel(for: token)
XCTAssertEqual(viewModel!.balance.value, .zero)
let testValue1 = BigUInt("10000000000000000000000") let testValue1 = BigUInt("10000000000000000000000")
dep.tokensService.setBalanceTestsOnly(balance: .init(value: testValue1), for: token) dep.tokensService.setBalanceTestsOnly(balance: .init(value: testValue1), for: token)
XCTAssertEqual(pipeline.tokenViewModel(for: token)!.balance.value, testValue1) let viewModel2 = await pipeline.tokenViewModel(for: token)
XCTAssertEqual(viewModel2!.balance.value, testValue1)
let testValue2 = BigUInt("20000000000000000000000") let testValue2 = BigUInt("20000000000000000000000")
dep.tokensService.setBalanceTestsOnly(balance: .init(value: testValue2), for: token) dep.tokensService.setBalanceTestsOnly(balance: .init(value: testValue2), for: token)
XCTAssertNotNil(pipeline.tokenViewModel(for: token)) let viewModel3 = await pipeline.tokenViewModel(for: token)
XCTAssertEqual(pipeline.tokenViewModel(for: token)!.balance.value, testValue2) XCTAssertNotNil(viewModel3)
let viewModel4 = await pipeline.tokenViewModel(for: token)
XCTAssertEqual(viewModel4!.balance.value, testValue2)
} }
} }

@ -15,7 +15,7 @@ extension TokensFilter {
extension RealmStore { extension RealmStore {
static func fake(for wallet: Wallet) -> RealmStore { static func fake(for wallet: Wallet) -> RealmStore {
RealmStore(realm: fakeRealm(wallet: wallet), name: RealmStore.threadName(for: wallet)) RealmStore(config: fakeRealm(wallet: wallet).configuration, name: RealmStore.threadName(for: wallet))
} }
} }
extension CurrencyService { extension CurrencyService {

@ -5,18 +5,11 @@ import Combine
@testable import AlphaWalletFoundation @testable import AlphaWalletFoundation
class FileTokenEntriesProviderTests: XCTestCase { class FileTokenEntriesProviderTests: XCTestCase {
func testLoadLocalJsonFile() { func testLoadLocalJsonFile() async {
var cancellables = Set<AnyCancellable>()
let expectation = self.expectation(description: "Wait for publisher") let expectation = self.expectation(description: "Wait for publisher")
FileTokenEntriesProvider().tokenEntries() let tokenEntries = try! await FileTokenEntriesProvider().tokenEntries()
.sink( XCTAssertFalse(tokenEntries.isEmpty)
receiveCompletion: { _ in }, expectation.fulfill()
receiveValue: { value in await fulfillment(of: [expectation], timeout: 0.1)
XCTAssertFalse(value.isEmpty)
expectation.fulfill()
}
).store(in: &cancellables)
wait(for: [expectation], timeout: 0.1)
} }
} }

@ -52,3 +52,32 @@ public extension Publisher {
} }
} }
} }
enum PublisherAsAsyncError: Error {
case finishedWithoutValue
}
//TODO remove most if not all callers once we migrate completely to async-await
public extension AnyPublisher {
func async() async throws -> Output {
try await withCheckedThrowingContinuation { continuation in
var cancellable: AnyCancellable?
var finishedWithoutValue = true
cancellable = first()
.sink { result in
switch result {
case .finished:
if finishedWithoutValue {
continuation.resume(throwing: PublisherAsAsyncError.finishedWithoutValue)
}
case let .failure(error):
continuation.resume(throwing: error)
}
cancellable?.cancel()
} receiveValue: { value in
finishedWithoutValue = false
continuation.resume(with: .success(value))
}
}
}
}

@ -0,0 +1,31 @@
// Copyright © 2023 Stormbird PTE. LTD.
import Foundation
public extension Sequence {
func asyncMap<T>(_ transform: (Element) async throws -> T) async rethrows -> [T] {
var values = [T]()
for element in self {
try await values.append(transform(element))
}
return values
}
func asyncCompactMap<T>(_ transform: (Self.Element) async throws -> T?) async rethrows -> [T] {
var values = [T]()
for element in self {
if let result = try await transform(element) {
values.append(result)
}
}
return values
}
func asyncFlatMap<T: Sequence>(_ transform: (Element) async throws -> T) async rethrows -> [T.Element] {
var values = [T.Element]()
for element in self {
try await values.append(contentsOf: transform(element))
}
return values
}
}

@ -21,7 +21,7 @@ public enum SmartContractError: Error {
public protocol ENSDelegate: AnyObject { public protocol ENSDelegate: AnyObject {
func callSmartContract(withChainId chainId: ChainId, contract: AlphaWallet.Address, functionName: String, abiString: String, parameters: [AnyObject]) -> AnyPublisher<[String: Any], SmartContractError> func callSmartContract(withChainId chainId: ChainId, contract: AlphaWallet.Address, functionName: String, abiString: String, parameters: [AnyObject]) -> AnyPublisher<[String: Any], SmartContractError>
func getSmartContractCallData(withChainId chainId: ChainId, contract: AlphaWallet.Address, functionName: String, abiString: String, parameters: [AnyObject]) -> Data? func getSmartContractCallData(withChainId chainId: ChainId, contract: AlphaWallet.Address, functionName: String, abiString: String, parameters: [AnyObject]) -> Data?
func getInterfaceSupported165(chainId: Int, hash: String, contract: AlphaWallet.Address) -> AnyPublisher<Bool, SmartContractError> func getInterfaceSupported165Async(chainId: Int, hash: String, contract: AlphaWallet.Address) async throws -> Bool
} }
extension ENSDelegate { extension ENSDelegate {
@ -32,6 +32,10 @@ extension ENSDelegate {
func getSmartContractCallData(withChainId chainId: ChainId, contract: AlphaWallet.Address, functionName: String, abiString: String, parameters: [AnyObject] = []) -> Data? { func getSmartContractCallData(withChainId chainId: ChainId, contract: AlphaWallet.Address, functionName: String, abiString: String, parameters: [AnyObject] = []) -> Data? {
getSmartContractCallData(withChainId: chainId, contract: contract, functionName: functionName, abiString: abiString, parameters: parameters) getSmartContractCallData(withChainId: chainId, contract: contract, functionName: functionName, abiString: abiString, parameters: parameters)
} }
func callSmartContractAsync(withChainId chainId: ChainId, contract: AlphaWallet.Address, functionName: String, abiString: String, parameters: [AnyObject]) async throws -> [String: Any] {
try await callSmartContract(withChainId: chainId, contract: contract, functionName: functionName, abiString: abiString, parameters: parameters).async()
}
} }
public class ENS { public class ENS {
@ -48,154 +52,137 @@ public class ENS {
self.chainId = chainId self.chainId = chainId
} }
public func getENSAddress(fromName name: String) -> AnyPublisher<AlphaWallet.Address, SmartContractError> { public func getENSAddress(fromName name: String) async throws -> AlphaWallet.Address {
//if already an address, send back the address //if already an address, send back the address
if let ethAddress = AlphaWallet.Address(string: name) { return .just(ethAddress) } if let ethAddress = AlphaWallet.Address(string: name) { return ethAddress }
//if it does not contain .eth, then it is not a valid ens name //if it does not contain .eth, then it is not a valid ens name
if !name.contains(".") { if !name.contains(".") { throw SmartContractError.embedded(ENSError(description: "Invalid ENS Name")) }
return .fail(.embedded(ENSError(description: "Invalid ENS Name")))
}
return getResolver(forName: name) let resolver = try await getResolver(forName: name)
.flatMap { resolver -> AnyPublisher<(AlphaWallet.Address, Bool), SmartContractError> in let supportsEnsIp10 = try await isSupportEnsIp10(resolver: resolver)
self.isSupportEnsIp10(resolver: resolver).map { (resolver, $0) }.eraseToAnyPublisher() verboseLog("[ENS] Fetch resolver: \(resolver.eip55String) supports ENSIP-10? \(supportsEnsIp10) for name: \(name)")
}.flatMap { resolver, supportsEnsIp10 -> AnyPublisher<AlphaWallet.Address, SmartContractError> in let node = name.lowercased().nameHash
verboseLog("[ENS] Fetch resolver: \(resolver.eip55String) supports ENSIP-10? \(supportsEnsIp10) for name: \(name)") if supportsEnsIp10 {
let node = name.lowercased().nameHash return try await getENSAddressFromResolverUsingResolve(forName: name, node: node, resolver: resolver)
if supportsEnsIp10 { } else {
return self.getENSAddressFromResolverUsingResolve(forName: name, node: node, resolver: resolver) return try await getENSAddressFromResolverUsingAddr(forName: name, node: node, resolver: resolver)
} else { }
return self.getENSAddressFromResolverUsingAddr(forName: name, node: node, resolver: resolver)
}
}.eraseToAnyPublisher()
} }
//Performs a ENS reverse lookup figure out ENS name from a given Ethereum address and then forward resolves the ENS name (look up Ethereum address from ENS name) to verify it. This is necessary because: //Performs a ENS reverse lookup figure out ENS name from a given Ethereum address and then forward resolves the ENS name (look up Ethereum address from ENS name) to verify it. This is necessary because:
// (quoted from https://docs.ens.domains/dapp-developer-guide/resolving-names) // (quoted from https://docs.ens.domains/dapp-developer-guide/resolving-names)
// > "ENS does not enforce the accuracy of reverse records - for instance, anyone may claim that the name for their address is 'alice.eth'. To be certain that the claim is accurate, you must always perform a forward resolution for the returned name and check it matches the original address." // > "ENS does not enforce the accuracy of reverse records - for instance, anyone may claim that the name for their address is 'alice.eth'. To be certain that the claim is accurate, you must always perform a forward resolution for the returned name and check it matches the original address."
public func getName(fromAddress address: AlphaWallet.Address) -> AnyPublisher<String, SmartContractError> { public func getName(fromAddress address: AlphaWallet.Address) async throws -> String {
//TODO improve if delegate is nil //TODO improve if delegate is nil
guard let delegate = delegate else { return .fail(SmartContractError.delegateNotFound) } guard let delegate = delegate else { throw SmartContractError.delegateNotFound }
//TODO extract get resolver and reverse lookup functions
let node = address.nameHash let node = address.nameHash
let function = GetENSResolverEncode() let resolverFunction = GetENSResolverEncode()
let chainId = chainId let chainId = chainId
return delegate.callSmartContract(withChainId: chainId, contract: Self.registrarContract, functionName: function.name, abiString: function.abi, parameters: [node] as [AnyObject]).flatMap { result -> AnyPublisher<[String: Any], SmartContractError> in let resolverResult = try await delegate.callSmartContractAsync(withChainId: chainId, contract: Self.registrarContract, functionName: resolverFunction.name, abiString: resolverFunction.abi, parameters: [node] as [AnyObject])
guard let resolverEthereumAddress = result["0"] as? EthereumAddress else { guard let resolverEthereumAddress = resolverResult["0"] as? EthereumAddress else {
let error = ENSError(description: "Error extracting result from \(Self.registrarContract).\(function.name)()") let error = ENSError(description: "Error extracting result from \(Self.registrarContract).\(resolverFunction.name)()")
return .fail(.embedded(error)) throw SmartContractError.embedded(error)
} }
let resolver = AlphaWallet.Address(address: resolverEthereumAddress) let resolver = AlphaWallet.Address(address: resolverEthereumAddress)
guard !resolver.isNull else { guard !resolver.isNull else {
let error = ENSError(description: "Null address returned") let error = ENSError(description: "Null address returned")
return .fail(.embedded(error)) throw SmartContractError.embedded(error)
} }
let function = ENSReverseLookupEncode() let reverseLookupFunction = ENSReverseLookupEncode()
return delegate.callSmartContract(withChainId: chainId, contract: resolver, functionName: function.name, abiString: function.abi, parameters: [node] as [AnyObject]) let reverseLookupResult = try await delegate.callSmartContractAsync(withChainId: chainId, contract: resolver, functionName: reverseLookupFunction.name, abiString: reverseLookupFunction.abi, parameters: [node] as [AnyObject])
}.flatMap { result -> AnyPublisher<(String, AlphaWallet.Address), SmartContractError> in guard let ensName = reverseLookupResult["0"] as? String, ensName.contains(".") else {
guard let ensName = result["0"] as? String, ensName.contains(".") else { let error = ENSError(description: "Incorrect data output from ENS resolver")
let error = ENSError(description: "Incorrect data output from ENS resolver") throw SmartContractError.embedded(error)
return .fail(.embedded(error)) }
} let resolvedAddress = try await getENSAddress(fromName: ensName)
return self.getENSAddress(fromName: ensName).map { (ensName, $0) }.eraseToAnyPublisher() if address == resolvedAddress {
}.tryMap { ensName, resolvedAddress -> String in return ensName
if address == resolvedAddress { } else {
return ensName throw ENSError(description: "Forward resolution of ENS name found by reverse look up doesn't match")
} else { }
throw ENSError(description: "Forward resolution of ENS name found by reverse look up doesn't match")
}
}.mapError { e in
SmartContractError.embedded(e)
}.eraseToAnyPublisher()
} }
public func getTextRecord(forName name: String, recordKey: EnsTextRecordKey) -> AnyPublisher<String, SmartContractError> { public func getTextRecord(forName name: String, recordKey: EnsTextRecordKey) async throws -> String {
//TODO improve if delegate is nil //TODO improve if delegate is nil
guard let delegate = delegate else { return .fail(.delegateNotFound) } guard let delegate = delegate else { throw SmartContractError.delegateNotFound }
guard !name.components(separatedBy: ".").isEmpty else { guard !name.components(separatedBy: ".").isEmpty else {
return .fail(.embedded(ENSError(description: "\(name) is invalid ENS name"))) throw SmartContractError.embedded(ENSError(description: "\(name) is invalid ENS name"))
} }
let addr = name.lowercased().nameHash let addr = name.lowercased().nameHash
let function = GetEnsTextRecord() let function = GetEnsTextRecord()
let chainId = chainId let chainId = chainId
return delegate.callSmartContract(withChainId: chainId, contract: getENSRecordsContract(forChainId: chainId), functionName: function.name, abiString: function.abi, parameters: [addr as AnyObject, recordKey.rawValue as AnyObject]).tryMap { result -> String in let result = try await delegate.callSmartContractAsync(withChainId: chainId, contract: getENSRecordsContract(forChainId: chainId), functionName: function.name, abiString: function.abi, parameters: [addr as AnyObject, recordKey.rawValue as AnyObject])
guard let record = result["0"] as? String else { throw ENSError(description: "interface doesn't support for chainId \(chainId)") } guard let record = result["0"] as? String else { throw ENSError(description: "interface doesn't support for chainId \(chainId)") }
guard !record.isEmpty else { throw ENSError(description: "ENS text record not found for record: \(record) for chainId: \(chainId)") } guard !record.isEmpty else { throw ENSError(description: "ENS text record not found for record: \(record) for chainId: \(chainId)") }
return record return record
}.mapError { e in
SmartContractError.embedded(e)
}.eraseToAnyPublisher()
} }
private func isSupportEnsIp10(resolver: AlphaWallet.Address) -> AnyPublisher<Bool, SmartContractError> { private func isSupportEnsIp10(resolver: AlphaWallet.Address) async throws -> Bool {
//TODO improve if delegate is nil //TODO improve if delegate is nil
guard let delegate = delegate else { return .fail(.delegateNotFound) } guard let delegate = delegate else { throw SmartContractError.delegateNotFound }
let hash = "0x9061b923" //ENSIP-10 resolve(bytes,bytes)" let hash = "0x9061b923" //ENSIP-10 resolve(bytes,bytes)"
return delegate.getInterfaceSupported165(chainId: chainId, hash: hash, contract: resolver) return try await delegate.getInterfaceSupported165Async(chainId: chainId, hash: hash, contract: resolver)
} }
private func getResolver(forName name: String) -> AnyPublisher<AlphaWallet.Address, SmartContractError> { private func getResolver(forName name: String) async throws -> AlphaWallet.Address {
//TODO improve if delegate is nil //TODO improve if delegate is nil
guard let delegate = delegate else { return .fail(.delegateNotFound) } guard let delegate = delegate else { throw SmartContractError.delegateNotFound }
let function = GetENSResolverEncode() let function = GetENSResolverEncode()
let chainId = chainId let chainId = chainId
let node = name.lowercased().nameHash let node = name.lowercased().nameHash
return delegate.callSmartContract(withChainId: chainId, contract: Self.registrarContract, functionName: function.name, abiString: function.abi, parameters: [node] as [AnyObject]).flatMap { result -> AnyPublisher<AlphaWallet.Address, SmartContractError> in let result = try await delegate.callSmartContractAsync(withChainId: chainId, contract: Self.registrarContract, functionName: function.name, abiString: function.abi, parameters: [node] as [AnyObject])
if let resolver = (result["0"] as? EthereumAddress).flatMap({ AlphaWallet.Address(address: $0) }) { if let resolver = (result["0"] as? EthereumAddress).flatMap({ AlphaWallet.Address(address: $0) }) {
verboseLog("[ENS] fetched resolver: \(resolver) for: \(name) arg: \(node)") verboseLog("[ENS] fetched resolver: \(resolver) for: \(name) arg: \(node)")
if resolver.isNull && name != "" { if resolver.isNull && name != "" {
//Wildcard resolution https://docs.ens.domains/ens-improvement-proposals/ensip-10-wildcard-resolution //Wildcard resolution https://docs.ens.domains/ens-improvement-proposals/ensip-10-wildcard-resolution
let parentName = name.split(separator: ".").dropFirst().joined(separator: ".") let parentName = name.split(separator: ".").dropFirst().joined(separator: ".")
verboseLog("[ENS] fetching parent \(parentName) resolver again for ENSIP-10. Was: \(resolver) for: \(name) arg: \(node)") verboseLog("[ENS] fetching parent \(parentName) resolver again for ENSIP-10. Was: \(resolver) for: \(name) arg: \(node)")
return self.getResolver(forName: parentName) return try await getResolver(forName: parentName)
} else {
if resolver.isNull {
let error = ENSError(description: "Null address returned")
throw SmartContractError.embedded(error)
} else { } else {
if resolver.isNull { return resolver
let error = ENSError(description: "Null address returned")
return .fail(.embedded(error))
} else {
return .just(resolver)
}
} }
} else {
let error = ENSError(description: "Error extracting result from \(Self.registrarContract).\(function.name)()")
return .fail(.embedded(error))
} }
}.eraseToAnyPublisher() } else {
let error = ENSError(description: "Error extracting result from \(Self.registrarContract).\(function.name)()")
throw SmartContractError.embedded(error)
}
} }
private func getENSAddressFromResolverUsingAddr(forName name: String, node: String, resolver: AlphaWallet.Address) -> AnyPublisher<AlphaWallet.Address, SmartContractError> { private func getENSAddressFromResolverUsingAddr(forName name: String, node: String, resolver: AlphaWallet.Address) async throws -> AlphaWallet.Address {
//TODO improve if delegate is nil //TODO improve if delegate is nil
guard let delegate = delegate else { return .fail(.delegateNotFound) } guard let delegate = delegate else { throw SmartContractError.delegateNotFound }
let function = GetENSRecordWithResolverAddrEncode() let function = GetENSRecordWithResolverAddrEncode()
let chainId = chainId let chainId = chainId
verboseLog("[ENS] calling function \(function.name) for name: \(name)") verboseLog("[ENS] calling function \(function.name) for name: \(name)")
return delegate.callSmartContract(withChainId: chainId, contract: resolver, functionName: function.name, abiString: function.abi, parameters: [node] as [AnyObject]).tryMap { result in let result = try await delegate.callSmartContractAsync(withChainId: chainId, contract: resolver, functionName: function.name, abiString: function.abi, parameters: [node] as [AnyObject])
guard let ensAddressEthereumAddress = result["0"] as? EthereumAddress else { throw ENSError(description: "Incorrect data output from ENS resolver") } guard let ensAddressEthereumAddress = result["0"] as? EthereumAddress else { throw ENSError(description: "Incorrect data output from ENS resolver") }
let ensAddress = AlphaWallet.Address(address: ensAddressEthereumAddress) let ensAddress = AlphaWallet.Address(address: ensAddressEthereumAddress)
verboseLog("[ENS] called function \(function.name) for name: \(name) result: \(ensAddress.eip55String)") verboseLog("[ENS] called function \(function.name) for name: \(name) result: \(ensAddress.eip55String)")
guard !ensAddress.isNull else { throw ENSError(description: "Null address returned") } guard !ensAddress.isNull else { throw ENSError(description: "Null address returned") }
return ensAddress return ensAddress
}.mapError { e in
SmartContractError.embedded(e)
}.eraseToAnyPublisher()
} }
private func getENSAddressFromResolverUsingResolve(forName name: String, node: String, resolver: AlphaWallet.Address) -> AnyPublisher<AlphaWallet.Address, SmartContractError> { private func getENSAddressFromResolverUsingResolve(forName name: String, node: String, resolver: AlphaWallet.Address) async throws -> AlphaWallet.Address {
//TODO improve if delegate is nil //TODO improve if delegate is nil
guard let delegate = delegate else { return .fail(.delegateNotFound) } guard let delegate = delegate else { throw SmartContractError.delegateNotFound }
let addrFunction = GetENSRecordWithResolverAddrEncode() let addrFunction = GetENSRecordWithResolverAddrEncode()
let resolveFunction = GetENSRecordWithResolverResolveEncode() let resolveFunction = GetENSRecordWithResolverResolveEncode()
let dnsEncodedName = functional.dnsEncode(name: name) let dnsEncodedName = functional.dnsEncode(name: name)
guard let callData = delegate.getSmartContractCallData(withChainId: chainId, contract: resolver, functionName: addrFunction.name, abiString: addrFunction.abi, parameters: [node] as [AnyObject]) else { guard let callData = try delegate.getSmartContractCallData(withChainId: chainId, contract: resolver, functionName: addrFunction.name, abiString: addrFunction.abi, parameters: [node] as [AnyObject]) else {
struct FailedToBuildCallDataForEnsIp10: Error {} struct FailedToBuildCallDataForEnsIp10: Error {}
return Fail(error: SmartContractError.embedded(FailedToBuildCallDataForEnsIp10())) throw SmartContractError.embedded(FailedToBuildCallDataForEnsIp10())
.eraseToAnyPublisher()
} }
verboseLog("[ENS] addr data calldata: \(callData.hexString)") verboseLog("[ENS] addr data calldata: \(callData.hexString)")
let parameters: [AnyObject] = [ let parameters: [AnyObject] = [
@ -204,17 +191,14 @@ public class ENS {
] ]
let chainId = chainId let chainId = chainId
verboseLog("[ENS] calling function \(resolveFunction.name) for name: \(name) DNS-encoded name: \(dnsEncodedName.hex()) callData: \(callData.hex())") verboseLog("[ENS] calling function \(resolveFunction.name) for name: \(name) DNS-encoded name: \(dnsEncodedName.hex()) callData: \(callData.hex())")
return delegate.callSmartContract(withChainId: chainId, contract: resolver, functionName: resolveFunction.name, abiString: resolveFunction.abi, parameters: parameters).tryMap { result in let result = try await delegate.callSmartContractAsync(withChainId: chainId, contract: resolver, functionName: resolveFunction.name, abiString: resolveFunction.abi, parameters: parameters)
guard let addressStringAsData = result["0"] as? Data else { throw ENSError(description: "Incorrect data output from ENS resolver") } guard let addressStringAsData = result["0"] as? Data else { throw ENSError(description: "Incorrect data output from ENS resolver") }
let addressStringLeftPaddedWithZeros = addressStringAsData.hexString let addressStringLeftPaddedWithZeros = addressStringAsData.hexString
let addressString = String(addressStringLeftPaddedWithZeros.dropFirst(addressStringLeftPaddedWithZeros.count - 40)) let addressString = String(addressStringLeftPaddedWithZeros.dropFirst(addressStringLeftPaddedWithZeros.count - 40))
verboseLog("[ENS] called function \(resolveFunction.name) for name: \(name) result: \(addressString)") verboseLog("[ENS] called function \(resolveFunction.name) for name: \(name) result: \(addressString)")
guard let address = AlphaWallet.Address(uncheckedAgainstNullAddress: addressString) else { throw ENSError(description: "Incorrect data output from ENS resolver") } guard let address = AlphaWallet.Address(uncheckedAgainstNullAddress: addressString) else { throw ENSError(description: "Incorrect data output from ENS resolver") }
guard !address.isNull else { throw ENSError(description: "Null address returned") } guard !address.isNull else { throw ENSError(description: "Null address returned") }
return address return address
}.mapError { e in
SmartContractError.embedded(e)
}.eraseToAnyPublisher()
} }
private func getENSRecordsContract(forChainId chainId: ChainId) -> AlphaWallet.Address { private func getENSRecordsContract(forChainId chainId: ChainId) -> AlphaWallet.Address {

@ -37,7 +37,11 @@ class ActivitiesGenerator {
func generateActivities() -> AnyPublisher<[ActivityTokenObjectTokenHolder], Never> { func generateActivities() -> AnyPublisher<[ActivityTokenObjectTokenHolder], Never> {
let tokens = sessionsProvider.sessions let tokens = sessionsProvider.sessions
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.map { self.getTokensForActivities(servers: Array($0.keys)) } .flatMap { sessions in
asFuture {
await self.getTokensForActivities(servers: Array(sessions.keys))
}
}
let eventsForActivities = sessionsProvider.sessions let eventsForActivities = sessionsProvider.sessions
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
@ -47,14 +51,14 @@ class ActivitiesGenerator {
return Publishers.CombineLatest(tokens, eventsForActivities) return Publishers.CombineLatest(tokens, eventsForActivities)
.map { self.getTokensAndXmlHandlers(tokens: $0.0) } .map { self.getTokensAndXmlHandlers(tokens: $0.0) }
.map { self.getContractsAndCards(contractServerXmlHandlers: $0) } .map { self.getContractsAndCards(contractServerXmlHandlers: $0) }
.map { self.getActivitiesAndTokens(contractsAndCards: $0) } .flatMap { contractsAndCards in asFuture { await self.getActivitiesAndTokens(contractsAndCards: contractsAndCards) } }
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
private func getTokensForActivities(servers: [RPCServer]) -> [Token] { private func getTokensForActivities(servers: [RPCServer]) async -> [Token] {
switch transactionsFilterStrategy { switch transactionsFilterStrategy {
case .all: case .all:
return tokensService.tokens(for: servers) return await tokensService.tokens(for: servers)
case .filter(_, let token): case .filter(_, let token):
precondition(servers.contains(token.server), "fatal error, no session for server: \(token.server)") precondition(servers.contains(token.server), "fatal error, no session for server: \(token.server)")
return [token] return [token]
@ -113,11 +117,11 @@ class ActivitiesGenerator {
return contractsAndCardsOptional.flatMap { $0 } return contractsAndCardsOptional.flatMap { $0 }
} }
private func getActivitiesAndTokens(contractsAndCards: ContractsAndCards) -> [ActivityTokenObjectTokenHolder] { private func getActivitiesAndTokens(contractsAndCards: ContractsAndCards) async -> [ActivityTokenObjectTokenHolder] {
var activitiesAndTokens: [ActivityTokenObjectTokenHolder] = .init() var activitiesAndTokens: [ActivityTokenObjectTokenHolder] = .init()
//NOTE: here is a lot of calculations, `contractsAndCards` could reach up of 1000 items, as well as recentEvents could reach 1000.Simply it call inner function 1 000 000 times //NOTE: here is a lot of calculations, `contractsAndCards` could reach up of 1000 items, as well as recentEvents could reach 1000.Simply it call inner function 1 000 000 times
for (contract, server, card, interpolatedFilter) in contractsAndCards { for (contract, server, card, interpolatedFilter) in contractsAndCards {
let activities = getActivities(contract: contract, server: server, card: card, interpolatedFilter: interpolatedFilter) let activities = await getActivities(contract: contract, server: server, card: card, interpolatedFilter: interpolatedFilter)
//NOTE: filter activities to avoid: `Fatal error: Duplicate values for key: '<id>'` //NOTE: filter activities to avoid: `Fatal error: Duplicate values for key: '<id>'`
let filteredActivities = activities.filter { data in !activitiesAndTokens.contains(where: { $0.activity.id == data.activity.id }) } let filteredActivities = activities.filter { data in !activitiesAndTokens.contains(where: { $0.activity.id == data.activity.id }) }
activitiesAndTokens.append(contentsOf: filteredActivities) activitiesAndTokens.append(contentsOf: filteredActivities)
@ -139,17 +143,13 @@ class ActivitiesGenerator {
} }
} }
private func getActivities(contract: AlphaWallet.Address, private func getActivities(contract: AlphaWallet.Address, server: RPCServer, card: TokenScriptCard, interpolatedFilter: String) async -> [ActivityTokenObjectTokenHolder] {
server: RPCServer,
card: TokenScriptCard,
interpolatedFilter: String) -> [ActivityTokenObjectTokenHolder] {
//NOTE: eventsActivityDataStore. getRecentEvents() returns only 100 events, that could cause error with creating activities (missing events) //NOTE: eventsActivityDataStore. getRecentEvents() returns only 100 events, that could cause error with creating activities (missing events)
//replace with fetching only filtered event instances, //replace with fetching only filtered event instances,
let events = eventsActivityDataStore.getRecentEventsSortedByBlockNumber(for: card.eventOrigin.contract, server: server, eventName: card.eventOrigin.eventName, interpolatedFilter: interpolatedFilter) let events = await eventsActivityDataStore.getRecentEventsSortedByBlockNumber(for: card.eventOrigin.contract, server: server, eventName: card.eventOrigin.eventName, interpolatedFilter: interpolatedFilter)
let activitiesForThisCard: [ActivityTokenObjectTokenHolder] = events.compactMap { eachEvent -> ActivityTokenObjectTokenHolder? in let activitiesForThisCard: [ActivityTokenObjectTokenHolder] = await events.asyncCompactMap { eachEvent -> ActivityTokenObjectTokenHolder? in
guard let token = tokensService.token(for: contract, server: server) else { return nil } guard let token = await tokensService.token(for: contract, server: server) else { return nil }
guard let session = sessionsProvider.session(for: token.server) else { return nil } guard let session = sessionsProvider.session(for: token.server) else { return nil }
let implicitAttributes = generateImplicitAttributesForToken(contract: contract, server: server, symbol: token.symbol) let implicitAttributes = generateImplicitAttributesForToken(contract: contract, server: server, symbol: token.symbol)
@ -183,7 +183,7 @@ class ActivitiesGenerator {
tokenHolders = [TokenHolder(tokens: [tokenScriptToken], contractAddress: token.contractAddress, hasAssetDefinition: true)] tokenHolders = [TokenHolder(tokens: [tokenScriptToken], contractAddress: token.contractAddress, hasAssetDefinition: true)]
} else { } else {
tokenHolders = session.tokenAdaptor.getTokenHolders(token: token) tokenHolders = await session.tokenAdaptor.getTokenHolders(token: token)
} }
tokensAndTokenHolders[token.addressAndRPCServer] = tokenHolders tokensAndTokenHolders[token.addressAndRPCServer] = tokenHolders
} }

@ -135,29 +135,26 @@ public class ActivitiesService: ActivitiesServiceType {
} }
private func combineActivitiesWithTransactions() { private func combineActivitiesWithTransactions() {
let transactions = transactionDataStore.transactions( Task { @MainActor in
forFilter: transactionsFilterStrategy, let transactions = await transactionDataStore.transactions(forFilter: transactionsFilterStrategy, servers: Array(sessionsProvider.activeSessions.keys), oldestBlockNumber: activities.last?.blockNumber)
servers: Array(sessionsProvider.activeSessions.keys), let items = await combine(activities: activities.all, with: transactions)
oldestBlockNumber: activities.last?.blockNumber) let activities = ActivityCollection.sorted(activities: items)
activitiesSubject.send(activities)
let items = combine(activities: activities.all, with: transactions) }
let activities = ActivityCollection.sorted(activities: items)
activitiesSubject.send(activities)
} }
//Combining includes filtering around activities (from events) for ERC20 send/receive transactions which are already covered by transactions //Combining includes filtering around activities (from events) for ERC20 send/receive transactions which are already covered by transactions
private func combine(activities: [Activity], with transactions: [Transaction]) -> [ActivityRowModel] { private func combine(activities: [Activity], with transactions: [Transaction]) async -> [ActivityRowModel] {
let all: [ActivityOrTransactionInstance] = activities.map { .activity($0) } + transactions.map { .transaction($0) } let all: [ActivityOrTransactionInstance] = activities.map { .activity($0) } + transactions.map { .transaction($0) }
let sortedAll: [ActivityOrTransactionInstance] = all.sorted { $0.blockNumber < $1.blockNumber } let sortedAll: [ActivityOrTransactionInstance] = all.sorted { $0.blockNumber < $1.blockNumber }
let counters = Dictionary(grouping: sortedAll, by: \.blockNumber) let counters = Dictionary(grouping: sortedAll, by: \.blockNumber)
return counters.map { return await counters.asyncMap {
generateRowModels(activityOrTransactions: $0.value, blockNumber: $0.key) await generateRowModels(activityOrTransactions: $0.value, blockNumber: $0.key)
}.flatMap { $0 } }.flatMap { $0 }
} }
private func generateRowModels(activityOrTransactions: [ActivityOrTransactionInstance], blockNumber: Int) -> [ActivityRowModel] { private func generateRowModels(activityOrTransactions: [ActivityOrTransactionInstance], blockNumber: Int) async -> [ActivityRowModel] {
if activityOrTransactions.isEmpty { if activityOrTransactions.isEmpty {
//Shouldn't be possible //Shouldn't be possible
return .init() return .init()
@ -170,7 +167,7 @@ public class ActivitiesService: ActivitiesServiceType {
let operations = transaction.localizedOperations let operations = transaction.localizedOperations
return operations.allSatisfy { activity != $0 } return operations.allSatisfy { activity != $0 }
} }
let activity = ActivityCollection.functional.createPseudoActivity(fromTransactionRow: .standalone(transaction), tokensService: tokensService, wallet: wallet.address) let activity = await ActivityCollection.functional.createPseudoActivity(fromTransactionRow: .standalone(transaction), tokensService: tokensService, wallet: wallet.address)
if transaction.localizedOperations.isEmpty && activities.isEmpty { if transaction.localizedOperations.isEmpty && activities.isEmpty {
results.append(.standaloneTransaction(transaction: transaction, activity: activity)) results.append(.standaloneTransaction(transaction: transaction, activity: activity))
} else if transaction.localizedOperations.count == 1, transaction.value == "0", activities.isEmpty { } else if transaction.localizedOperations.count == 1, transaction.value == "0", activities.isEmpty {
@ -182,8 +179,8 @@ public class ActivitiesService: ActivitiesServiceType {
let isSwap = self.isSwap(activities: activities, operations: transaction.localizedOperations, wallet: wallet) let isSwap = self.isSwap(activities: activities, operations: transaction.localizedOperations, wallet: wallet)
results.append(.parentTransaction(transaction: transaction, isSwap: isSwap, activities: activities)) results.append(.parentTransaction(transaction: transaction, isSwap: isSwap, activities: activities))
results.append(contentsOf: transaction.localizedOperations.map { results.append(contentsOf: await transaction.localizedOperations.asyncMap {
let activity = ActivityCollection.functional.createPseudoActivity(fromTransactionRow: .item(transaction: transaction, operation: $0), tokensService: tokensService, wallet: wallet.address) let activity = await ActivityCollection.functional.createPseudoActivity(fromTransactionRow: .item(transaction: transaction, operation: $0), tokensService: tokensService, wallet: wallet.address)
return .childTransaction(transaction: transaction, operation: $0, activity: activity) return .childTransaction(transaction: transaction, operation: $0, activity: activity)
}) })
for each in activities { for each in activities {
@ -200,7 +197,7 @@ public class ActivitiesService: ActivitiesServiceType {
case .activity(let activity): case .activity(let activity):
return [.standaloneActivity(activity: activity)] return [.standaloneActivity(activity: activity)]
case .transaction(let transaction): case .transaction(let transaction):
let activity = ActivityCollection.functional.createPseudoActivity(fromTransactionRow: .standalone(transaction), tokensService: tokensService, wallet: wallet.address) let activity = await ActivityCollection.functional.createPseudoActivity(fromTransactionRow: .standalone(transaction), tokensService: tokensService, wallet: wallet.address)
if transaction.localizedOperations.isEmpty { if transaction.localizedOperations.isEmpty {
return [.standaloneTransaction(transaction: transaction, activity: activity)] return [.standaloneTransaction(transaction: transaction, activity: activity)]
} else if transaction.localizedOperations.count == 1 { } else if transaction.localizedOperations.count == 1 {
@ -209,8 +206,8 @@ public class ActivitiesService: ActivitiesServiceType {
let isSwap = self.isSwap(activities: activities.all, operations: transaction.localizedOperations, wallet: wallet) let isSwap = self.isSwap(activities: activities.all, operations: transaction.localizedOperations, wallet: wallet)
var results: [ActivityRowModel] = .init() var results: [ActivityRowModel] = .init()
results.append(.parentTransaction(transaction: transaction, isSwap: isSwap, activities: .init())) results.append(.parentTransaction(transaction: transaction, isSwap: isSwap, activities: .init()))
results.append(contentsOf: transaction.localizedOperations.map { results.append(contentsOf: await transaction.localizedOperations.asyncMap {
let activity = ActivityCollection.functional.createPseudoActivity(fromTransactionRow: .item(transaction: transaction, operation: $0), tokensService: tokensService, wallet: wallet.address) let activity = await ActivityCollection.functional.createPseudoActivity(fromTransactionRow: .item(transaction: transaction, operation: $0), tokensService: tokensService, wallet: wallet.address)
return .childTransaction(transaction: transaction, operation: $0, activity: activity) return .childTransaction(transaction: transaction, operation: $0, activity: activity)
}) })

@ -207,7 +207,7 @@ extension ActivityCollection {
extension ActivityCollection.functional { extension ActivityCollection.functional {
public static func extractTokenAndActivityName(fromTransactionRow transactionRow: TransactionRow, tokensService: TokensService, wallet: AlphaWallet.Address) -> (token: Token, activityName: String)? { public static func extractTokenAndActivityName(fromTransactionRow transactionRow: TransactionRow, tokensService: TokensService, wallet: AlphaWallet.Address) async -> (token: Token, activityName: String)? {
enum TokenOperation { enum TokenOperation {
case nativeCryptoTransfer(Token) case nativeCryptoTransfer(Token)
case completedTransfer(Token) case completedTransfer(Token)
@ -238,13 +238,29 @@ extension ActivityCollection.functional {
//Explicitly listing out combinations so future changes to enums will be caught by compiler //Explicitly listing out combinations so future changes to enums will be caught by compiler
switch (transactionRow.state, transactionRow.operation?.operationType) { switch (transactionRow.state, transactionRow.operation?.operationType) {
case (.pending, .nativeCurrencyTokenTransfer), (.pending, .erc20TokenTransfer), (.pending, .erc721TokenTransfer), (.pending, .erc875TokenTransfer), (.pending, .erc1155TokenTransfer): case (.pending, .nativeCurrencyTokenTransfer), (.pending, .erc20TokenTransfer), (.pending, .erc721TokenTransfer), (.pending, .erc875TokenTransfer), (.pending, .erc1155TokenTransfer):
erc20TokenOperation = transactionRow.operation?.contractAddress.flatMap { tokensService.token(for: $0, server: transactionRow.server) }.flatMap { TokenOperation.pendingTransfer($0) } if let address = transactionRow.operation?.contractAddress {
erc20TokenOperation = await tokensService.token(for: address, server: transactionRow.server).flatMap { TokenOperation.pendingTransfer($0) }
} else {
erc20TokenOperation = nil
}
case (.completed, .nativeCurrencyTokenTransfer), (.completed, .erc20TokenTransfer), (.completed, .erc721TokenTransfer), (.completed, .erc875TokenTransfer), (.completed, .erc1155TokenTransfer): case (.completed, .nativeCurrencyTokenTransfer), (.completed, .erc20TokenTransfer), (.completed, .erc721TokenTransfer), (.completed, .erc875TokenTransfer), (.completed, .erc1155TokenTransfer):
erc20TokenOperation = transactionRow.operation?.contractAddress.flatMap { tokensService.token(for: $0, server: transactionRow.server) }.flatMap { TokenOperation.completedTransfer($0) } if let address = transactionRow.operation?.contractAddress {
erc20TokenOperation = await tokensService.token(for: address, server: transactionRow.server) .flatMap { TokenOperation.completedTransfer($0) }
} else {
erc20TokenOperation = nil
}
case (.pending, .erc20TokenApprove): case (.pending, .erc20TokenApprove):
erc20TokenOperation = transactionRow.operation?.contractAddress.flatMap { tokensService.token(for: $0, server: transactionRow.server) }.flatMap { TokenOperation.pendingErc20Approval($0) } if let address = transactionRow.operation?.contractAddress {
erc20TokenOperation = await tokensService.token(for: address, server: transactionRow.server) .flatMap { TokenOperation.pendingErc20Approval($0) }
} else {
erc20TokenOperation = nil
}
case (.completed, .erc20TokenApprove): case (.completed, .erc20TokenApprove):
erc20TokenOperation = transactionRow.operation?.contractAddress.flatMap { tokensService.token(for: $0, server: transactionRow.server) }.flatMap { TokenOperation.completedErc20Approval($0) } if let address = transactionRow.operation?.contractAddress {
erc20TokenOperation = await tokensService.token(for: address, server: transactionRow.server) .flatMap { TokenOperation.completedErc20Approval($0) }
} else {
erc20TokenOperation = nil
}
case (.pending, .erc721TokenApproveAll): case (.pending, .erc721TokenApproveAll):
//TODO support ERC721 setApprovalForAll() //TODO support ERC721 setApprovalForAll()
erc20TokenOperation = .none erc20TokenOperation = .none
@ -270,8 +286,8 @@ extension ActivityCollection.functional {
return (token: token, activityName: activityName) return (token: token, activityName: activityName)
} }
static func createPseudoActivity(fromTransactionRow transactionRow: TransactionRow, tokensService: TokensService, wallet: AlphaWallet.Address) -> Activity? { static func createPseudoActivity(fromTransactionRow transactionRow: TransactionRow, tokensService: TokensService, wallet: AlphaWallet.Address) async -> Activity? {
guard let (token, activityName) = extractTokenAndActivityName(fromTransactionRow: transactionRow, tokensService: tokensService, wallet: wallet) else { return nil } guard let (token, activityName) = await extractTokenAndActivityName(fromTransactionRow: transactionRow, tokensService: tokensService, wallet: wallet) else { return nil }
var cardAttributes = [AttributeId: AssetInternalValue]() var cardAttributes = [AttributeId: AssetInternalValue]()
cardAttributes.setSymbol(string: transactionRow.server.symbol) cardAttributes.setSymbol(string: transactionRow.server.symbol)

@ -13,7 +13,7 @@ import AlphaWalletENS
import AlphaWalletCore import AlphaWalletCore
public protocol NftAssetImageProvider: AnyObject { public protocol NftAssetImageProvider: AnyObject {
func assetImageUrl(for url: Eip155URL) -> AnyPublisher<URL, PromiseError> func assetImageUrl(for url: Eip155URL) async throws -> URL
} }
public class BlockiesGenerator { public class BlockiesGenerator {
@ -41,68 +41,57 @@ public class BlockiesGenerator {
self.storage = storage self.storage = storage
} }
public func getBlockieOrEnsAvatarImage(address: AlphaWallet.Address, ens: String? = nil, size: Int = 8, scale: Int = 3, fallbackImage: BlockiesImage) -> AnyPublisher<BlockiesImage, Never> { public func getBlockieOrEnsAvatarImage(address: AlphaWallet.Address, ens: String? = nil, size: Int = 8, scale: Int = 3, fallbackImage: BlockiesImage) async -> BlockiesImage {
return getBlockieOrEnsAvatarImage(address: address, ens: ens, size: size, scale: size) do {
.prepend(fallbackImage) return try await getBlockieOrEnsAvatarImage(address: address, ens: ens, size: size, scale: size)
.replaceError(with: fallbackImage) } catch {
.eraseToAnyPublisher() return fallbackImage
}
} }
public func getBlockieOrEnsAvatarImage(address: AlphaWallet.Address, ens: String? = nil, size: Int = 8, scale: Int = 3) -> AnyPublisher<BlockiesImage, SmartContractError> { public func getBlockieOrEnsAvatarImage(address: AlphaWallet.Address, ens: String? = nil, size: Int = 8, scale: Int = 3) async throws -> BlockiesImage {
if let cached = self.cachedBlockie(address: address, size: .sized(size: size, scale: scale)) { if let cached = self.cachedBlockie(address: address, size: .sized(size: size, scale: scale)) {
return .just(cached) return cached
} }
func generageBlockieFallback() -> AnyPublisher<BlockiesImage, SmartContractError> { func generateBlockieFallback() async throws -> BlockiesImage {
return createBlockieImage(address: address, size: size, scale: scale) let blockie = try await createBlockieImage(address: address, size: size, scale: scale)
.receive(on: queue) //NOTE: to make sure that updating storage is thread safe self.cacheBlockie(address: address, blockie: blockie, size: .sized(size: size, scale: scale))
.handleEvents(receiveOutput: { blockie in return blockie
self.cacheBlockie(address: address, blockie: blockie, size: .sized(size: size, scale: scale))
}).mapError { SmartContractError.embedded($0) }
.eraseToAnyPublisher()
} }
return Just(address) do {
.setFailureType(to: SmartContractError.self) let blockie = try await fetchEnsAvatar(for: address, ens: ens)
.receive(on: queue) cacheBlockie(address: address, blockie: blockie, size: .none)
.flatMap { [queue] address -> AnyPublisher<BlockiesImage, SmartContractError> in return blockie
return self.fetchEnsAvatar(for: address, ens: ens) } catch {
.receive(on: queue) //TODO not cache this fallback too? Performance?
.handleEvents(receiveOutput: { blockie in return try await generateBlockieFallback()
self.cacheBlockie(address: address, blockie: blockie, size: .none) }
}).catch { _ in return generageBlockieFallback() }
.eraseToAnyPublisher()
}.receive(on: RunLoop.main)
.eraseToAnyPublisher()
} }
private func fetchEnsAvatar(for address: AlphaWallet.Address, ens: String?) -> AnyPublisher<BlockiesImage, SmartContractError> { private func fetchEnsAvatar(for address: AlphaWallet.Address, ens: String?) async throws -> BlockiesImage {
return ensTextRecordFetcher.getEnsAvatar(for: address, ens: ens) let imageOrEip155 = try await ensTextRecordFetcher.getEnsAvatar(for: address, ens: ens)
.flatMap { imageOrEip155 -> AnyPublisher<BlockiesImage, SmartContractError> in switch imageOrEip155 {
switch imageOrEip155 { case .image(let img, _):
case .image(let img, _): return img
return .just(img) case .eip155(let url, let raw):
case .eip155(let url, let raw): return try await getImageFromOpenSea(for: url, rawUrl: raw, nameOrAddress: ens ?? address.eip55String)
return self.getImageFromOpenSea(for: url, rawUrl: raw, nameOrAddress: ens ?? address.eip55String) }
}
}.eraseToAnyPublisher()
} }
private func getImageFromOpenSea(for url: Eip155URL, rawUrl: String, nameOrAddress: String) -> AnyPublisher<BlockiesImage, SmartContractError> { private func getImageFromOpenSea(for url: Eip155URL, rawUrl: String, nameOrAddress: String) async throws -> BlockiesImage {
return assetImageProvider.assetImageUrl(for: url) do {
.mapError { SmartContractError.embedded($0) } let url = try await assetImageProvider.assetImageUrl(for: url)
//NOTE: cache fetched open sea image url and rewrite ens avatar with new image //NOTE: cache fetched open sea image url and rewrite ens avatar with new image
.handleEvents(receiveOutput: { [storage] url in let key = DomainNameLookupKey(nameOrAddress: nameOrAddress, server: .forResolvingDomainNames, record: .avatar)
let key = DomainNameLookupKey(nameOrAddress: nameOrAddress, server: .forResolvingDomainNames, record: .avatar) await storage.addOrUpdate(record: .init(key: key, value: .record(url.absoluteString)))
storage.addOrUpdate(record: .init(key: key, value: .record(url.absoluteString))) let blockies = BlockiesImage.url(url: WebImageURL(url: url, rewriteGoogleContentSizeUrl: .s120), isEnsAvatar: true)
}).map { url -> BlockiesImage in return blockies
return .url(url: WebImageURL(url: url, rewriteGoogleContentSizeUrl: .s120), isEnsAvatar: true) } catch {
}.catch { error -> AnyPublisher<BlockiesImage, SmartContractError> in guard let url = URL(string: rawUrl) else { throw SmartContractError.embedded(error) }
guard let url = URL(string: rawUrl) else { return .fail(.embedded(error)) } return BlockiesImage.url(url: WebImageURL(url: url, rewriteGoogleContentSizeUrl: .s120), isEnsAvatar: true)
}
return .just(.url(url: WebImageURL(url: url, rewriteGoogleContentSizeUrl: .s120), isEnsAvatar: true))
}.share()
.eraseToAnyPublisher()
} }
private func cacheBlockie(address: AlphaWallet.Address, blockie: BlockiesImage, size: BlockieSize) { private func cacheBlockie(address: AlphaWallet.Address, blockie: BlockiesImage, size: BlockieSize) {
@ -125,24 +114,16 @@ public class BlockiesGenerator {
} }
} }
private func createBlockieImage(address: AlphaWallet.Address, size: Int, scale: Int) -> AnyPublisher<BlockiesImage, PromiseError> { private func createBlockieImage(address: AlphaWallet.Address, size: Int, scale: Int) async throws -> BlockiesImage {
enum AnyError: Error { enum AnyError: Error {
case blockieCreateFailure case blockieCreateFailure
} }
return Deferred { let blockies = Blockies(seed: address.eip55String, size: size, scale: scale)
Future<BlockiesImage, PromiseError> { seal in if let image = blockies.createImage() {
DispatchQueue.global().async { return .image(image: image, isEnsAvatar: false)
let blockies = Blockies(seed: address.eip55String, size: size, scale: scale) } else {
DispatchQueue.main.async { throw AnyError.blockieCreateFailure
if let image = blockies.createImage() { }
seal(.success(.image(image: image, isEnsAvatar: false)))
} else {
seal(.failure(.some(error: AnyError.blockieCreateFailure)))
}
}
}
}
}.eraseToAnyPublisher()
} }
} }

@ -9,6 +9,7 @@ public final class BookmarksStore {
return realm.objects(Bookmark.self) return realm.objects(Bookmark.self)
.sorted(byKeyPath: "order", ascending: true) .sorted(byKeyPath: "order", ascending: true)
} }
//TODO should use a RealmStore.performInBackground or similar
private let realm: Realm private let realm: Realm
public var bookmarksChangeset: AnyPublisher<ChangeSet<[BookmarkObject]>, Never> { public var bookmarksChangeset: AnyPublisher<ChangeSet<[BookmarkObject]>, Never> {

@ -5,6 +5,7 @@ import RealmSwift
import Combine import Combine
public final class BrowserHistoryStorage { public final class BrowserHistoryStorage {
//TODO should use a RealmStore.performInBackground or similar
private let realm: Realm private let realm: Realm
private let ignoreUrls: Set<URL> private let ignoreUrls: Set<URL>
@ -23,7 +24,7 @@ public final class BrowserHistoryStorage {
} }
}.eraseToAnyPublisher() }.eraseToAnyPublisher()
} }
public var firstHistoryRecord: BrowserHistoryRecord? { public var firstHistoryRecord: BrowserHistoryRecord? {
histories.first histories.first
} }

@ -16,9 +16,9 @@ public class BaseCoinTickersFetcher: CoinTickersFetcher {
private let networking: CoinTickerNetworking private let networking: CoinTickerNetworking
private let tickerIdsFetcher: TickerIdsFetcher private let tickerIdsFetcher: TickerIdsFetcher
/// Cached fetch ticker prices operations /// Cached fetch ticker prices operations
private var inlightPromises: AtomicDictionary<FetchTickerKey, AnyCancellable> = .init() private var inflightFetchers: AtomicDictionary<FetchTickerKey, Task<Void, Never>> = .init()
/// Resolving ticker ids operations /// Resolving ticker ids operations
private var tickerResolvers: AtomicDictionary<TokenMappedToTicker, AnyCancellable> = .init() private var inflightResolvers: AtomicDictionary<TokenMappedToTicker, Task<Void, Never>> = .init()
public init(networking: CoinTickerNetworking, storage: CoinTickersStorage & ChartHistoryStorage & TickerIdsStorage, tickerIdsFetcher: TickerIdsFetcher) { public init(networking: CoinTickerNetworking, storage: CoinTickersStorage & ChartHistoryStorage & TickerIdsStorage, tickerIdsFetcher: TickerIdsFetcher) {
self.networking = networking self.networking = networking
@ -32,8 +32,8 @@ public class BaseCoinTickersFetcher: CoinTickersFetcher {
} }
public func cancel() { public func cancel() {
inlightPromises.values.values.forEach { $0.cancel() } inflightFetchers.values.values.forEach { $0.cancel() }
inlightPromises.removeAll() inflightFetchers.removeAll()
} }
private struct FetchTickerKey: Hashable { private struct FetchTickerKey: Hashable {
@ -53,60 +53,63 @@ public class BaseCoinTickersFetcher: CoinTickersFetcher {
public func fetchTickers(for tokens: [TokenMappedToTicker], force: Bool = false, currency: Currency) { public func fetchTickers(for tokens: [TokenMappedToTicker], force: Bool = false, currency: Currency) {
//NOTE: cancel all previous requests for prev currency //NOTE: cancel all previous requests for prev currency
inlightPromises.removeAll { $0.currency != currency } inflightFetchers.removeAll { $0.currency != currency }
let targetTokensToFetchTickers = tokens.filter { Task { @MainActor in
let key = FetchTickerKey(contractAddress: $0.contractAddress, server: $0.server, currency: currency) var targetTokensToFetchTickers: [TokenMappedToTicker] = []
if inlightPromises[key] != nil { for each in tokens {
return false let key = FetchTickerKey(contractAddress: each.contractAddress, server: each.server, currency: currency)
} else { if inflightFetchers[key] != nil {
return force || hasExpiredTickersLifeTimeSinceLastUpdate(for: $0, currency: currency) return
} else {
let include = await hasExpiredTickersLifeTimeSinceLastUpdate(for: each, currency: currency)
if force || include {
targetTokensToFetchTickers.append(each)
}
}
} }
}
guard !targetTokensToFetchTickers.isEmpty else { return } guard !targetTokensToFetchTickers.isEmpty else { return }
//NOTE: use shared loading tickers operation for batch of tokens //NOTE: use shared loading tickers operation for batch of tokens
let operation = fetchBatchOfTickers(for: targetTokensToFetchTickers, currency: currency) let task = Task<Void, Never> {
.sink(receiveCompletion: { [inlightPromises] _ in do {
let tickers = try await fetchBatchOfTickers(for: targetTokensToFetchTickers, currency: currency)
storage.addOrUpdate(tickers: tickers)
} catch {}
for token in targetTokensToFetchTickers { for token in targetTokensToFetchTickers {
let key = FetchTickerKey(contractAddress: token.contractAddress, server: token.server, currency: currency) let key = FetchTickerKey(contractAddress: token.contractAddress, server: token.server, currency: currency)
inlightPromises.removeValue(forKey: key) inflightFetchers[key] = nil
} }
}, receiveValue: { [storage] in storage.addOrUpdate(tickers: $0) }) }
for token in targetTokensToFetchTickers { for token in targetTokensToFetchTickers {
let key = FetchTickerKey(contractAddress: token.contractAddress, server: token.server, currency: currency) let key = FetchTickerKey(contractAddress: token.contractAddress, server: token.server, currency: currency)
inlightPromises[key] = operation inflightFetchers[key] = task
}
} }
} }
public func resolveTickerIds(for tokens: [TokenMappedToTicker]) { public func resolveTickerIds(for tokens: [TokenMappedToTicker]) {
for each in tokens { for each in tokens {
guard tickerResolvers[each] == nil else { continue } guard inflightResolvers[each] == nil else { continue }
let task = Task<Void, Never> { @MainActor in
tickerResolvers[each] = tickerIdsFetcher.tickerId(for: each) await tickerIdsFetcher.tickerId(for: each)
.handleEvents(receiveCompletion: { [tickerResolvers] _ in inflightResolvers[each] = nil
tickerResolvers.removeValue(forKey: each) }
}, receiveCancel: { [tickerResolvers] in inflightResolvers[each] = task
tickerResolvers.removeValue(forKey: each)
}).sink { _ in }
} }
} }
/// Returns cached chart history if its not expired otherwise download a new version of history, if ticker id has found /// Returns cached chart history if its not expired otherwise download a new version of history, if ticker id has found
public func fetchChartHistories(for token: TokenMappedToTicker, force: Bool, periods: [ChartHistoryPeriod], currency: Currency) -> AnyPublisher<[ChartHistoryPeriod: ChartHistory], Never> { public func fetchChartHistories(for token: TokenMappedToTicker, force: Bool, periods: [ChartHistoryPeriod], currency: Currency) async -> [ChartHistoryPeriod: ChartHistory] {
let publishers = periods.map { fetchChartHistory(force: force, period: $0, for: token, currency: currency) } let unorderedHistoryToPeriods = await periods.asyncMap { await fetchChartHistory(force: force, period: $0, for: token, currency: currency) }
let historyToPeriods = unorderedHistoryToPeriods.reorder(by: periods)
return Publishers.MergeMany(publishers).collect() var values: [ChartHistoryPeriod: ChartHistory] = [:]
.map { $0.reorder(by: periods) } for each in historyToPeriods {
.map { mapped -> [ChartHistoryPeriod: ChartHistory] in values[each.period] = each.history
var values: [ChartHistoryPeriod: ChartHistory] = [:] }
for each in mapped { return values
values[each.period] = each.history
}
return values
}.eraseToAnyPublisher()
} }
struct HistoryToPeriod { struct HistoryToPeriod {
@ -114,31 +117,26 @@ public class BaseCoinTickersFetcher: CoinTickersFetcher {
let history: ChartHistory let history: ChartHistory
} }
private func fetchChartHistory(force: Bool, period: ChartHistoryPeriod, for token: TokenMappedToTicker, currency: Currency) -> AnyPublisher<HistoryToPeriod, Never> { private func fetchChartHistory(force: Bool, period: ChartHistoryPeriod, for token: TokenMappedToTicker, currency: Currency) async -> HistoryToPeriod {
return tickerIdsFetcher.tickerId(for: token) guard let tickerIdString = await tickerIdsFetcher.tickerId(for: token) else { return HistoryToPeriod(period: period, history: ChartHistory.empty(currency: currency)) }
.flatMap { [storage, networking, weak self] tickerId -> AnyPublisher<HistoryToPeriod, Never> in let tickerId = AssignedCoinTickerId(tickerId: tickerIdString, token: token)
guard let strongSelf = self else { return .empty() }
guard let tickerId = tickerId.flatMap({ AssignedCoinTickerId(tickerId: $0, token: token) }) else {
return .just(.init(period: period, history: .empty(currency: currency)))
}
if let data = storage.chartHistory(period: period, for: tickerId, currency: currency), !strongSelf.hasExpired(history: data, for: period), !force { if let data = await storage.chartHistory(period: period, for: tickerId, currency: currency), !hasExpired(history: data, for: period), !force {
return .just(.init(period: period, history: data.history)) return HistoryToPeriod(period: period, history: data.history)
} else { } else {
return networking.fetchChartHistory(for: period, tickerId: tickerId.tickerId, currency: currency) do {
.handleEvents(receiveOutput: { history in let history = try await networking.fetchChartHistory(for: period, tickerId: tickerId.tickerId, currency: currency)
storage.addOrUpdateChartHistory(history: history, period: period, for: tickerId) storage.addOrUpdateChartHistory(history: history, period: period, for: tickerId)
}).replaceError(with: .empty(currency: currency)) return HistoryToPeriod(period: period, history: history)
.map { HistoryToPeriod(period: period, history: $0) } } catch {
.receive(on: RunLoop.main) return HistoryToPeriod(period: period, history: ChartHistory.empty(currency: currency))
.eraseToAnyPublisher() }
} }
}.eraseToAnyPublisher()
} }
private func hasExpiredTickersLifeTimeSinceLastUpdate(for token: TokenMappedToTicker, currency: Currency) -> Bool { private func hasExpiredTickersLifeTimeSinceLastUpdate(for token: TokenMappedToTicker, currency: Currency) async -> Bool {
let key = AddressAndRPCServer(address: token.contractAddress, server: token.server) let key = AddressAndRPCServer(address: token.contractAddress, server: token.server)
if let ticker = storage.ticker(for: key, currency: currency), Date().timeIntervalSince(ticker.lastUpdatedAt) <= pricesCacheLifetime { if let ticker = await storage.ticker(for: key, currency: currency), Date().timeIntervalSince(ticker.lastUpdatedAt) <= pricesCacheLifetime {
return false return false
} }
@ -164,32 +162,24 @@ public class BaseCoinTickersFetcher: CoinTickersFetcher {
} }
} }
private func fetchBatchOfTickers(for tokens: [TokenMappedToTicker], currency: Currency) -> AnyPublisher<[AssignedCoinTickerId: CoinTicker], PromiseError> { private func fetchBatchOfTickers(for tokens: [TokenMappedToTicker], currency: Currency) async throws -> [AssignedCoinTickerId: CoinTicker] {
let publishers = tokens.map { token in let assignedCoinTickerIds: [AssignedCoinTickerId] = await tokens.asyncCompactMap { token in
tickerIdsFetcher.tickerId(for: token).map { $0.flatMap { AssignedCoinTickerId(tickerId: $0, token: token) } } if let tickerId = await tickerIdsFetcher.tickerId(for: token) {
return AssignedCoinTickerId(tickerId: tickerId, token: token)
} else {
return nil
}
} }
let tickerIds = assignedCoinTickerIds.map { $0.tickerId }
return Publishers.MergeMany(publishers).collect() guard !tickerIds.isEmpty else { return [:] }
.setFailureType(to: PromiseError.self) let tickers = try await networking.fetchTickers(for: tickerIds, currency: currency)
.flatMap { [networking] tickerIds -> AnyPublisher<[AssignedCoinTickerId: CoinTicker], PromiseError> in var result: [AssignedCoinTickerId: CoinTicker] = [:]
let tickerIds = tickerIds.compactMap { $0 } for ticker in tickers {
let ids = tickerIds.compactMap { $0.tickerId } for each in assignedCoinTickerIds.filter({ $0.tickerId == ticker.id }) {
result[each] = ticker
guard !ids.isEmpty else { }
return .just([:]) }
} return result
return networking.fetchTickers(for: ids, currency: currency).map { tickers in
var result: [AssignedCoinTickerId: CoinTicker] = [:]
for ticker in tickers {
for tickerId in tickerIds.filter({ $0.tickerId == ticker.id }) {
result[tickerId] = ticker
}
}
return result
}.eraseToAnyPublisher()
}.eraseToAnyPublisher()
} }
} }

@ -13,16 +13,16 @@ import AlphaWalletLogger
public class FakeCoinTickerNetworking: CoinTickerNetworking { public class FakeCoinTickerNetworking: CoinTickerNetworking {
public init() {} public init() {}
public func fetchSupportedTickerIds() -> AnyPublisher<[TickerId], PromiseError> { public func fetchSupportedTickerIds() async throws -> [TickerId] {
Empty(completeImmediately: true).eraseToAnyPublisher() return []
} }
public func fetchTickers(for tickerIds: [String], currency: Currency) -> AnyPublisher<[CoinTicker], PromiseError> { public func fetchTickers(for tickerIds: [TickerIdString], currency: Currency) async throws -> [CoinTicker] {
Empty(completeImmediately: true).eraseToAnyPublisher() return []
} }
public func fetchChartHistory(for period: ChartHistoryPeriod, tickerId: String, currency: Currency) -> AnyPublisher<ChartHistory, PromiseError> { public func fetchChartHistory(for period: ChartHistoryPeriod, tickerId: String, currency: Currency) async throws -> ChartHistory {
Empty(completeImmediately: true).eraseToAnyPublisher() return ChartHistory.empty(currency: currency)
} }
} }
@ -61,66 +61,49 @@ public class CoinGeckoCoinTickerNetworking: CoinTickerNetworking {
} }
} }
public func fetchSupportedTickerIds() -> AnyPublisher<[TickerId], PromiseError> { public func fetchSupportedTickerIds() async throws -> [TickerId] {
transporter let response = try await transporter.dataTask(TokensThatHasPricesRequest())
.dataTaskPublisher(TokensThatHasPricesRequest()) log(response: response)
.handleEvents(receiveOutput: { self.log(response: $0) }) return try decoder.decode([TickerId].self, from: response.data)
.tryMap { [decoder] in try decoder.decode([TickerId].self, from: $0.data) }
.mapError { PromiseError.some(error: $0) }
.share()
.eraseToAnyPublisher()
} }
public func fetchTickers(for tickerIds: [TickerIdString], currency: Currency) -> AnyPublisher<[CoinTicker], PromiseError> { public func fetchTickers(for tickerIds: [TickerIdString], currency: Currency) async throws -> [CoinTicker] {
let ids = Set(tickerIds).joined(separator: ",") let ids = Set(tickerIds).joined(separator: ",")
var page = 1 var page = 1
var allResults: [CoinTicker] = .init() var allResults: [CoinTicker] = .init()
func fetchPageImpl() -> AnyPublisher<[CoinTicker], PromiseError> { func fetchPageImpl() async throws -> [CoinTicker] {
fetchPricesPage(for: ids, page: page, currency: currency) let results = try await fetchPricesPage(for: ids, page: page, currency: currency)
.flatMap { results -> AnyPublisher<[CoinTicker], PromiseError> in if results.isEmpty {
if results.isEmpty { return allResults
return .just(allResults) } else {
} else { allResults.append(contentsOf: results)
allResults.append(contentsOf: results) page += 1
page += 1 return try await fetchPageImpl()
return fetchPageImpl() }
}
}.eraseToAnyPublisher()
} }
return try await fetchPageImpl()
return fetchPageImpl()
.share()
.eraseToAnyPublisher()
} }
public func fetchChartHistory(for period: ChartHistoryPeriod, tickerId: String, currency: Currency) -> AnyPublisher<ChartHistory, PromiseError> { public func fetchChartHistory(for period: ChartHistoryPeriod, tickerId: String, currency: Currency) async throws -> ChartHistory {
return transporter let response = try await transporter.dataTask(PriceHistoryOfTokenRequest(id: tickerId, currency: currency.code, days: period.rawValue))
.dataTaskPublisher(PriceHistoryOfTokenRequest(id: tickerId, currency: currency.code, days: period.rawValue)) log(response: response)
.handleEvents(receiveOutput: { self.log(response: $0) }) return try ChartHistory(json: try JSON(data: response.data), currency: currency)
.tryMap { try ChartHistory(json: try JSON(data: $0.data), currency: currency) }
.mapError { PromiseError.some(error: $0) }
.share()
.eraseToAnyPublisher()
} }
private func fetchPricesPage(for tickerIds: String, page: Int, currency: Currency) -> AnyPublisher<[CoinTicker], PromiseError> { private func fetchPricesPage(for tickerIds: String, page: Int, currency: Currency) async throws -> [CoinTicker] {
return transporter let response = try await transporter.dataTask(PricesOfTokensRequest(ids: tickerIds, currency: currency.code, page: page))
.dataTaskPublisher(PricesOfTokensRequest(ids: tickerIds, currency: currency.code, page: page)) log(response: response)
.handleEvents(receiveOutput: { self.log(response: $0) }) do {
.tryMap { [decoder] in let coinTickers = try decoder.decode([CoinTicker].self, from: response.data)
do { //NOTE: we re not able to set currency in init method, using `override(currency: )` instead
return try decoder.decode([CoinTicker].self, from: $0.data) return coinTickers.map { $0.override(currency: currency) }
} catch { } catch {
if let response = try? decoder.decode(CoinGeckoErrorResponse.self, from: $0.data) { if let response = try? decoder.decode(CoinGeckoErrorResponse.self, from: response.data) {
throw PromiseError.some(error: response.status) throw response.status
} else { } else {
throw error throw error
}
}
} }
.map { $0.map { $0.override(currency: currency) } }//NOTE: we re not able to set currency in init method, using `override(currency: )` instead }
.mapError { PromiseError.some(error: $0) }
.eraseToAnyPublisher()
} }
private struct CoinGeckoErrorResponse: Decodable { private struct CoinGeckoErrorResponse: Decodable {

@ -16,13 +16,11 @@ public protocol SupportedTickerIdsFetcherConfig {
/// Ticker ids are havy objects, that don't change often, keep them cached and in separate fetcher to extract logic /// Ticker ids are havy objects, that don't change often, keep them cached and in separate fetcher to extract logic
public final class SupportedTickerIdsFetcher: TickerIdsFetcher { public final class SupportedTickerIdsFetcher: TickerIdsFetcher {
typealias TickerIdsPublisher = AnyPublisher<Void, PromiseError>
private let networking: CoinTickerNetworking private let networking: CoinTickerNetworking
private let storage: TickerIdsStorage & CoinTickersStorage private let storage: TickerIdsStorage & CoinTickersStorage
private var config: SupportedTickerIdsFetcherConfig private var config: SupportedTickerIdsFetcherConfig
private let pricesCacheLifetime: TimeInterval private let pricesCacheLifetime: TimeInterval
private var fetchSupportedTickerIdsPublisher: TickerIdsPublisher? private var fetchSupportedTickerIdsTask: Task<Void, Never>?
private let queue = DispatchQueue(label: "org.alphawallet.swift.coinGeckoTicker.IdsFetcher") private let queue = DispatchQueue(label: "org.alphawallet.swift.coinGeckoTicker.IdsFetcher")
/// Init method /// Init method
@ -38,45 +36,37 @@ public final class SupportedTickerIdsFetcher: TickerIdsFetcher {
} }
/// Searching for ticker id very havy operation, and takes to mutch time, we use cacing in `knownTickerIds` to store all know ticker ids /// Searching for ticker id very havy operation, and takes to mutch time, we use cacing in `knownTickerIds` to store all know ticker ids
public func tickerId(for token: TokenMappedToTicker) -> AnyPublisher<TickerIdString?, Never> { public func tickerId(for token: TokenMappedToTicker) async -> TickerIdString? {
return Just(token) do {
.receive(on: queue) try await fetchSupportedTickerIds()
.flatMap { [weak self, storage] token -> AnyPublisher<TickerIdString?, Never> in } catch {
guard let strongSelf = self else { return .empty() } return nil
}
return strongSelf.fetchSupportedTickerIds() if let tickerId = await storage.tickerId(for: token)?.id {
.map { storage.tickerId(for: token)?.id } storage.addOrUpdate(tickerId: tickerId, for: token)
.handleEvents(receiveOutput: { tickerId in return tickerId
guard let tickerId = tickerId else { return } } else {
storage.addOrUpdate(tickerId: tickerId, for: token) return nil
}) }
.replaceError(with: nil)
.eraseToAnyPublisher()
}.receive(on: RunLoop.main)
.eraseToAnyPublisher()
} }
private func fetchSupportedTickerIds() -> TickerIdsPublisher { private func fetchSupportedTickerIds() async throws {
if let lastFetchingDate = config.tickerIdsLastFetchedDate, Date().timeIntervalSince(lastFetchingDate) <= pricesCacheLifetime, storage.hasTickerIds() { if let lastFetchingDate = config.tickerIdsLastFetchedDate, Date().timeIntervalSince(lastFetchingDate) <= pricesCacheLifetime, await storage.hasTickerIds() {
return .just(()) return
} else { } else {
if let publisher = fetchSupportedTickerIdsPublisher { if let task = fetchSupportedTickerIdsTask {
return publisher return await task.value
} else { } else {
let publisher = networking.fetchSupportedTickerIds() let task: Task<Void, Never> = Task { () -> Void in
.receive(on: queue) do {
.handleEvents(receiveOutput: { [storage, weak self] tickerIds in let tickerIds = try await networking.fetchSupportedTickerIds()
storage.addOrUpdate(tickerIds: tickerIds) storage.addOrUpdate(tickerIds: tickerIds)
self?.config.tickerIdsLastFetchedDate = Date() config.tickerIdsLastFetchedDate = Date()
}, receiveCompletion: { [weak self] _ in self.fetchSupportedTickerIdsTask = nil
self?.fetchSupportedTickerIdsPublisher = .none } catch {}
}).share() }
.mapToVoid() fetchSupportedTickerIdsTask = task
.eraseToAnyPublisher() return try await task.value
fetchSupportedTickerIdsPublisher = publisher
return publisher
} }
} }
} }

@ -10,7 +10,7 @@ import Combine
import AlphaWalletCore import AlphaWalletCore
public protocol CoinTickerNetworking { public protocol CoinTickerNetworking {
func fetchSupportedTickerIds() -> AnyPublisher<[TickerId], PromiseError> func fetchSupportedTickerIds() async throws -> [TickerId]
func fetchTickers(for tickerIds: [TickerIdString], currency: Currency) -> AnyPublisher<[CoinTicker], PromiseError> func fetchTickers(for tickerIds: [TickerIdString], currency: Currency) async throws -> [CoinTicker]
func fetchChartHistory(for period: ChartHistoryPeriod, tickerId: String, currency: Currency) -> AnyPublisher<ChartHistory, PromiseError> func fetchChartHistory(for period: ChartHistoryPeriod, tickerId: String, currency: Currency) async throws -> ChartHistory
} }

@ -7,7 +7,7 @@ import AlphaWalletCore
public final class CoinTickers { public final class CoinTickers {
private let fetchers: AtomicArray<CoinTickersFetcher> = .init() private let fetchers: AtomicArray<CoinTickersFetcher> = .init()
private let storage: CoinTickersStorage & ChartHistoryStorage & TickerIdsStorage private let storage: CoinTickersStorage & ChartHistoryStorage & TickerIdsStorage
private var chartHistories: [TokenMappedToTicker: CurrentValueSubject<[ChartHistoryPeriod: ChartHistory], Never>] = .init() private var chartHistories: [TokenMappedToTicker: Task<[ChartHistoryPeriod: ChartHistory], Never>] = .init()
private var cancelable = Set<AnyCancellable>() private var cancelable = Set<AnyCancellable>()
public init(fetchers: [CoinTickersFetcher], storage: CoinTickersStorage & ChartHistoryStorage & TickerIdsStorage) { public init(fetchers: [CoinTickersFetcher], storage: CoinTickersStorage & ChartHistoryStorage & TickerIdsStorage) {
@ -18,7 +18,8 @@ public final class CoinTickers {
public convenience init(transporter: ApiTransporter, analytics: AnalyticsLogger) { public convenience init(transporter: ApiTransporter, analytics: AnalyticsLogger) {
let storage: CoinTickersStorage & ChartHistoryStorage & TickerIdsStorage let storage: CoinTickersStorage & ChartHistoryStorage & TickerIdsStorage
if isRunningTests() { if isRunningTests() {
storage = RealmStore(realm: fakeRealm(), name: "org.alphawallet.swift.realmStore.shared.wallet") //TODO should be injected in tests instead
storage = RealmStore(config: fakeRealm().configuration, name: "org.alphawallet.swift.realmStore.shared.wallet")
} else { } else {
storage = RealmStore.shared storage = RealmStore.shared
} }
@ -44,11 +45,11 @@ extension CoinTickers: CoinTickersFetcher {
} }
} }
public func fetchChartHistories(for token: TokenMappedToTicker, force: Bool, periods: [ChartHistoryPeriod], currency: Currency) -> AnyPublisher<[ChartHistoryPeriod: ChartHistory], Never> { public func fetchChartHistories(for token: TokenMappedToTicker, force: Bool, periods: [ChartHistoryPeriod], currency: Currency) async -> [ChartHistoryPeriod: ChartHistory] {
if let fetcher = functional.getFetcher(forTokenMappedToTicker: token, fetchers: fetchers) { if let fetcher = functional.getFetcher(forTokenMappedToTicker: token, fetchers: fetchers) {
return fetcher.fetchChartHistories(for: token, force: force, periods: periods, currency: currency) return await fetcher.fetchChartHistories(for: token, force: force, periods: periods, currency: currency)
} else { } else {
return .empty() return [:]
} }
} }
@ -67,8 +68,8 @@ extension CoinTickers: CoinTickersProvider {
storage.updateTickerIds storage.updateTickerIds
} }
public func ticker(for key: AddressAndRPCServer, currency: Currency) -> CoinTicker? { public func ticker(for key: AddressAndRPCServer, currency: Currency) async -> CoinTicker? {
return storage.ticker(for: key, currency: currency) return await storage.ticker(for: key, currency: currency)
} }
public func addOrUpdateTestsOnly(ticker: CoinTicker?, for token: TokenMappedToTicker) { public func addOrUpdateTestsOnly(ticker: CoinTicker?, for token: TokenMappedToTicker) {
@ -80,19 +81,16 @@ extension CoinTickers: CoinTickersProvider {
storage.addOrUpdate(tickers: tickers) storage.addOrUpdate(tickers: tickers)
} }
public func chartHistories(for token: TokenMappedToTicker, currency: Currency) -> AnyPublisher<[ChartHistoryPeriod: ChartHistory], Never> { public func chartHistories(for token: TokenMappedToTicker, currency: Currency) async -> [ChartHistoryPeriod: ChartHistory] {
guard let fetcher = functional.getFetcher(forTokenMappedToTicker: token, fetchers: fetchers) else { return .empty() } guard let fetcher = functional.getFetcher(forTokenMappedToTicker: token, fetchers: fetchers) else { return [:] }
if let tokenAndChartHistories = chartHistories[token] { if let tokenAndChartHistories = chartHistories[token] {
return tokenAndChartHistories.eraseToAnyPublisher() return await tokenAndChartHistories.value
} else { } else {
let publisher = CurrentValueSubject<[ChartHistoryPeriod: ChartHistory], Never>(.init()) let task = Task<[ChartHistoryPeriod: ChartHistory], Never> {
chartHistories[token] = publisher await fetchChartHistories(for: token, force: false, periods: ChartHistoryPeriod.allCases, currency: currency)
//TODO user might have changed the currency between fetches? Does it work? }
fetchChartHistories(for: token, force: false, periods: ChartHistoryPeriod.allCases, currency: currency) chartHistories[token] = task
.assign(to: \.value, on: publisher) return await task.value
.store(in: &cancelable)
return publisher.eraseToAnyPublisher()
} }
} }
} }

@ -15,22 +15,21 @@ public typealias TickerIdString = String
public protocol CoinTickersStorage { public protocol CoinTickersStorage {
var tickersDidUpdate: AnyPublisher<Void, Never> { get } var tickersDidUpdate: AnyPublisher<Void, Never> { get }
func historyLastUpdatedAt(for key: AddressAndRPCServer, currency: Currency) -> Date? func ticker(for key: AddressAndRPCServer, currency: Currency) async -> CoinTicker?
func ticker(for key: AddressAndRPCServer, currency: Currency) -> CoinTicker?
func addOrUpdate(tickers: [AssignedCoinTickerId: CoinTicker]) func addOrUpdate(tickers: [AssignedCoinTickerId: CoinTicker])
} }
public protocol ChartHistoryStorage { public protocol ChartHistoryStorage {
func chartHistory(period: ChartHistoryPeriod, for tickerId: AssignedCoinTickerId, currency: Currency) -> MappedChartHistory? func chartHistory(period: ChartHistoryPeriod, for tickerId: AssignedCoinTickerId, currency: Currency) async -> MappedChartHistory?
func addOrUpdateChartHistory(history: ChartHistory, period: ChartHistoryPeriod, for tickerId: AssignedCoinTickerId) func addOrUpdateChartHistory(history: ChartHistory, period: ChartHistoryPeriod, for tickerId: AssignedCoinTickerId)
} }
public protocol TickerIdsStorage { public protocol TickerIdsStorage {
var updateTickerIds: AnyPublisher<[(tickerId: TickerIdString, key: AddressAndRPCServer)], Never> { get } var updateTickerIds: AnyPublisher<[(tickerId: TickerIdString, key: AddressAndRPCServer)], Never> { get }
func hasTickerIds() -> Bool func hasTickerIds() async -> Bool
func tickerId(for token: TokenMappedToTicker) -> TickerId? func tickerId(for token: TokenMappedToTicker) async -> TickerId?
func knownTickerId(for key: TokenMappedToTicker) -> TickerIdString? func knownTickerId(for key: TokenMappedToTicker) async -> TickerIdString?
func addOrUpdate(tickerId: TickerIdString?, for key: TokenMappedToTicker) func addOrUpdate(tickerId: TickerIdString?, for key: TokenMappedToTicker)
func addOrUpdate(tickerIds: [TickerId]) func addOrUpdate(tickerIds: [TickerId])
func removeTickerIds() func removeTickerIds()
@ -39,37 +38,40 @@ public protocol TickerIdsStorage {
extension RealmStore: TickerIdsStorage { extension RealmStore: TickerIdsStorage {
public var updateTickerIds: AnyPublisher<[(tickerId: TickerIdString, key: AddressAndRPCServer)], Never> { public var updateTickerIds: AnyPublisher<[(tickerId: TickerIdString, key: AddressAndRPCServer)], Never> {
var publisher: AnyPublisher<[(tickerId: TickerIdString, key: AddressAndRPCServer)], Never>! let publisher = PassthroughSubject<[(tickerId: TickerIdString, key: AddressAndRPCServer)], Never>()
performSync { realm in Task {
publisher = realm.objects(KnownTickerIdObject.self) await performSync { realm in
.changesetPublisher realm.objects(KnownTickerIdObject.self)
.compactMap { changeset -> [AssignedCoinTickerId]? in .changesetPublisher
switch changeset { .compactMap { changeset -> [AssignedCoinTickerId]? in
case .error, .initial: switch changeset {
return nil case .error, .initial:
case .update(let values, let deletions, let insertions, let modifications): return nil
let objects = insertions.map { values[$0] } + modifications.map { values[$0] } case .update(let values, let deletions, let insertions, let modifications):
return objects.map { AssignedCoinTickerId(tickerId: $0.tickerIdString, primaryToken: .init(address: $0.contractAddress, server: $0.server)) } let objects = insertions.map { values[$0] } + modifications.map { values[$0] }
} return objects.map { AssignedCoinTickerId(tickerId: $0.tickerIdString, primaryToken: .init(address: $0.contractAddress, server: $0.server)) }
}.map { $0.map { (tickerId: $0.tickerId, key: $0.primaryToken) } } }
.eraseToAnyPublisher() }.map { $0.map { (tickerId: $0.tickerId, key: $0.primaryToken) } }
.sink { value in
publisher.send(value)
}.store(in: &self.cancellables)
}
} }
return publisher.eraseToAnyPublisher()
return publisher
} }
public func hasTickerIds() -> Bool { public func hasTickerIds() async -> Bool {
var hasTickerIds: Bool = false var hasTickerIds: Bool = false
performSync { realm in await perform { realm in
hasTickerIds = !realm.objects(TickerIdObject.self).isEmpty hasTickerIds = !realm.objects(TickerIdObject.self).isEmpty
} }
return hasTickerIds return hasTickerIds
} }
public func knownTickerId(for key: TokenMappedToTicker) -> TickerIdString? { public func knownTickerId(for key: TokenMappedToTicker) async -> TickerIdString? {
var tickerIdString: TickerIdString? var tickerIdString: TickerIdString?
performSync { realm in await perform { realm in
let primaryKey = ContractAddressObject.generatePrimaryKey(fromContract: key.contractAddress, server: key.server) let primaryKey = ContractAddressObject.generatePrimaryKey(fromContract: key.contractAddress, server: key.server)
tickerIdString = realm.object(ofType: KnownTickerIdObject.self, forPrimaryKey: primaryKey)?.tickerIdString tickerIdString = realm.object(ofType: KnownTickerIdObject.self, forPrimaryKey: primaryKey)?.tickerIdString
} }
@ -78,30 +80,32 @@ extension RealmStore: TickerIdsStorage {
} }
public func addOrUpdate(tickerId: TickerIdString?, for key: TokenMappedToTicker) { public func addOrUpdate(tickerId: TickerIdString?, for key: TokenMappedToTicker) {
performSync { realm in Task {
let primaryKey = ContractAddressObject.generatePrimaryKey(fromContract: key.contractAddress, server: key.server) await perform { realm in
let storedTicker = realm.object(ofType: KnownTickerIdObject.self, forPrimaryKey: primaryKey) let primaryKey = ContractAddressObject.generatePrimaryKey(fromContract: key.contractAddress, server: key.server)
if let tickerId = tickerId, let storedTickerId = storedTicker?.tickerIdString, tickerId == storedTickerId { return } let storedTicker = realm.object(ofType: KnownTickerIdObject.self, forPrimaryKey: primaryKey)
if let tickerId = tickerId, let storedTickerId = storedTicker?.tickerIdString, tickerId == storedTickerId { return }
try? realm.safeWrite {
if let tickerId = tickerId { try? realm.safeWrite {
if let value = storedTicker { if let tickerId = tickerId {
value.tickerIdString = tickerId if let value = storedTicker {
value.tickerIdString = tickerId
} else {
let knownTickerId = KnownTickerIdObject(server: key.server, contractAddress: key.contractAddress, tickerIdString: tickerId)
realm.add(knownTickerId, update: .all)
}
} else { } else {
let knownTickerId = KnownTickerIdObject(server: key.server, contractAddress: key.contractAddress, tickerIdString: tickerId) guard let _storedTicker = storedTicker else { return }
realm.add(knownTickerId, update: .all) realm.delete(_storedTicker)
} }
} else {
guard let _storedTicker = storedTicker else { return }
realm.delete(_storedTicker)
} }
} }
} }
} }
public func tickerId(for token: TokenMappedToTicker) -> TickerId? { public func tickerId(for token: TokenMappedToTicker) async -> TickerId? {
var tickerId: TickerId? var tickerId: TickerId?
performSync { realm in await perform { realm in
let filter = TickerIdFilter() let filter = TickerIdFilter()
tickerId = realm.objects(TickerIdObject.self) tickerId = realm.objects(TickerIdObject.self)
.filter { filter.filterMathesInPlatforms(token: token, tickerId: $0) } .filter { filter.filterMathesInPlatforms(token: token, tickerId: $0) }
@ -113,19 +117,22 @@ extension RealmStore: TickerIdsStorage {
public func addOrUpdate(tickerIds: [TickerId]) { public func addOrUpdate(tickerIds: [TickerId]) {
guard !tickerIds.isEmpty else { return } guard !tickerIds.isEmpty else { return }
let tickerIdsToSave = tickerIds.map { TickerIdObject(tickerId: $0) } let tickerIdsToSave = tickerIds.map { TickerIdObject(tickerId: $0) }
performSync { realm in Task {
try? realm.safeWrite { await perform { realm in
realm.add(tickerIdsToSave, update: .all) try? realm.safeWrite {
realm.add(tickerIdsToSave, update: .all)
}
} }
} }
} }
public func removeTickerIds() { public func removeTickerIds() {
performSync { realm in Task {
try? realm.safeWrite { await perform { realm in
realm.delete(realm.objects(TickerIdObject.self)) try? realm.safeWrite {
realm.delete(realm.objects(TickerIdObject.self))
}
} }
} }
} }
@ -133,9 +140,9 @@ extension RealmStore: TickerIdsStorage {
extension RealmStore: ChartHistoryStorage { extension RealmStore: ChartHistoryStorage {
public func chartHistory(period: ChartHistoryPeriod, for tickerId: AssignedCoinTickerId, currency: Currency) -> MappedChartHistory? { public func chartHistory(period: ChartHistoryPeriod, for tickerId: AssignedCoinTickerId, currency: Currency) async -> MappedChartHistory? {
var history: MappedChartHistory? var history: MappedChartHistory?
performSync { realm in await perform { realm in
let primaryKey = ContractAddressObject.generatePrimaryKey(fromContract: tickerId.primaryToken.address, server: tickerId.primaryToken.server) let primaryKey = ContractAddressObject.generatePrimaryKey(fromContract: tickerId.primaryToken.address, server: tickerId.primaryToken.server)
history = realm.object(ofType: AssignedCoinTickerIdObject.self, forPrimaryKey: primaryKey).flatMap { $0.chartHistory?[period]?[currency] } history = realm.object(ofType: AssignedCoinTickerIdObject.self, forPrimaryKey: primaryKey).flatMap { $0.chartHistory?[period]?[currency] }
} }
@ -157,24 +164,26 @@ extension RealmStore: ChartHistoryStorage {
} }
public func addOrUpdateChartHistory(history: ChartHistory, period: ChartHistoryPeriod, for tickerId: AssignedCoinTickerId) { public func addOrUpdateChartHistory(history: ChartHistory, period: ChartHistoryPeriod, for tickerId: AssignedCoinTickerId) {
performSync { realm in Task {
try? realm.safeWrite { await perform { realm in
let primaryKey = ContractAddressObject.generatePrimaryKey(fromContract: tickerId.primaryToken.address, server: tickerId.primaryToken.server) try? realm.safeWrite {
let knownTickerId = Self.getOrCreateKnownTickerId(for: tickerId, in: realm) let primaryKey = ContractAddressObject.generatePrimaryKey(fromContract: tickerId.primaryToken.address, server: tickerId.primaryToken.server)
let knownTickerId = Self.getOrCreateKnownTickerId(for: tickerId, in: realm)
if let assignedCoinTicker = realm.object(ofType: AssignedCoinTickerIdObject.self, forPrimaryKey: primaryKey) {
var histories = assignedCoinTicker.chartHistory ?? [:] if let assignedCoinTicker = realm.object(ofType: AssignedCoinTickerIdObject.self, forPrimaryKey: primaryKey) {
var historyForPeriod = histories[period] ?? [:] var histories = assignedCoinTicker.chartHistory ?? [:]
historyForPeriod[history.currency] = .init(history: history, fetchDate: Date()) var historyForPeriod = histories[period] ?? [:]
histories[period] = historyForPeriod historyForPeriod[history.currency] = .init(history: history, fetchDate: Date())
histories[period] = historyForPeriod
assignedCoinTicker.chartHistory = histories
} else { assignedCoinTicker.chartHistory = histories
var newHistories: [ChartHistoryPeriod: [Currency: MappedChartHistory]] = [:] } else {
newHistories[period] = [history.currency: .init(history: history, fetchDate: Date())] var newHistories: [ChartHistoryPeriod: [Currency: MappedChartHistory]] = [:]
newHistories[period] = [history.currency: .init(history: history, fetchDate: Date())]
let assignedCoinTicker = AssignedCoinTickerIdObject(tickerId: knownTickerId, tickers: [], chartHistories: newHistories)
realm.add(assignedCoinTicker, update: .all) let assignedCoinTicker = AssignedCoinTickerIdObject(tickerId: knownTickerId, tickers: [], chartHistories: newHistories)
realm.add(assignedCoinTicker, update: .all)
}
} }
} }
} }
@ -182,36 +191,24 @@ extension RealmStore: ChartHistoryStorage {
} }
extension RealmStore: CoinTickersStorage { extension RealmStore: CoinTickersStorage {
public var tickersDidUpdate: AnyPublisher<Void, Never> { public var tickersDidUpdate: AnyPublisher<Void, Never> {
var publisher: AnyPublisher<Void, Never>! let publisher = PassthroughSubject<Void, Never>()
performSync { realm in Task {
publisher = realm.objects(AssignedCoinTickerIdObject.self) await performSync { realm in
.changesetPublisher realm.objects(AssignedCoinTickerIdObject.self)
.mapToVoid() .changesetPublisher
.eraseToAnyPublisher() .mapToVoid()
} .sink { value in
publisher.send(value)
return publisher }.store(in: &self.cancellables)
}
public func historyLastUpdatedAt(for key: AddressAndRPCServer, currency: Currency) -> Date? {
var updatedAt: Date?
performSync { realm in
let primaryKey = ContractAddressObject.generatePrimaryKey(fromContract: key.address, server: key.server)
let obj = realm.object(ofType: AssignedCoinTickerIdObject.self, forPrimaryKey: primaryKey)
let historiesToCurrencies = obj?.chartHistory?.map { $0.value } ?? []
if let anyHistory = historiesToCurrencies.compactMap { $0[currency] }.first {
updatedAt = anyHistory.fetchDate
} }
} }
return publisher.eraseToAnyPublisher()
return updatedAt
} }
public func ticker(for key: AddressAndRPCServer, currency: Currency) -> CoinTicker? { public func ticker(for key: AddressAndRPCServer, currency: Currency) async -> CoinTicker? {
var ticker: CoinTicker? var ticker: CoinTicker?
performSync { realm in await perform { realm in
let primaryKey = ContractAddressObject.generatePrimaryKey(fromContract: key.address, server: key.server) let primaryKey = ContractAddressObject.generatePrimaryKey(fromContract: key.address, server: key.server)
ticker = realm.object(ofType: AssignedCoinTickerIdObject.self, forPrimaryKey: primaryKey) ticker = realm.object(ofType: AssignedCoinTickerIdObject.self, forPrimaryKey: primaryKey)
.flatMap { $0.tickers.first(where: { $0.currency == currency.code }).flatMap { CoinTicker(coinTickerObject: $0) } } .flatMap { $0.tickers.first(where: { $0.currency == currency.code }).flatMap { CoinTicker(coinTickerObject: $0) } }
@ -223,24 +220,26 @@ extension RealmStore: CoinTickersStorage {
public func addOrUpdate(tickers: [AssignedCoinTickerId: CoinTicker]) { public func addOrUpdate(tickers: [AssignedCoinTickerId: CoinTicker]) {
guard !tickers.isEmpty else { return } guard !tickers.isEmpty else { return }
performSync { realm in Task {
try? realm.safeWrite { await perform { realm in
for each in tickers { try? realm.safeWrite {
let primaryKey = ContractAddressObject.generatePrimaryKey(fromContract: each.key.primaryToken.address, server: each.key.primaryToken.server) for each in tickers {
let tickerId = Self.getOrCreateKnownTickerId(for: each.key, in: realm) let primaryKey = ContractAddressObject.generatePrimaryKey(fromContract: each.key.primaryToken.address, server: each.key.primaryToken.server)
let tickerId = Self.getOrCreateKnownTickerId(for: each.key, in: realm)
let ticker = CoinTickerObject(coinTicker: each.value)
realm.add(ticker, update: .all) let ticker = CoinTickerObject(coinTicker: each.value)
realm.add(ticker, update: .all)
if let assignedCoinTicker = realm.object(ofType: AssignedCoinTickerIdObject.self, forPrimaryKey: primaryKey) {
var tickers = Array(assignedCoinTicker.tickers.filter { !$0.isEqual(ticker) }) if let assignedCoinTicker = realm.object(ofType: AssignedCoinTickerIdObject.self, forPrimaryKey: primaryKey) {
tickers.append(ticker) var tickers = Array(assignedCoinTicker.tickers.filter { !$0.isEqual(ticker) })
tickers.append(ticker)
assignedCoinTicker.tickers.removeAll()
assignedCoinTicker.tickers.append(objectsIn: tickers) assignedCoinTicker.tickers.removeAll()
} else { assignedCoinTicker.tickers.append(objectsIn: tickers)
let assignedCoinTicker = AssignedCoinTickerIdObject(tickerId: tickerId, tickers: [ticker], chartHistories: nil) } else {
realm.add(assignedCoinTicker, update: .all) let assignedCoinTicker = AssignedCoinTickerIdObject(tickerId: tickerId, tickers: [ticker], chartHistories: nil)
realm.add(assignedCoinTicker, update: .all)
}
} }
} }
} }

@ -26,40 +26,32 @@ public class AlphaWalletRemoteTickerIdsFetcher: TickerIdsFetcher {
} }
/// Returns already defined, stored associated with token ticker id /// Returns already defined, stored associated with token ticker id
public func tickerId(for token: TokenMappedToTicker) -> AnyPublisher<TickerIdString?, Never> { public func tickerId(for token: TokenMappedToTicker) async -> TickerIdString? {
return Just(token) let entries: [TokenEntry] = (try? await provider.tokenEntries()) ?? []
.receive(on: queue) return await resolveTickerId(in: entries, for: token)
.flatMap { [provider] _ in provider.tokenEntries().replaceError(with: []) }
.flatMap { [weak self] entries -> AnyPublisher<TickerIdString?, Never> in
guard let strongSelf = self else { return .empty() }
return strongSelf.resolveTickerId(in: entries, for: token)
}.receive(on: RunLoop.main)
.eraseToAnyPublisher()
} }
private func resolveTickerId(in tokenEntries: [TokenEntry], for token: TokenMappedToTicker) -> AnyPublisher<TickerIdString?, Never> { private func resolveTickerId(in tokenEntries: [TokenEntry], for token: TokenMappedToTicker) async -> TickerIdString? {
return Just(token) let targetContract: TokenEntry.Contract = .init(address: token.contractAddress.eip55String, chainId: token.server.chainID)
.map { token -> TokenEntry? in if let entry = tokenEntries.first(where: { entry in entry.contracts.contains(targetContract) }) {
let targetContract: TokenEntry.Contract = .init(address: token.contractAddress.eip55String, chainId: token.server.chainID) return await lookupAnyTickerId(for: entry, token: token)
return tokenEntries.first(where: { entry in entry.contracts.contains(targetContract) }) } else {
}.flatMap { [weak self] entry -> AnyPublisher<TickerIdString?, Never> in return nil
guard let strongSelf = self else { return .empty() } }
guard let entry = entry else { return .just(nil) }
return strongSelf.lookupAnyTickerId(for: entry, token: token)
}.eraseToAnyPublisher()
} }
/// Searches for non nil ticker id in token entries array, might be improved for large entries array. /// Searches for non nil ticker id in token entries array, might be improved for large entries array.
private func lookupAnyTickerId(for entry: TokenEntry, token: TokenMappedToTicker) -> AnyPublisher<TickerIdString?, Never> { private func lookupAnyTickerId(for entry: TokenEntry, token: TokenMappedToTicker) async -> TickerIdString? {
let publishers = entry.contracts.compactMap { contract -> TokenMappedToTicker? in let tokensMappedToTickerId: [TokenMappedToTicker] = entry.contracts.compactMap { contract -> TokenMappedToTicker? in
guard let contractAddress = AlphaWallet.Address(string: contract.address) else { return nil } guard let contractAddress = AlphaWallet.Address(string: contract.address) else { return nil }
let server = RPCServer(chainID: contract.chainId) let server = RPCServer(chainID: contract.chainId)
return TokenMappedToTicker(symbol: token.symbol, name: token.name, contractAddress: contractAddress, server: server, coinGeckoId: nil) return TokenMappedToTicker(symbol: token.symbol, name: token.name, contractAddress: contractAddress, server: server, coinGeckoId: nil)
}.map { tickerIdsFetcher.tickerId(for: $0) } }
for each in tokensMappedToTickerId {
return Publishers.MergeMany(publishers).collect() if let tickerId = await tickerIdsFetcher.tickerId(for: each) {
.map { tickerIds in tickerIds.compactMap { $0 }.first } return tickerId
.eraseToAnyPublisher() }
}
return nil
} }
} }

@ -32,23 +32,23 @@ public final class FileTokenEntriesProvider: TokenEntriesProvider {
return url.path return url.path
} }
public func tokenEntries() -> AnyPublisher<[TokenEntry], PromiseError> { public func tokenEntries() async throws -> [TokenEntry] {
if cachedTokenEntries.isEmpty { if cachedTokenEntries.isEmpty {
do { do {
guard let jsonData = try String(contentsOfFile: absoluteFilename).data(using: .utf8) else { throw TokenJsonReader.error.fileIsNotUtf8 } guard let jsonData = try String(contentsOfFile: absoluteFilename).data(using: .utf8) else { throw TokenJsonReader.error.fileIsNotUtf8 }
do { do {
cachedTokenEntries = try JSONDecoder().decode([TokenEntry].self, from: jsonData) cachedTokenEntries = try JSONDecoder().decode([TokenEntry].self, from: jsonData)
return .just(cachedTokenEntries) return cachedTokenEntries
} catch DecodingError.dataCorrupted { } catch DecodingError.dataCorrupted {
throw TokenJsonReader.error.fileCannotBeDecoded throw TokenJsonReader.error.fileCannotBeDecoded
} catch { } catch {
throw TokenJsonReader.error.unknown(error) throw TokenJsonReader.error.unknown(error)
} }
} catch { } catch {
return .fail(.some(error: error)) throw error
} }
} else { } else {
return .just(cachedTokenEntries) return cachedTokenEntries
} }
} }
} }

@ -18,12 +18,11 @@ public class InMemoryTickerIdsFetcher: TickerIdsFetcher {
} }
/// Returns already defined, stored associated with token ticker id /// Returns already defined, stored associated with token ticker id
public func tickerId(for token: TokenMappedToTicker) -> AnyPublisher<TickerIdString?, Never> { public func tickerId(for token: TokenMappedToTicker) async -> TickerIdString? {
if let id = token.knownCoinGeckoTickerId { if let id = token.knownCoinGeckoTickerId {
return .just(id) return id
} else { } else {
let tickerId = storage.knownTickerId(for: token) return await self.storage.knownTickerId(for: token)
return .just(tickerId)
} }
} }
} }

@ -12,9 +12,7 @@ import AlphaWalletCore
//TODO: Future impl for remote TokenEntries provider //TODO: Future impl for remote TokenEntries provider
public final class RemoteTokenEntriesProvider: TokenEntriesProvider { public final class RemoteTokenEntriesProvider: TokenEntriesProvider {
public func tokenEntries() -> AnyPublisher<[TokenEntry], PromiseError> { public func tokenEntries() async throws -> [TokenEntry] {
return .just([]) return []
.share()
.eraseToAnyPublisher()
} }
} }

@ -9,7 +9,7 @@ import Foundation
import Combine import Combine
public protocol TickerIdsFetcher: AnyObject { public protocol TickerIdsFetcher: AnyObject {
func tickerId(for token: TokenMappedToTicker) -> AnyPublisher<TickerIdString?, Never> func tickerId(for token: TokenMappedToTicker) async -> TickerIdString?
} }
/// Returns first matching ticker id, perform searching in data source sequentially, wait until publisher being resolved and resolves next one /// Returns first matching ticker id, perform searching in data source sequentially, wait until publisher being resolved and resolves next one
@ -21,26 +21,15 @@ public class TickerIdsFetcherImpl: TickerIdsFetcher {
self.providers = providers self.providers = providers
} }
/// Returns associated ticker id, callback on .main queue, or immideatelly if ticker id has already exists /// Returns associated ticker id, callback on .main queue, or immediately if ticker id has already exists
public func tickerId(for token: TokenMappedToTicker) -> AnyPublisher<TickerIdString?, Never> { public func tickerId(for token: TokenMappedToTicker) async -> TickerIdString? {
let spamNeedle = AddressAndRPCServer(address: token.contractAddress, server: token.server) let spamNeedle = AddressAndRPCServer(address: token.contractAddress, server: token.server)
if spamTokens.isSpamToken(spamNeedle) { return .empty() } if spamTokens.isSpamToken(spamNeedle) { return nil }
for each in providers {
let publishers = providers.map { $0.tickerId(for: token) } if let tickerId = await each.tickerId(for: token) {
return tickerId
func firstMatchingTickerId(_ publishers: [AnyPublisher<TickerIdString?, Never>]) -> AnyPublisher<TickerIdString?, Never> { }
var publishers = publishers
guard !publishers.isEmpty else { return .empty() }
let publisher = publishers.removeFirst()
return publisher.replaceEmpty(with: nil)
.flatMap { tickerId -> AnyPublisher<TickerIdString?, Never> in
guard let tickerId = tickerId else { return firstMatchingTickerId(publishers) }
return .just(tickerId)
}.eraseToAnyPublisher()
} }
return nil
return firstMatchingTickerId(publishers)
} }
} }

@ -11,5 +11,5 @@ import AlphaWalletCore
/// Provides tokens groups /// Provides tokens groups
public protocol TokenEntriesProvider { public protocol TokenEntriesProvider {
func tokenEntries() -> AnyPublisher<[TokenEntry], PromiseError> func tokenEntries() async throws -> [TokenEntry]
} }

@ -6,6 +6,6 @@ import Combine
public protocol CoinTickersFetcher { public protocol CoinTickersFetcher {
func fetchTickers(for tokens: [TokenMappedToTicker], force: Bool, currency: Currency) func fetchTickers(for tokens: [TokenMappedToTicker], force: Bool, currency: Currency)
func resolveTickerIds(for tokens: [TokenMappedToTicker]) func resolveTickerIds(for tokens: [TokenMappedToTicker])
func fetchChartHistories(for token: TokenMappedToTicker, force: Bool, periods: [ChartHistoryPeriod], currency: Currency) -> AnyPublisher<[ChartHistoryPeriod: ChartHistory], Never> func fetchChartHistories(for token: TokenMappedToTicker, force: Bool, periods: [ChartHistoryPeriod], currency: Currency) async -> [ChartHistoryPeriod: ChartHistory]
func cancel() func cancel()
} }

@ -7,9 +7,9 @@ public protocol CoinTickersProvider: AnyObject {
var tickersDidUpdate: AnyPublisher<Void, Never> { get } var tickersDidUpdate: AnyPublisher<Void, Never> { get }
var updateTickerIds: AnyPublisher<[(tickerId: TickerIdString, key: AddressAndRPCServer)], Never> { get } var updateTickerIds: AnyPublisher<[(tickerId: TickerIdString, key: AddressAndRPCServer)], Never> { get }
func ticker(for key: AddressAndRPCServer, currency: Currency) -> CoinTicker? func ticker(for key: AddressAndRPCServer, currency: Currency) async -> CoinTicker?
func addOrUpdateTestsOnly(ticker: CoinTicker?, for token: TokenMappedToTicker) func addOrUpdateTestsOnly(ticker: CoinTicker?, for token: TokenMappedToTicker)
func chartHistories(for token: TokenMappedToTicker, currency: Currency) -> AnyPublisher<[ChartHistoryPeriod: ChartHistory], Never> func chartHistories(for token: TokenMappedToTicker, currency: Currency) async -> [ChartHistoryPeriod: ChartHistory]
} }
public struct AssignedCoinTickerId: Hashable, Codable { public struct AssignedCoinTickerId: Hashable, Codable {

@ -30,6 +30,8 @@ public struct TokenMappedToTicker {
return "xdai" return "xdai"
} else if server == .arbitrum && contractAddress == Constants.nativeCryptoAddressInDatabase { } else if server == .arbitrum && contractAddress == Constants.nativeCryptoAddressInDatabase {
return "ethereum" return "ethereum"
} else if server == .main && contractAddress == Constants.nativeCryptoAddressInDatabase {
return "ethereum"
} else { } else {
return nil return nil
} }

@ -28,73 +28,57 @@ public class DomainResolutionService {
} }
extension DomainResolutionService: DomainNameResolutionServiceType { extension DomainResolutionService: DomainNameResolutionServiceType {
public func resolveAddress(string value: String) -> AnyPublisher<AlphaWallet.Address, PromiseError> { public func resolveAddress(string value: String) async throws -> AlphaWallet.Address {
let services: [CachedDomainNameResolutionServiceType] = [ let services: [CachedDomainNameResolutionServiceType] = [
getEnsAddressResolver, getEnsAddressResolver,
unstoppableDomainsResolver unstoppableDomainsResolver
] ]
if let cached = services.compactMap({ $0.cachedAddress(for: value) }).first { if let cached = await services.asyncCompactMap({ await $0.cachedAddress(for: value) }).first {
return .just(cached) return cached
} }
return Just(value) do {
.setFailureType(to: SmartContractError.self) return try await getEnsAddressResolver.getENSAddressFromResolver(for: value)
.flatMap { [getEnsAddressResolver] value in } catch {
getEnsAddressResolver.getENSAddressFromResolver(for: value) return try await unstoppableDomainsResolver.resolveAddress(forName: value)
}.catch { [unstoppableDomainsResolver] _ -> AnyPublisher<AlphaWallet.Address, PromiseError> in }
unstoppableDomainsResolver.resolveAddress(forName: value)
}.receive(on: RunLoop.main)//We want to be sure it's on main
.eraseToAnyPublisher()
} }
public func resolveEnsAndBlockie(address: AlphaWallet.Address, server actualServer: RPCServer) -> AnyPublisher<BlockieAndAddressOrEnsResolution, PromiseError> { public func resolveEnsAndBlockie(address: AlphaWallet.Address, server actualServer: RPCServer) async throws -> BlockieAndAddressOrEnsResolution {
func getBlockieImage(for ens: String) -> AnyPublisher<BlockieAndAddressOrEnsResolution, PromiseError> { let ens = try await reverseResolveDomainName(address: address, server: actualServer)
return blockiesGenerator.getBlockieOrEnsAvatarImage(address: address, ens: ens) do {
.map { image -> BlockieAndAddressOrEnsResolution in let image = try await blockiesGenerator.getBlockieOrEnsAvatarImage(address: address, ens: ens)
return (image, .resolved(.domainName(ens))) return (image, .resolved(.domainName(ens)))
}.catch { _ -> AnyPublisher<BlockieAndAddressOrEnsResolution, PromiseError> in } catch {
return .just((nil, .resolved(.domainName(ens)))) return (nil, .resolved(.domainName(ens)))
}.eraseToAnyPublisher()
} }
return reverseResolveDomainName(address: address, server: actualServer)
.flatMap { getBlockieImage(for: $0) }
.eraseToAnyPublisher()
} }
public func resolveAddressAndBlockie(string: String) -> AnyPublisher<BlockieAndAddressOrEnsResolution, PromiseError> { public func resolveAddressAndBlockie(string: String) async throws -> BlockieAndAddressOrEnsResolution {
func getBlockieImage(for addr: AlphaWallet.Address) -> AnyPublisher<BlockieAndAddressOrEnsResolution, PromiseError> { let address = try await resolveAddress(string: string)
return blockiesGenerator.getBlockieOrEnsAvatarImage(address: addr, ens: string) do {
.map { image -> BlockieAndAddressOrEnsResolution in let image = try await blockiesGenerator.getBlockieOrEnsAvatarImage(address: address, ens: string)
return (image, .resolved(.address(addr))) return (image, .resolved(.address(address)))
}.catch { _ -> AnyPublisher<BlockieAndAddressOrEnsResolution, PromiseError> in } catch {
return .just((nil, .resolved(.address(addr)))) return (nil, .resolved(.address(address)))
}.eraseToAnyPublisher()
} }
return resolveAddress(string: string)
.flatMap { getBlockieImage(for: $0) }
.eraseToAnyPublisher()
} }
public func reverseResolveDomainName(address: AlphaWallet.Address, server actualServer: RPCServer) -> AnyPublisher<DomainName, PromiseError> { public func reverseResolveDomainName(address: AlphaWallet.Address, server actualServer: RPCServer) async throws -> DomainName {
let services: [CachedDomainNameReverseResolutionServiceType] = [ let services: [CachedDomainNameReverseResolutionServiceType] = [
ensReverseLookupResolver, ensReverseLookupResolver,
unstoppableDomainsResolver unstoppableDomainsResolver
] ]
if let cached = services.compactMap({ $0.cachedDomainName(for: address) }).first { if let cached = await services.asyncCompactMap({ await $0.cachedDomainName(for: address) }).first {
return .just(cached) return cached
} }
return Just(address) do {
.setFailureType(to: SmartContractError.self) return try await ensReverseLookupResolver.getENSNameFromResolver(for: address)
.flatMap { [ensReverseLookupResolver] address in } catch {
ensReverseLookupResolver.getENSNameFromResolver(for: address) return try await unstoppableDomainsResolver.resolveDomain(address: address, server: actualServer)
}.catch { [unstoppableDomainsResolver] _ -> AnyPublisher<String, PromiseError> in }
unstoppableDomainsResolver.resolveDomain(address: address, server: actualServer)
}.receive(on: RunLoop.main)//We want to be sure it's on main
.eraseToAnyPublisher()
} }
} }

@ -6,9 +6,10 @@
// //
import Foundation import Foundation
import Combine
import AlphaWalletCore
import AlphaWalletENS import AlphaWalletENS
import AlphaWalletWeb3 import AlphaWalletWeb3
import Combine
class ENSDelegateImpl: ENSDelegate { class ENSDelegateImpl: ENSDelegate {
private let blockchainProvider: BlockchainProvider private let blockchainProvider: BlockchainProvider
@ -17,11 +18,12 @@ class ENSDelegateImpl: ENSDelegate {
self.blockchainProvider = blockchainProvider self.blockchainProvider = blockchainProvider
} }
func getInterfaceSupported165(chainId: Int, hash: String, contract: AlphaWallet.Address) -> AnyPublisher<Bool, AlphaWalletENS.SmartContractError> { func getInterfaceSupported165Async(chainId: Int, hash: String, contract: AlphaWallet.Address) async throws -> Bool {
return IsInterfaceSupported165(blockchainProvider: blockchainProvider) let publisher = IsInterfaceSupported165(blockchainProvider: blockchainProvider)
.getInterfaceSupported165(hash: hash, contract: contract) .getInterfaceSupported165(hash: hash, contract: contract)
.mapError { e in SmartContractError.embedded(e) } .mapError { e in SmartContractError.embedded(e) }
.eraseToAnyPublisher() .eraseToAnyPublisher()
return try await publisher.async()
} }
func callSmartContract(withChainId chainId: ChainId, contract: AlphaWallet.Address, functionName: String, abiString: String, parameters: [AnyObject]) -> AnyPublisher<[String: Any], SmartContractError> { func callSmartContract(withChainId chainId: ChainId, contract: AlphaWallet.Address, functionName: String, abiString: String, parameters: [AnyObject]) -> AnyPublisher<[String: Any], SmartContractError> {

@ -17,23 +17,21 @@ public class EnsResolver {
self.storage = storage self.storage = storage
} }
public func getENSAddressFromResolver(for name: String) -> AnyPublisher<AlphaWallet.Address, SmartContractError> { public func getENSAddressFromResolver(for name: String) async throws -> AlphaWallet.Address {
if let cachedResult = cachedAddress(for: name) { if let cachedResult = await cachedAddress(for: name) {
return .just(cachedResult) return cachedResult
} }
let address = try await ens.getENSAddress(fromName: name)
return ens.getENSAddress(fromName: name) let key = DomainNameLookupKey(nameOrAddress: name, server: server)
.handleEvents(receiveOutput: { [server, storage] address in await storage.addOrUpdate(record: .init(key: key, value: .address(address)))
let key = DomainNameLookupKey(nameOrAddress: name, server: server) return address
storage.addOrUpdate(record: .init(key: key, value: .address(address)))
}).eraseToAnyPublisher()
} }
} }
extension EnsResolver: CachedDomainNameResolutionServiceType { extension EnsResolver: CachedDomainNameResolutionServiceType {
public func cachedAddress(for name: String) -> AlphaWallet.Address? { public func cachedAddress(for name: String) async -> AlphaWallet.Address? {
let key = DomainNameLookupKey(nameOrAddress: name, server: self.server) let key = DomainNameLookupKey(nameOrAddress: name, server: self.server)
switch storage.record(for: key, expirationTime: Constants.DomainName.recordExpiration)?.value { switch await storage.record(for: key, expirationTime: Constants.DomainName.recordExpiration)?.value {
case .address(let address): case .address(let address):
return address return address
case .none, .record, .domainName: case .none, .record, .domainName:

@ -17,23 +17,22 @@ class EnsReverseResolver {
} }
//TODO make calls from multiple callers at the same time for the same address more efficient //TODO make calls from multiple callers at the same time for the same address more efficient
func getENSNameFromResolver(for address: AlphaWallet.Address) -> AnyPublisher<String, SmartContractError> { func getENSNameFromResolver(for address: AlphaWallet.Address) async throws -> String {
if let cachedResult = cachedDomainName(for: address) { if let cachedResult = await cachedDomainName(for: address) {
return .just(cachedResult) return cachedResult
} }
return ens.getName(fromAddress: address) let name = try await ens.getName(fromAddress: address)
.handleEvents(receiveOutput: { [server, storage] name in let key = DomainNameLookupKey(nameOrAddress: address.eip55String, server: server)
let key = DomainNameLookupKey(nameOrAddress: address.eip55String, server: server) await storage.addOrUpdate(record: .init(key: key, value: .domainName(name)))
storage.addOrUpdate(record: .init(key: key, value: .domainName(name))) return name
}).eraseToAnyPublisher()
} }
} }
extension EnsReverseResolver: CachedDomainNameReverseResolutionServiceType { extension EnsReverseResolver: CachedDomainNameReverseResolutionServiceType {
func cachedDomainName(for address: AlphaWallet.Address) -> String? { func cachedDomainName(for address: AlphaWallet.Address) async -> String? {
let key = DomainNameLookupKey(nameOrAddress: address.eip55String, server: server) let key = DomainNameLookupKey(nameOrAddress: address.eip55String, server: server)
switch storage.record(for: key, expirationTime: Constants.DomainName.recordExpiration)?.value { switch await storage.record(for: key, expirationTime: Constants.DomainName.recordExpiration)?.value {
case .domainName(let ens): case .domainName(let ens):
return ens return ens
case .none, .record, .address: case .none, .record, .address:

@ -23,28 +23,25 @@ final class GetEnsTextRecord {
ensReverseLookup = EnsReverseResolver(storage: storage, blockchainProvider: blockchainProvider) ensReverseLookup = EnsReverseResolver(storage: storage, blockchainProvider: blockchainProvider)
} }
func getENSRecord(forAddress address: AlphaWallet.Address, record: EnsTextRecordKey) -> AnyPublisher<String, SmartContractError> { func getENSRecord(forAddress address: AlphaWallet.Address, record: EnsTextRecordKey) async throws -> String {
ensReverseLookup.getENSNameFromResolver(for: address) let ens = try await ensReverseLookup.getENSNameFromResolver(for: address)
.flatMap { ens in return try await getENSRecord(forName: ens, record: record)
self.getENSRecord(forName: ens, record: record)
}.eraseToAnyPublisher()
} }
func getENSRecord(forName name: String, record: EnsTextRecordKey) -> AnyPublisher<String, SmartContractError> { func getENSRecord(forName name: String, record: EnsTextRecordKey) async throws -> String {
if let cachedResult = cachedResult(forName: name, record: record) { if let cachedResult = await cachedResult(forName: name, record: record) {
return .just(cachedResult) return cachedResult
} }
return ens.getTextRecord(forName: name, recordKey: record) let value = try await ens.getTextRecord(forName: name, recordKey: record)
.handleEvents(receiveOutput: { [storage, server] value in let key = DomainNameLookupKey(nameOrAddress: name, server: server, record: record)
let key = DomainNameLookupKey(nameOrAddress: name, server: server, record: record) await storage.addOrUpdate(record: .init(key: key, value: .record(value)))
storage.addOrUpdate(record: .init(key: key, value: .record(value))) return value
}).eraseToAnyPublisher()
} }
private func cachedResult(forName name: String, record: EnsTextRecordKey) -> String? { private func cachedResult(forName name: String, record: EnsTextRecordKey) async -> String? {
let key = DomainNameLookupKey(nameOrAddress: name, server: server, record: record) let key = DomainNameLookupKey(nameOrAddress: name, server: server, record: record)
switch storage.record(for: key, expirationTime: Constants.DomainName.recordExpiration)?.value { switch await storage.record(for: key, expirationTime: Constants.DomainName.recordExpiration)?.value {
case .record(let record): case .record(let record):
return record return record
case .domainName, .address, .none: case .domainName, .address, .none:
@ -60,29 +57,27 @@ extension GetEnsTextRecord {
case eip155(url: Eip155URL, raw: String) case eip155(url: Eip155URL, raw: String)
} }
func getEnsAvatar(for address: AlphaWallet.Address, ens: String?) -> AnyPublisher<Eip155URLOrWebImageURL, SmartContractError> { func getEnsAvatar(for address: AlphaWallet.Address, ens: String?) async throws -> Eip155URLOrWebImageURL {
enum AnyError: Error { enum AnyError: Error {
case blockieCreateFailure case blockieCreateFailure
} }
let publisher: AnyPublisher<String, SmartContractError> let _url: String
if let ens = ens { if let ens = ens {
publisher = getENSRecord(forName: ens, record: .avatar) _url = try await getENSRecord(forName: ens, record: .avatar)
} else { } else {
publisher = getENSRecord(forAddress: address, record: .avatar) _url = try await getENSRecord(forAddress: address, record: .avatar)
} }
return publisher.flatMap { _url -> AnyPublisher<Eip155URLOrWebImageURL, SmartContractError> in //NOTE: once open sea image url cached it will be here as `url`, so the next time we willn't decode it as eip155 and return it as it is
//NOTE: once open sea image url cached it will be here as `url`, so the next time we willn't decode it as eip155 and return it as it is guard let result = Eip155UrlCoder.decode(from: _url) else {
guard let result = Eip155UrlCoder.decode(from: _url) else { guard let url = URL(string: _url) else {
guard let url = URL(string: _url) else { throw SmartContractError.embedded(AnyError.blockieCreateFailure)
return .fail(.embedded(AnyError.blockieCreateFailure))
}
//NOTE: fallback to URL in case if result isn't eip155
return .just(.image(image: .url(url: WebImageURL(url: url, rewriteGoogleContentSizeUrl: .s120), isEnsAvatar: true), raw: _url))
} }
//NOTE: fallback to URL in case if result isn't eip155
return .image(image: .url(url: WebImageURL(url: url, rewriteGoogleContentSizeUrl: .s120), isEnsAvatar: true), raw: _url)
}
return .just(.eip155(url: result, raw: _url)) return .eip155(url: result, raw: _url)
}.eraseToAnyPublisher()
} }
} }

@ -11,11 +11,11 @@ import Combine
import AlphaWalletENS import AlphaWalletENS
public protocol DomainNameRecordsStorage: AnyObject { public protocol DomainNameRecordsStorage: AnyObject {
var allRecords: [DomainNameRecord] { get } var allRecords: [DomainNameRecord] { get async }
func record(for key: DomainNameLookupKey, expirationTime: TimeInterval) -> DomainNameRecord? func record(for key: DomainNameLookupKey, expirationTime: TimeInterval) async -> DomainNameRecord?
func addOrUpdate(record: DomainNameRecord) func addOrUpdate(record: DomainNameRecord) async
func removeRecord(for key: DomainNameLookupKey) func removeRecord(for key: DomainNameLookupKey) async
} }
extension DomainNameLookupKey { extension DomainNameLookupKey {
@ -33,21 +33,22 @@ extension DomainNameLookupKey {
} }
extension RealmStore: DomainNameRecordsStorage { extension RealmStore: DomainNameRecordsStorage {
public var allRecords: [DomainNameRecord] { public var allRecords: [DomainNameRecord] {
var records: [DomainNameRecord] = [] get async {
performSync { realm in var records: [DomainNameRecord] = []
records = realm.objects(EnsRecordObject.self).compactMap { DomainNameRecord(recordObject: $0) } await perform { realm in
records = realm.objects(EnsRecordObject.self).compactMap { DomainNameRecord(recordObject: $0) }
}
return records
} }
return records
} }
public func record(for key: DomainNameLookupKey, expirationTime: TimeInterval) -> DomainNameRecord? { public func record(for key: DomainNameLookupKey, expirationTime: TimeInterval) async -> DomainNameRecord? {
var record: DomainNameRecord? var record: DomainNameRecord?
let expirationDate = NSDate(timeInterval: expirationTime, since: Date()) let expirationDate = NSDate(timeInterval: expirationTime, since: Date())
let predicate = NSPredicate(format: "uid = %@ AND creatingDate > %@", key.description, expirationDate) let predicate = NSPredicate(format: "uid = %@ AND creatingDate > %@", key.description, expirationDate)
performSync { realm in await perform { realm in
record = realm.objects(EnsRecordObject.self) record = realm.objects(EnsRecordObject.self)
.filter(predicate) .filter(predicate)
.first .first
@ -57,8 +58,8 @@ extension RealmStore: DomainNameRecordsStorage {
return record return record
} }
public func addOrUpdate(record: DomainNameRecord) { public func addOrUpdate(record: DomainNameRecord) async {
performSync { realm in await perform { realm in
try? realm.safeWrite { try? realm.safeWrite {
let object = EnsRecordObject(record: record) let object = EnsRecordObject(record: record)
@ -67,9 +68,9 @@ extension RealmStore: DomainNameRecordsStorage {
} }
} }
public func removeRecord(for key: DomainNameLookupKey) { public func removeRecord(for key: DomainNameLookupKey) async {
let predicate = NSPredicate(format: "uid == '\(key.description)'") let predicate = NSPredicate(format: "uid == '\(key.description)'")
performSync { realm in await perform { realm in
try? realm.safeWrite { try? realm.safeWrite {
let objects = realm.objects(EnsRecordObject.self).filter(predicate) let objects = realm.objects(EnsRecordObject.self).filter(predicate)
realm.delete(objects) realm.delete(objects)

@ -11,17 +11,17 @@ import AlphaWalletCore
import AlphaWalletENS import AlphaWalletENS
public protocol DomainNameResolutionServiceType { public protocol DomainNameResolutionServiceType {
func resolveAddress(string value: String) -> AnyPublisher<AlphaWallet.Address, PromiseError> func resolveAddress(string value: String) async throws -> AlphaWallet.Address
func reverseResolveDomainName(address: AlphaWallet.Address, server: RPCServer) -> AnyPublisher<DomainName, PromiseError> func reverseResolveDomainName(address: AlphaWallet.Address, server: RPCServer) async throws -> DomainName
//TODO does UnstoppableDomains support blockies the same way as ENS? //TODO does UnstoppableDomains support blockies the same way as ENS?
func resolveEnsAndBlockie(address: AlphaWallet.Address, server: RPCServer) -> AnyPublisher<BlockieAndAddressOrEnsResolution, PromiseError> func resolveEnsAndBlockie(address: AlphaWallet.Address, server actualServer: RPCServer) async throws -> BlockieAndAddressOrEnsResolution
func resolveAddressAndBlockie(string: String) -> AnyPublisher<BlockieAndAddressOrEnsResolution, PromiseError> func resolveAddressAndBlockie(string: String) async throws -> BlockieAndAddressOrEnsResolution
} }
public protocol CachedDomainNameResolutionServiceType { public protocol CachedDomainNameResolutionServiceType {
func cachedAddress(for name: String) -> AlphaWallet.Address? func cachedAddress(for name: String) async -> AlphaWallet.Address?
} }
public protocol CachedDomainNameReverseResolutionServiceType { public protocol CachedDomainNameReverseResolutionServiceType {
func cachedDomainName(for address: AlphaWallet.Address) -> String? func cachedDomainName(for address: AlphaWallet.Address) async -> String?
} }

@ -18,24 +18,19 @@ struct UnstoppableDomainsNetworkProvider {
self.networkService = networkService self.networkService = networkService
} }
func resolveAddress(forName name: String) -> AnyPublisher<AlphaWallet.Address, PromiseError> { func resolveAddress(forName name: String) async throws -> AlphaWallet.Address {
return networkService let response = try await networkService.dataTask(AddressRequest(name: name))
.dataTaskPublisher(AddressRequest(name: name)) guard let json = try? JSON(data: response.data) else {
.receive(on: DispatchQueue.global()) throw UnstoppableDomainsApiError(localizedDescription: "Error calling \(Constants.unstoppableDomainsAPI.absoluteString) API isMainThread: \(Thread.isMainThread)")
.tryMap { response -> AlphaWallet.Address in }
guard let json = try? JSON(data: response.data) else {
throw UnstoppableDomainsApiError(localizedDescription: "Error calling \(Constants.unstoppableDomainsAPI.absoluteString) API isMainThread: \(Thread.isMainThread)") let value = try UnstoppableDomainsResolver.AddressResolution.Response(json: json)
} if let owner = value.meta.owner {
infoLog("[UnstoppableDomains] resolved name: \(name) result: \(owner.eip55String)")
let value = try UnstoppableDomainsResolver.AddressResolution.Response(json: json) return owner
if let owner = value.meta.owner { } else {
infoLog("[UnstoppableDomains] resolved name: \(name) result: \(owner.eip55String)") throw UnstoppableDomainsApiError(localizedDescription: "Error calling \(Constants.unstoppableDomainsAPI.absoluteString) API isMainThread: \(Thread.isMainThread)")
return owner }
} else {
throw UnstoppableDomainsApiError(localizedDescription: "Error calling \(Constants.unstoppableDomainsAPI.absoluteString) API isMainThread: \(Thread.isMainThread)")
}
}.mapError { PromiseError.some(error: $0) }
.eraseToAnyPublisher()
} }
private struct AddressRequest: URLRequestConvertible { private struct AddressRequest: URLRequestConvertible {

@ -28,25 +28,21 @@ class UnstoppableDomainsResolver {
self.networkProvider = .init(networkService: networkService) self.networkProvider = .init(networkService: networkService)
} }
func resolveDomain(address: AlphaWallet.Address, server actualServer: RPCServer) -> AnyPublisher<String, PromiseError> { func resolveDomain(address: AlphaWallet.Address, server actualServer: RPCServer) async throws -> String {
let fallbackServer = fallbackServer do {
return Just(actualServer) return try await _resolveDomain(address: address, server: actualServer)
.setFailureType(to: PromiseError.self) } catch {
.flatMap { [self] actualServer in if actualServer == fallbackServer {
_resolveDomain(address: address, server: actualServer) throw error
.catch { error -> AnyPublisher<String, PromiseError> in } else {
if actualServer == fallbackServer { return try await _resolveDomain(address: address, server: fallbackServer)
return Fail(error: error).eraseToAnyPublisher() }
} else { }
return _resolveDomain(address: address, server: fallbackServer)
}
}.receive(on: RunLoop.main)
}.eraseToAnyPublisher()
} }
private func _resolveDomain(address: AlphaWallet.Address, server: RPCServer) -> AnyPublisher<String, PromiseError> { private func _resolveDomain(address: AlphaWallet.Address, server: RPCServer) async throws -> String {
if let cachedResult = cachedDomainName(for: address) { if let cachedResult = await cachedDomainName(for: address) {
return .just(cachedResult) return cachedResult
} }
let parameters = [EthereumAddress(address: address)] as [AnyObject] let parameters = [EthereumAddress(address: address)] as [AnyObject]
@ -63,46 +59,39 @@ class UnstoppableDomainsResolver {
}, },
] ]
""" """
return callSmartContract(withServer: server, contract: AlphaWallet.Address(string: "0xa9a6A3626993D487d2Dbda3173cf58cA1a9D9e9f")!, functionName: "reverseNameOf", abiString: abiString, parameters: parameters) do {
.publisher() let result = try await callSmartContractAsync(withServer: server, contract: AlphaWallet.Address(string: "0xa9a6A3626993D487d2Dbda3173cf58cA1a9D9e9f")!, functionName: "reverseNameOf", abiString: abiString, parameters: parameters)
.tryMap { result in if let name = result["0"] as? String, !name.isEmpty {
if let name = result["0"] as? String, !name.isEmpty { return name
return name } else {
} else { throw UnstoppableDomainsApiError(localizedDescription: "Can't reverse resolve \(address.eip55String) on: \(server)")
throw UnstoppableDomainsApiError(localizedDescription: "Can't reverse resolve \(address.eip55String) on: \(server)")
}
} }
.mapError { PromiseError.some(error: $0) } } catch {
.eraseToAnyPublisher() throw UnstoppableDomainsApiError(localizedDescription: "Can't reverse resolve \(address.eip55String) on: \(server)")
}
} }
func resolveAddress(forName name: String) -> AnyPublisher<AlphaWallet.Address, PromiseError> { func resolveAddress(forName name: String) async throws -> AlphaWallet.Address {
if let value = AlphaWallet.Address(string: name) { if let value = AlphaWallet.Address(string: name) {
return .just(value) return value
} }
if let value = self.cachedAddress(for: name) { if let value = await self.cachedAddress(for: name) {
return .just(value) return value
} }
return Just(name) infoLog("[UnstoppableDomains] resolving name: \(name)")
.setFailureType(to: PromiseError.self) let address = try await networkProvider.resolveAddress(forName: name)
.flatMap { [networkProvider] name -> AnyPublisher<AlphaWallet.Address, PromiseError> in let key = DomainNameLookupKey(nameOrAddress: name, server: self.fallbackServer)
infoLog("[UnstoppableDomains] resolving name: \(name)") await self.storage.addOrUpdate(record: .init(key: key, value: .address(address)))
return networkProvider.resolveAddress(forName: name) return address
.handleEvents(receiveOutput: { address in
let key = DomainNameLookupKey(nameOrAddress: name, server: self.fallbackServer)
self.storage.addOrUpdate(record: .init(key: key, value: .address(address)))
}).share()
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
} }
} }
extension UnstoppableDomainsResolver: CachedDomainNameReverseResolutionServiceType { extension UnstoppableDomainsResolver: CachedDomainNameReverseResolutionServiceType {
func cachedDomainName(for address: AlphaWallet.Address) -> String? { func cachedDomainName(for address: AlphaWallet.Address) async -> String? {
let key = DomainNameLookupKey(nameOrAddress: address.eip55String, server: fallbackServer) let key = DomainNameLookupKey(nameOrAddress: address.eip55String, server: fallbackServer)
switch storage.record(for: key, expirationTime: Constants.DomainName.recordExpiration)?.value { switch await storage.record(for: key, expirationTime: Constants.DomainName.recordExpiration)?.value {
case .domainName(let domainName): case .domainName(let domainName):
return domainName return domainName
case .record, .address, .none: case .record, .address, .none:
@ -112,9 +101,9 @@ extension UnstoppableDomainsResolver: CachedDomainNameReverseResolutionServiceTy
} }
extension UnstoppableDomainsResolver: CachedDomainNameResolutionServiceType { extension UnstoppableDomainsResolver: CachedDomainNameResolutionServiceType {
func cachedAddress(for name: String) -> AlphaWallet.Address? { func cachedAddress(for name: String) async -> AlphaWallet.Address? {
let key = DomainNameLookupKey(nameOrAddress: name, server: fallbackServer) let key = DomainNameLookupKey(nameOrAddress: name, server: fallbackServer)
switch storage.record(for: key, expirationTime: Constants.DomainName.recordExpiration)?.value { switch await storage.record(for: key, expirationTime: Constants.DomainName.recordExpiration)?.value {
case .address(let address): case .address(let address):
return address return address
case .record, .domainName, .none: case .record, .domainName, .none:

@ -140,3 +140,27 @@ public extension Future where Failure == Error {
} }
} }
} }
//TODO remove most if not all callers once we migrate completely to async-await
public func asFuture<T>(block: @escaping () async -> T) -> Future<T, Never> {
Future<T, Never> { promise in
Task { @MainActor in
let result: T = await block()
promise(.success(result))
}
}
}
//TODO remove most if not all callers once we migrate completely to async-await
public func asFutureThrowable<T>(block: @escaping () async throws -> T) -> Future<T, Error> {
Future<T, Error> { promise in
Task { @MainActor in
do {
let result: T = try await block()
promise(.success(result))
} catch {
promise(.failure(error))
}
}
}
}

@ -47,4 +47,19 @@ extension APIKitSession {
return publisher return publisher
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
}
class func sendPublisherAsync<Request: APIKit.Request>(_ request: Request, server: RPCServer, analytics: AnalyticsLogger, callbackQueue: CallbackQueue? = nil) async throws -> Request.Response {
try await Task.retrying(times: 2) { @MainActor in
return try await withCheckedThrowingContinuation { continuation in
var sessionTask: SessionTask? = APIKitSession.send(request, callbackQueue: callbackQueue) { result in
switch result {
case .success(let result):
continuation.resume(returning: result)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}.value
}
}

@ -17,3 +17,20 @@ extension Task {
.init { self.cancel() } .init { self.cancel() }
} }
} }
extension Task where Failure == Error {
@discardableResult static func retrying(priority: TaskPriority? = nil, times maxRetryCount: Int = 3, operation: @Sendable @escaping () async throws -> Success) -> Task {
Task(priority: priority) {
for _ in 0..<maxRetryCount {
try Task<Never, Never>.checkCancellation()
do {
return try await operation()
} catch {
continue
}
}
try Task<Never, Never>.checkCancellation()
return try await operation()
}
}
}

@ -39,8 +39,8 @@ public final class Enjin {
} }
} }
func token(tokenId: TokenId) -> EnjinToken? { func token(tokenId: TokenId) async -> EnjinToken? {
return storage.getEnjinToken(for: tokenId, server: server) return await storage.getEnjinToken(for: tokenId, server: server)
} }
public func fetchTokens(wallet: Wallet) -> AnyPublisher<[EnjinToken], PromiseError> { public func fetchTokens(wallet: Wallet) -> AnyPublisher<[EnjinToken], PromiseError> {

@ -9,14 +9,14 @@ import Foundation
import RealmSwift import RealmSwift
public protocol EnjinStorage { public protocol EnjinStorage {
func getEnjinToken(for tokenId: TokenId, server: RPCServer) -> EnjinToken? func getEnjinToken(for tokenId: TokenId, server: RPCServer) async -> EnjinToken?
func addOrUpdate(enjinTokens tokens: [EnjinToken], server: RPCServer) func addOrUpdate(enjinTokens tokens: [EnjinToken], server: RPCServer)
} }
extension RealmStore: EnjinStorage { extension RealmStore: EnjinStorage {
public func getEnjinToken(for tokenId: TokenId, server: RPCServer) -> EnjinToken? { public func getEnjinToken(for tokenId: TokenId, server: RPCServer) async -> EnjinToken? {
var token: EnjinToken? var token: EnjinToken?
performSync { realm in await perform { realm in
let primaryKey = EnjinTokenObject.generatePrimaryKey( let primaryKey = EnjinTokenObject.generatePrimaryKey(
server: server, server: server,
tokenId: TokenIdConverter.toTokenIdSubstituted(string: tokenId.description)) tokenId: TokenIdConverter.toTokenIdSubstituted(string: tokenId.description))
@ -30,12 +30,14 @@ extension RealmStore: EnjinStorage {
public func addOrUpdate(enjinTokens tokens: [EnjinToken], server: RPCServer) { public func addOrUpdate(enjinTokens tokens: [EnjinToken], server: RPCServer) {
guard !tokens.isEmpty else { return } guard !tokens.isEmpty else { return }
performSync { realm in Task {
try? realm.safeWrite { await perform { realm in
realm.delete(realm.objects(EnjinTokenObject.self)) try? realm.safeWrite {
realm.delete(realm.objects(EnjinTokenObject.self))
let tokens = tokens.map { EnjinTokenObject(token: $0, server: server) } let tokens = tokens.map { EnjinTokenObject(token: $0, server: server) }
realm.add(tokens, update: .all) realm.add(tokens, update: .all)
}
} }
} }
} }

@ -15,22 +15,19 @@ public typealias NonFungiblesTokens = (openSea: OpenSeaAddressesToNonFungibles,
public protocol NFTProvider { public protocol NFTProvider {
func collectionStats(collectionId: String) -> AnyPublisher<Stats, PromiseError> func collectionStats(collectionId: String) -> AnyPublisher<Stats, PromiseError>
func nonFungible() -> AnyPublisher<NonFungiblesTokens, Never> func nonFungible() -> AnyPublisher<NonFungiblesTokens, Never>
func enjinToken(tokenId: TokenId) -> EnjinToken? func enjinToken(tokenId: TokenId) async -> EnjinToken?
} }
extension OpenSea: NftAssetImageProvider { extension OpenSea: NftAssetImageProvider {
public func assetImageUrl(for url: Eip155URL) -> AnyPublisher<URL, PromiseError> { public func assetImageUrl(for url: Eip155URL) async throws -> URL {
fetchAsset(for: url) let asset = try await fetchAsset(for: url)
.map { [$0.imageUrl, $0.thumbnailUrl, $0.imageOriginalUrl].compactMap { URL(string: $0) } } let imageUrls = [asset.imageUrl, asset.thumbnailUrl, asset.imageOriginalUrl].compactMap { URL(string: $0) }
.tryMap { if let url = imageUrls.first {
if let url = $0.first { return url
return url } else {
} else { struct AssetImageUrlNotFound: Error {}
struct AssetImageUrlNotFound: Error {} throw PromiseError(error: AssetImageUrlNotFound())
throw PromiseError(error: AssetImageUrlNotFound()) }
}
}.mapError { PromiseError(error: $0) }
.eraseToAnyPublisher()
} }
} }
@ -73,8 +70,8 @@ public final class AlphaWalletNFTProvider: NFTProvider {
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
public func enjinToken(tokenId: TokenId) -> EnjinToken? { public func enjinToken(tokenId: TokenId) async -> EnjinToken? {
enjin.token(tokenId: tokenId) await enjin.token(tokenId: tokenId)
} }
public func nonFungible() -> AnyPublisher<NonFungiblesTokens, Never> { public func nonFungible() -> AnyPublisher<NonFungiblesTokens, Never> {

@ -65,11 +65,13 @@ public final class OpenSea {
}.eraseToAnyPublisher() }.eraseToAnyPublisher()
} }
public func fetchAsset(for value: Eip155URL) -> AnyPublisher<NftAsset, PromiseError> { public func fetchAsset(for value: Eip155URL) async throws -> NftAsset {
//OK and safer to return a promise that never resolves so we don't mangle with real OpenSea data we stored previously, since this is for development only //OK and safer to return a promise that never resolves so we don't mangle with real OpenSea data we stored previously, since this is for development only
guard !config.development.isOpenSeaFetchingDisabled else { return .empty() } //TODO fix
struct DisabledError: Error {}
guard !config.development.isOpenSeaFetchingDisabled else { throw DisabledError() }
return openSea.fetchAsset(asset: value.path, chainId: server.chainID) return try await openSea.fetchAsset(asset: value.path, chainId: server.chainID)
} }
public func collectionStats(collectionId: String) -> AnyPublisher<Stats, PromiseError> { public func collectionStats(collectionId: String) -> AnyPublisher<Stats, PromiseError> {

@ -13,6 +13,7 @@ import AlphaWalletWeb3
import APIKit import APIKit
import BigInt import BigInt
import JSONRPCKit import JSONRPCKit
import PromiseKit
public protocol BlockchainProvider: BlockchainCallable { public protocol BlockchainProvider: BlockchainCallable {
var server: RPCServer { get } var server: RPCServer { get }
@ -23,8 +24,8 @@ public protocol BlockchainProvider: BlockchainCallable {
func call(from: AlphaWallet.Address?, to: AlphaWallet.Address?, value: String?, data: String) -> AnyPublisher<String, SessionTaskError> func call(from: AlphaWallet.Address?, to: AlphaWallet.Address?, value: String?, data: String) -> AnyPublisher<String, SessionTaskError>
func transaction(byHash hash: String) -> AnyPublisher<EthereumTransaction?, SessionTaskError> func transaction(byHash hash: String) -> AnyPublisher<EthereumTransaction?, SessionTaskError>
func nextNonce(wallet: AlphaWallet.Address) -> AnyPublisher<Int, SessionTaskError> func nextNonce(wallet: AlphaWallet.Address) -> AnyPublisher<Int, SessionTaskError>
func block(by blockNumber: BigUInt) -> AnyPublisher<Block, SessionTaskError> func block(by blockNumber: BigUInt) async throws -> Block
func eventLogs(contractAddress: AlphaWallet.Address, eventName: String, abiString: String, filter: EventFilter) -> AnyPublisher<[EventParserResultProtocol], SessionTaskError> func eventLogs(contractAddress: AlphaWallet.Address, eventName: String, abiString: String, filter: EventFilter) async throws -> [EventParserResultProtocol]
func gasEstimates() -> AnyPublisher<LegacyGasEstimates, PromiseError> func gasEstimates() -> AnyPublisher<LegacyGasEstimates, PromiseError>
func gasLimit(wallet: AlphaWallet.Address, value: BigUInt, toAddress: AlphaWallet.Address?, data: Data) -> AnyPublisher<BigUInt, SessionTaskError> func gasLimit(wallet: AlphaWallet.Address, value: BigUInt, toAddress: AlphaWallet.Address?, data: Data) -> AnyPublisher<BigUInt, SessionTaskError>
func send(rawTransaction: String) -> AnyPublisher<String, SessionTaskError> func send(rawTransaction: String) -> AnyPublisher<String, SessionTaskError>
@ -94,6 +95,11 @@ public final class RpcBlockchainProvider: BlockchainProvider {
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
public func callAsync<R: ContractMethodCall>(_ method: R, block: BlockParameter) async throws -> R.Response {
let response = try await callSmartContractAsync(withServer: server, contract: method.contract, functionName: method.name, abiString: method.abi, parameters: method.parameters)
return try method.response(from: response)
}
public func blockNumber() -> AnyPublisher<Int, SessionTaskError> { public func blockNumber() -> AnyPublisher<Int, SessionTaskError> {
let request = EtherServiceRequest(server: server, batch: BatchFactory().create(BlockNumberRequest())) let request = EtherServiceRequest(server: server, batch: BatchFactory().create(BlockNumberRequest()))
@ -112,10 +118,9 @@ public final class RpcBlockchainProvider: BlockchainProvider {
return APIKitSession.sendPublisher(request, server: server, analytics: analytics) return APIKitSession.sendPublisher(request, server: server, analytics: analytics)
} }
public func block(by blockNumber: BigUInt) -> AnyPublisher<Block, SessionTaskError> { public func block(by blockNumber: BigUInt) async throws -> Block {
let request = EtherServiceRequest(server: server, batch: BatchFactory().create(BlockByNumberRequest(number: blockNumber))) let request = EtherServiceRequest(server: server, batch: BatchFactory().create(BlockByNumberRequest(number: blockNumber)))
return try await APIKitSession.sendPublisherAsync(request, server: server, analytics: analytics)
return APIKitSession.sendPublisher(request, server: server, analytics: analytics)
} }
public func feeHistory(blockCount: Int, block: BlockParameter, rewardPercentile: [Int]) -> AnyPublisher<FeeHistory, SessionTaskError> { public func feeHistory(blockCount: Int, block: BlockParameter, rewardPercentile: [Int]) -> AnyPublisher<FeeHistory, SessionTaskError> {
@ -125,11 +130,16 @@ public final class RpcBlockchainProvider: BlockchainProvider {
return APIKitSession.sendPublisher(request, server: server, analytics: analytics) return APIKitSession.sendPublisher(request, server: server, analytics: analytics)
} }
public func eventLogs(contractAddress: AlphaWallet.Address, eventName: String, abiString: String, filter: EventFilter) -> AnyPublisher<[EventParserResultProtocol], SessionTaskError> { public func eventLogs(contractAddress: AlphaWallet.Address, eventName: String, abiString: String, filter: EventFilter) async throws -> [EventParserResultProtocol] {
getEventLogs.getEventLogs(contractAddress: contractAddress, server: server, eventName: eventName, abiString: abiString, filter: filter) return try await withCheckedThrowingContinuation { continuation in
.publisher() firstly {
.mapError { SessionTaskError.responseError($0.embedded) } getEventLogs.getEventLogs(contractAddress: contractAddress, server: server, eventName: eventName, abiString: abiString, filter: filter)
.eraseToAnyPublisher() }.done { result in
continuation.resume(returning: result)
}.catch {
continuation.resume(throwing: $0)
}
}
} }
public func gasEstimates() -> AnyPublisher<LegacyGasEstimates, PromiseError> { public func gasEstimates() -> AnyPublisher<LegacyGasEstimates, PromiseError> {

@ -115,6 +115,18 @@ public func callSmartContract(withServer server: RPCServer, contract contractAdd
}) })
} }
public func callSmartContractAsync(withServer server: RPCServer, contract contractAddress: AlphaWallet.Address, functionName: String, abiString: String, parameters: [AnyObject] = [], shouldDelayIfCached: Bool = false) async throws -> [String: Any] {
return try await withCheckedThrowingContinuation { continuation in
firstly {
callSmartContract(withServer: server, contract: contractAddress, functionName: functionName, abiString: abiString, parameters: parameters)
}.done {
continuation.resume(returning: $0)
}.catch { error in
continuation.resume(throwing: error)
}
}
}
public func getSmartContractCallData(withServer server: RPCServer, contract contractAddress: AlphaWallet.Address, functionName: String, abiString: String, parameters: [AnyObject] = []) -> Data? { public func getSmartContractCallData(withServer server: RPCServer, contract contractAddress: AlphaWallet.Address, functionName: String, abiString: String, parameters: [AnyObject] = []) -> Data? {
guard let web3 = try? Web3.instance(for: server, timeout: 60) else { return nil } guard let web3 = try? Web3.instance(for: server, timeout: 60) else { return nil }
guard let contract = try? Web3.Contract(web3: web3, abiString: abiString, at: EthereumAddress(address: contractAddress), options: web3.options) else { return nil } guard let contract = try? Web3.Contract(web3: web3, abiString: abiString, at: EthereumAddress(address: contractAddress), options: web3.options) else { return nil }

@ -149,10 +149,10 @@ public final class SwapOptionsConfigurator {
self.server = server self.server = server
} }
public func isAvailable(server: RPCServer) -> Bool { public func isAvailable(server: RPCServer) async -> Bool {
switch tokenSwapper.supportState(for: server) { switch tokenSwapper.supportState(for: server) {
case .supports: case .supports:
return hasAnySuportedToken(forServer: server) return await hasAnySuportedToken(forServer: server)
case .notSupports, .failure: case .notSupports, .failure:
return false return false
} }
@ -231,29 +231,31 @@ public final class SwapOptionsConfigurator {
} }
private func validateSwapPair(forServer server: RPCServer, isInitialServerValidation: Bool) { private func validateSwapPair(forServer server: RPCServer, isInitialServerValidation: Bool) {
do { Task { @MainActor in
let tokens = try supportedTokens(forServer: server) do {
let token = try firstSupportedFromToken(forServer: server, tokens: tokens) let tokens = try await supportedTokens(forServer: server)
if isInitialServerValidation && swapPair.from.contractAddress != token.contractAddress { let token = try firstSupportedFromToken(forServer: server, tokens: tokens)
let _ = try firstSupportedFromToken(forServer: server, tokens: [swapPair.from]) if isInitialServerValidation && swapPair.from.contractAddress != token.contractAddress {
//NOTE: no changes needed as current swapPair.from supports let _ = try firstSupportedFromToken(forServer: server, tokens: [swapPair.from])
} else { //NOTE: no changes needed as current swapPair.from supports
swapPair = SwapPair(from: token, to: nil) } else {
swapPair = SwapPair(from: token, to: nil)
}
} catch TokenSwapper.TokenSwapperError.fromTokenNotFound {
errorSubject.send(.fromTokenNotFound)
} }
} catch TokenSwapper.TokenSwapperError.fromTokenNotFound { }
errorSubject.send(.fromTokenNotFound)
} catch { }
} }
private func hasAnySuportedToken(forServer server: RPCServer) -> Bool { private func hasAnySuportedToken(forServer server: RPCServer) async -> Bool {
guard let tokens = try? supportedTokens(forServer: server) else { return false } guard let tokens = try? await supportedTokens(forServer: server) else { return false }
let token = try? firstSupportedFromToken(forServer: server, tokens: tokens) let token = try? firstSupportedFromToken(forServer: server, tokens: tokens)
return token != nil return token != nil
} }
private func supportedTokens(forServer server: RPCServer) throws -> [Token] { private func supportedTokens(forServer server: RPCServer) async throws -> [Token] {
guard swapPairs(for: server) != nil else { throw TokenSwapper.TokenSwapperError.swapPairNotFound } guard swapPairs(for: server) != nil else { throw TokenSwapper.TokenSwapperError.swapPairNotFound }
return tokensService.tokens(for: [server]) return await tokensService.tokens(for: [server])
} }
private func firstSupportedFromToken(forServer server: RPCServer, tokens: [Token]) throws -> Token { private func firstSupportedFromToken(forServer server: RPCServer, tokens: [Token]) throws -> Token {

@ -24,46 +24,35 @@ final class EventFetcher {
self.sessionsProvider = sessionsProvider self.sessionsProvider = sessionsProvider
} }
func fetchEvents(tokenId: TokenId, token: Token, eventOrigin: EventOrigin, oldEventBlockNumber: Int?) -> AnyPublisher<[EventInstanceValue], SessionTaskError> { func fetchEvents(tokenId: TokenId, token: Token, eventOrigin: EventOrigin, oldEventBlockNumber: Int?) async throws -> [EventInstanceValue] {
Just(tokenId) guard let session = sessionsProvider.session(for: token.server) else {
.setFailureType(to: SessionTaskError.self) throw SessionTaskError(error: WalletSession.SessionError.sessionNotFound)
.flatMap { [sessionsProvider] tokenId -> AnyPublisher<[EventInstanceValue], SessionTaskError> in }
guard let session = sessionsProvider.session(for: token.server) else {
return .fail(SessionTaskError(error: WalletSession.SessionError.sessionNotFound)) let (filterName, filterValue) = eventOrigin.eventFilter
} let filterParam: [(filter: [EventFilterable], textEquivalent: String)?] = eventOrigin.parameters
.filter { $0.isIndexed }
let (filterName, filterValue) = eventOrigin.eventFilter .map { EventSource.functional.formFilterFrom(fromParameter: $0, tokenId: tokenId, filterName: filterName, filterValue: filterValue, wallet: session.account) }
let filterParam = eventOrigin let fromBlock: EventFilter.Block
.parameters if let newestEvent = oldEventBlockNumber {
.filter { $0.isIndexed } fromBlock = .blockNumber(UInt64(newestEvent + 1))
.map { EventSource.functional.formFilterFrom(fromParameter: $0, tokenId: tokenId, filterName: filterName, filterValue: filterValue, wallet: session.account) } } else {
fromBlock = .blockNumber(token.server.startBlock)
let fromBlock: EventFilter.Block }
if let newestEvent = oldEventBlockNumber { let addresses = [EthereumAddress(address: eventOrigin.contract)]
fromBlock = .blockNumber(UInt64(newestEvent + 1)) let parameterFilters = filterParam.map { $0?.filter }
} else {
fromBlock = .blockNumber(token.server.startBlock) let eventFilter = EventFilter(fromBlock: fromBlock, toBlock: .latest, addresses: addresses, parameterFilters: parameterFilters)
}
let addresses = [EthereumAddress(address: eventOrigin.contract)] do {
let parameterFilters = filterParam.map { $0?.filter } let events = try await session.blockchainProvider.eventLogs(contractAddress: eventOrigin.contract, eventName: eventOrigin.eventName, abiString: eventOrigin.eventAbiString, filter: eventFilter)
let result = events.compactMap {
let eventFilter = EventFilter(fromBlock: fromBlock, toBlock: .latest, addresses: addresses, parameterFilters: parameterFilters) EventSource.functional.convertEventToDatabaseObject($0, filterParam: filterParam, eventOrigin: eventOrigin, contractAddress: token.contractAddress, server: token.server)
}
return session.blockchainProvider return result
.eventLogs( } catch {
contractAddress: eventOrigin.contract, logError(error, rpcServer: token.server, address: token.contractAddress)
eventName: eventOrigin.eventName, throw error
abiString: eventOrigin.eventAbiString, }
filter: eventFilter)
.map { result -> [EventInstanceValue] in
return result.compactMap {
EventSource.functional.convertEventToDatabaseObject($0, filterParam: filterParam, eventOrigin: eventOrigin, contractAddress: token.contractAddress, server: token.server)
}
}.handleEvents(receiveCompletion: { result in
guard case .failure(let e) = result else { return }
logError(e, rpcServer: token.server, address: token.contractAddress)
}).eraseToAnyPublisher()
}.eraseToAnyPublisher()
} }
} }

@ -17,84 +17,66 @@ final class EventForActivitiesFetcher {
case invalidParams case invalidParams
case sessionNotFound case sessionNotFound
} }
private let sessionsProvider: SessionsProvider private let sessionsProvider: SessionsProvider
init(sessionsProvider: SessionsProvider) { init(sessionsProvider: SessionsProvider) {
self.sessionsProvider = sessionsProvider self.sessionsProvider = sessionsProvider
} }
func fetchEvents(token: Token, card: TokenScriptCard, oldEventBlockNumber: Int?) -> AnyPublisher<[EventActivityInstance], SessionTaskError> { func fetchEvents(token: Token, card: TokenScriptCard, oldEventBlockNumber: Int?) async throws -> [EventActivityInstance] {
Just(token) guard let session = sessionsProvider.session(for: token.server) else {
.setFailureType(to: SessionTaskError.self) throw SessionTaskError(error: FetcherError.sessionNotFound)
.flatMap { [sessionsProvider] token -> AnyPublisher<[EventActivityInstance], SessionTaskError> in }
guard let session = sessionsProvider.session(for: token.server) else {
return .fail(SessionTaskError(error: FetcherError.sessionNotFound))
}
let eventOrigin = card.eventOrigin
let (filterName, filterValue) = eventOrigin.eventFilter
let filterParam = eventOrigin.parameters
.filter { $0.isIndexed }
.map { EventSourceForActivities.functional.formFilterFrom(fromParameter: $0, filterName: filterName, filterValue: filterValue, wallet: session.account) }
if filterParam.allSatisfy({ $0 == nil }) {
//TODO log to console as diagnostic
return .fail(SessionTaskError(error: FetcherError.invalidParams))
}
let fromBlock: (EventFilter.Block, UInt64) //Just(token)
if let blockNumber = oldEventBlockNumber { // .flatMap { [sessionsProvider] token -> AnyPublisher<[EventActivityInstance], SessionTaskError> in
let value = UInt64(blockNumber + 1)
fromBlock = (.blockNumber(value), value)
} else {
fromBlock = (.blockNumber(token.server.startBlock), token.server.startBlock)
}
let parameterFilters = filterParam.map { $0?.filter } let eventOrigin = card.eventOrigin
let addresses = [EthereumAddress(address: eventOrigin.contract)] let (filterName, filterValue) = eventOrigin.eventFilter
let toBlock = token.server.makeMaximumToBlockForEvents(fromBlockNumber: fromBlock.1) let filterParam = eventOrigin.parameters
.filter {
$0.isIndexed
}
.map {
EventSourceForActivities.functional.formFilterFrom(fromParameter: $0, filterName: filterName, filterValue: filterValue, wallet: session.account)
}
let eventFilter = EventFilter( if filterParam.allSatisfy({ $0 == nil }) {
fromBlock: fromBlock.0, //TODO log to console as diagnostic
toBlock: toBlock, throw SessionTaskError(error: FetcherError.invalidParams)
addresses: addresses, }
parameterFilters: parameterFilters)
return session.blockchainProvider let fromBlock: (EventFilter.Block, UInt64)
.eventLogs( if let blockNumber = oldEventBlockNumber {
contractAddress: eventOrigin.contract, let value = UInt64(blockNumber + 1)
eventName: eventOrigin.eventName, fromBlock = (.blockNumber(value), value)
abiString: eventOrigin.eventAbiString, } else {
filter: eventFilter) fromBlock = (.blockNumber(token.server.startBlock), token.server.startBlock)
.flatMap { events -> AnyPublisher<[EventActivityInstance], SessionTaskError> in }
let publishers = events.compactMap { event -> AnyPublisher<EventActivityInstance?, Never> in
guard let blockNumber = event.eventLog?.blockNumber else {
return .just(nil)
}
return session.blockchainProvider.block(by: blockNumber)
.map { block in
EventSourceForActivities.functional.convertEventToDatabaseObject(
event,
date: block.timestamp,
filterParam: filterParam,
eventOrigin: eventOrigin,
tokenContract: token.contractAddress,
server: token.server)
}.replaceError(with: nil)
.eraseToAnyPublisher()
}
return Publishers.MergeMany(publishers) let parameterFilters = filterParam.map {
.collect() $0?.filter
.map { $0.compactMap { $0 } } }
.setFailureType(to: SessionTaskError.self) let addresses = [EthereumAddress(address: eventOrigin.contract)]
.eraseToAnyPublisher() let toBlock = token.server.makeMaximumToBlockForEvents(fromBlockNumber: fromBlock.1)
}.handleEvents(receiveCompletion: { result in let eventFilter = EventFilter(
guard case .failure(let e) = result else { return } fromBlock: fromBlock.0,
toBlock: toBlock,
addresses: addresses,
parameterFilters: parameterFilters)
logError(e, rpcServer: token.server, address: token.contractAddress) let events = try await session.blockchainProvider.eventLogs(contractAddress: eventOrigin.contract, eventName: eventOrigin.eventName, abiString: eventOrigin.eventAbiString, filter: eventFilter)
}).eraseToAnyPublisher() let results = await events.asyncCompactMap { event -> EventActivityInstance? in
}.eraseToAnyPublisher() guard let blockNumber = event.eventLog?.blockNumber else { return nil }; do {
let block = try await session.blockchainProvider.block(by: blockNumber)
return EventSourceForActivities.functional.convertEventToDatabaseObject(event, date: block.timestamp, filterParam: filterParam, eventOrigin: eventOrigin, tokenContract: token.contractAddress, server: token.server)
} catch {
logError(error, rpcServer: token.server, address: token.contractAddress)
return nil
}
}
return results
} }
} }

@ -97,8 +97,8 @@ final class EventSource {
var tokenScriptChanged: AnyPublisher<[Token], Never> { var tokenScriptChanged: AnyPublisher<[Token], Never> {
assetDefinitionStore.bodyChange assetDefinitionStore.bodyChange
.receive(on: queue) .receive(on: queue)
.compactMap { [tokensService] in tokensService.token(for: $0) } .flatMap { [tokensService] address in asFuture { await tokensService.token(for: address) } }.compactMap { $0 }
.compactMap { self.tokensBasedOnTokenScriptServer(token: $0) } .flatMap { token in asFuture { await self.tokensBasedOnTokenScriptServer(token: token) } }
.handleEvents(receiveOutput: { [eventsDataStore] in $0.map { eventsDataStore.deleteEvents(for: $0.contractAddress) } }) .handleEvents(receiveOutput: { [eventsDataStore] in $0.map { eventsDataStore.deleteEvents(for: $0.contractAddress) } })
.share() .share()
.eraseToAnyPublisher() .eraseToAnyPublisher()
@ -115,14 +115,14 @@ final class EventSource {
self.tokensService = tokensService self.tokensService = tokensService
} }
private func tokensBasedOnTokenScriptServer(token: Token) -> [Token] { private func tokensBasedOnTokenScriptServer(token: Token) async -> [Token] {
guard let session = sessionsProvider.session(for: token.server) else { return [] } guard let session = sessionsProvider.session(for: token.server) else { return [] }
let xmlHandler = session.tokenAdaptor.xmlHandler(token: token) let xmlHandler = session.tokenAdaptor.xmlHandler(token: token)
guard xmlHandler.hasAssetDefinition, let server = xmlHandler.server else { return [] } guard xmlHandler.hasAssetDefinition, let server = xmlHandler.server else { return [] }
switch server { switch server {
case .any: case .any:
let enabledServers = sessionsProvider.activeSessions.map { $0.key } let enabledServers = sessionsProvider.activeSessions.map { $0.key }
return enabledServers.compactMap { tokensService.token(for: token.contractAddress, server: $0) } return await enabledServers.asyncCompactMap { server in await tokensService.token(for: token.contractAddress, server: server) }
case .server(let server): case .server(let server):
return [token] return [token]
} }
@ -285,7 +285,9 @@ final class EventSource {
cancellable = Publishers.Merge3(initial, force, timedOrWaitForCurrent) cancellable = Publishers.Merge3(initial, force, timedOrWaitForCurrent)
.receive(on: DispatchQueue.global()) .receive(on: DispatchQueue.global())
.flatMap { [eventsFetcher] in eventsFetcher.fetchEvents(token: $0.token) } .flatMap { [eventsFetcher] request in
eventsFetcher.fetchEvents(token: request.token)
}
.sink { _ in } .sink { _ in }
} }
@ -315,7 +317,7 @@ final class EventSource {
self.eventsDataStore = eventsDataStore self.eventsDataStore = eventsDataStore
} }
private func getEventOriginsAndTokenIds(token: Token) -> [(eventOrigin: EventOrigin, tokenIds: [TokenId])] { private func getEventOriginsAndTokenIds(token: Token) async -> [(eventOrigin: EventOrigin, tokenIds: [TokenId])] {
guard let session = sessionsProvider.session(for: token.server) else { return [] } guard let session = sessionsProvider.session(for: token.server) else { return [] }
var cards: [(eventOrigin: EventOrigin, tokenIds: [TokenId])] = [] var cards: [(eventOrigin: EventOrigin, tokenIds: [TokenId])] = []
@ -326,7 +328,7 @@ final class EventSource {
for each in xmlHandler.attributesWithEventSource { for each in xmlHandler.attributesWithEventSource {
guard let eventOrigin = each.eventOrigin else { continue } guard let eventOrigin = each.eventOrigin else { continue }
let tokenHolders = session.tokenAdaptor.getTokenHolders(token: token, isSourcedFromEvents: false) let tokenHolders = await session.tokenAdaptor.getTokenHolders(token: token, isSourcedFromEvents: false)
let tokenIds = tokenHolders.flatMap { $0.tokenIds } let tokenIds = tokenHolders.flatMap { $0.tokenIds }
cards.append((eventOrigin, tokenIds)) cards.append((eventOrigin, tokenIds))
@ -336,28 +338,22 @@ final class EventSource {
} }
func fetchEvents(token: Token) -> EventPublisher { func fetchEvents(token: Token) -> EventPublisher {
let publishers = getEventOriginsAndTokenIds(token: token) let subject = PassthroughSubject<[EventInstanceValue], Never>()
.flatMap { value in Task { @MainActor in
value.tokenIds.map { tokenId -> EventPublisher in let eventsAndTokenIds: [(eventOrigin: EventOrigin, tokenIds: [TokenId])] = await self.getEventOriginsAndTokenIds(token: token)
let all: [EventInstanceValue] = try await eventsAndTokenIds.asyncMap { (value: (eventOrigin: EventOrigin, tokenIds: [TokenId])) in
let what: [EventInstanceValue] = try await value.tokenIds.asyncFlatMap { tokenId in
let eventOrigin = value.eventOrigin let eventOrigin = value.eventOrigin
let oldEvent = eventsDataStore.getLastMatchingEventSortedByBlockNumber( let oldEvent = await self.eventsDataStore.getLastMatchingEventSortedByBlockNumber(for: eventOrigin.contract, tokenContract: token.contractAddress, server: token.server, eventName: eventOrigin.eventName)
for: eventOrigin.contract, let events: [EventInstanceValue] = try await self.eventFetcher.fetchEvents(tokenId: tokenId, token: token, eventOrigin: eventOrigin, oldEventBlockNumber: oldEvent?.blockNumber)
tokenContract: token.contractAddress, self.eventsDataStore.addOrUpdate(events: events)
server: token.server, return events
eventName: eventOrigin.eventName)
return eventFetcher
.fetchEvents(tokenId: tokenId, token: token, eventOrigin: eventOrigin, oldEventBlockNumber: oldEvent?.blockNumber)
.handleEvents(receiveOutput: { [eventsDataStore] in eventsDataStore.addOrUpdate(events: $0) })
.replaceError(with: [])
.eraseToAnyPublisher()
} }
} return what
}.flatMap { $0 }
return Publishers.MergeMany(publishers) subject.send(all)
.collect() }
.map { $0.flatMap { $0 } } return subject.eraseToAnyPublisher()
.eraseToAnyPublisher()
} }
} }
} }

@ -98,8 +98,8 @@ final class EventSourceForActivities {
var tokenScriptChanged: AnyPublisher<[Token], Never> { var tokenScriptChanged: AnyPublisher<[Token], Never> {
assetDefinitionStore.bodyChange assetDefinitionStore.bodyChange
.receive(on: queue) .receive(on: queue)
.compactMap { [tokensService] in tokensService.token(for: $0) } .flatMap { [tokensService] address in asFuture { await tokensService.token(for: address) } }.compactMap { $0 }
.compactMap { [weak self] in self?.map(token: $0) } .flatMap { [weak self] token in asFuture { await self?.map(token: token) } }.compactMap { $0 }
.share() .share()
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
@ -113,15 +113,15 @@ final class EventSourceForActivities {
self.tokensService = tokensService self.tokensService = tokensService
} }
private func map(token: Token) -> [Token] { private func map(token: Token) async -> [Token] {
guard let session = sessionsProvider.session(for: token.server) else { return [] } guard let session = sessionsProvider.session(for: token.server) else { return [] }
let xmlHandler = XMLHandler(contract: token.contractAddress, tokenType: token.type, assetDefinitionStore: assetDefinitionStore) let xmlHandler = XMLHandler(contract: token.contractAddress, tokenType: token.type, assetDefinitionStore: assetDefinitionStore)
guard xmlHandler.hasAssetDefinition, let server = xmlHandler.server else { return [] } guard xmlHandler.hasAssetDefinition, let server = xmlHandler.server else { return [] }
switch server { switch server {
case .any: case .any:
let enabledServers = sessionsProvider.activeSessions.map { $0.key } let enabledServers: [RPCServer] = sessionsProvider.activeSessions.map { $0.key }
return enabledServers.compactMap { tokensService.token(for: token.contractAddress, server: $0) } return await enabledServers.asyncCompactMap { server in await tokensService.token(for: token.contractAddress, server: server) }.compactMap { $0 }
case .server(let server): case .server(let server):
return [token] return [token]
} }
@ -272,8 +272,11 @@ final class EventSourceForActivities {
cancellable = Publishers.Merge3(initial, force, timedOrWaitForCurrent) cancellable = Publishers.Merge3(initial, force, timedOrWaitForCurrent)
.receive(on: DispatchQueue.global()) .receive(on: DispatchQueue.global())
.flatMap { [eventsFetcher] in eventsFetcher.fetchEvents(token: $0.token) } .sink { [eventsFetcher] request in
.sink { _ in } Task { @MainActor in
await eventsFetcher.fetchEvents(token: request.token)
}
}
} }
func send(request: FetchRequest) { func send(request: FetchRequest) {
@ -309,26 +312,14 @@ final class EventSourceForActivities {
return xmlHandler.activityCards return xmlHandler.activityCards
} }
func fetchEvents(token: Token) -> EventForActivityPublisher { func fetchEvents(token: Token) async {
let publishers = getActivityCards(token: token) let cards = getActivityCards(token: token)
.map { [eventFetcher] card -> EventForActivityPublisher in for card in cards {
let eventOrigin = card.eventOrigin let eventOrigin = card.eventOrigin
let oldEvent = eventsDataStore.getLastMatchingEventSortedByBlockNumber( let oldEvent = await eventsDataStore.getLastMatchingEventSortedByBlockNumber(for: eventOrigin.contract, tokenContract: token.contractAddress, server: token.server, eventName: eventOrigin.eventName)
for: eventOrigin.contract, let events = (try? await eventFetcher.fetchEvents(token: token, card: card, oldEventBlockNumber: oldEvent?.blockNumber)) ?? []
tokenContract: token.contractAddress, eventsDataStore.addOrUpdate(events: events)
server: token.server, }
eventName: eventOrigin.eventName)
return eventFetcher.fetchEvents(token: token, card: card, oldEventBlockNumber: oldEvent?.blockNumber)
.handleEvents(receiveOutput: { [eventsDataStore] in eventsDataStore.addOrUpdate(events: $0) })
.replaceError(with: [])
.eraseToAnyPublisher()
}
return Publishers.MergeMany(publishers)
.collect()
.map { $0.flatMap { $0 } }
.eraseToAnyPublisher()
} }
} }
} }

@ -22,9 +22,13 @@ public class FetchTokenScriptFiles {
public func start() { public func start() {
sessionsProvider.sessions sessionsProvider.sessions
.compactMap { $0.keys } .map { $0.keys }
.receive(on: queue) .receive(on: queue)
.map { [tokensDataStore] in tokensDataStore.tokens(for: Array($0)) } .flatMap { [tokensDataStore] servers in
asFuture {
await tokensDataStore.tokens(for: Array(servers))
}
}
.map { tokens in .map { tokens in
return tokens.filter { return tokens.filter {
switch $0.type { switch $0.type {

@ -6,51 +6,55 @@ import Combine
public protocol EventsActivityDataStoreProtocol { public protocol EventsActivityDataStoreProtocol {
func recentEventsChangeset(servers: [RPCServer]) -> AnyPublisher<ChangeSet<[EventActivityInstance]>, Never> func recentEventsChangeset(servers: [RPCServer]) -> AnyPublisher<ChangeSet<[EventActivityInstance]>, Never>
func getRecentEventsSortedByBlockNumber(for contract: AlphaWallet.Address, server: RPCServer, eventName: String, interpolatedFilter: String) async -> [EventActivityInstance]
func getRecentEventsSortedByBlockNumber(for contract: AlphaWallet.Address, server: RPCServer, eventName: String, interpolatedFilter: String) -> [EventActivityInstance] func getLastMatchingEventSortedByBlockNumber(for contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String) async -> EventActivityInstance?
func getLastMatchingEventSortedByBlockNumber(for contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String) -> EventActivityInstance?
func addOrUpdate(events: [EventActivityInstance]) func addOrUpdate(events: [EventActivityInstance])
} }
public class EventsActivityDataStore: EventsActivityDataStoreProtocol { public class EventsActivityDataStore: EventsActivityDataStoreProtocol {
private let store: RealmStore private let store: RealmStore
private var cancellables = Set<AnyCancellable>()
public init(store: RealmStore) { public init(store: RealmStore) {
self.store = store self.store = store
} }
public func recentEventsChangeset(servers: [RPCServer]) -> AnyPublisher<ChangeSet<[EventActivityInstance]>, Never> { public func recentEventsChangeset(servers: [RPCServer]) -> AnyPublisher<ChangeSet<[EventActivityInstance]>, Never> {
var publisher: AnyPublisher<ChangeSet<[EventActivityInstance]>, Never>! let publisher = PassthroughSubject<ChangeSet<[EventActivityInstance]>, Never>()
Task {
store.performSync { realm in //Let the app start up and load Wallet tab quickly
publisher = realm.objects(EventActivity.self) try await Task.sleep(nanoseconds: 3_000_000_000)
.filter(EventsActivityDataStore.functional.chainIdPredicate(servers: servers)) await store.performSync { realm in
.sorted(byKeyPath: "date", ascending: false) realm.objects(EventActivity.self)
.changesetPublisher .filter(EventsActivityDataStore.functional.chainIdPredicate(servers: servers))
.freeze() .sorted(byKeyPath: "date", ascending: false)
.receive(on: DispatchQueue.global()) .changesetPublisher
.map { change in .freeze()
switch change { .receive(on: DispatchQueue.global())
case .initial(let eventActivities): .map { change in
return .initial(Array(eventActivities.map { EventActivityInstance(event: $0) })) switch change {
case .update(let eventActivities, let deletions, let insertions, let modifications): case .initial(let eventActivities):
return .update(Array(eventActivities.map { EventActivityInstance(event: $0) }), deletions: deletions, insertions: insertions, modifications: modifications) return .initial(Array(eventActivities.map { EventActivityInstance(event: $0) }))
case .error(let error): case .update(let eventActivities, let deletions, let insertions, let modifications):
return .error(error) return .update(Array(eventActivities.map { EventActivityInstance(event: $0) }), deletions: deletions, insertions: insertions, modifications: modifications)
} case .error(let error):
} return .error(error)
.eraseToAnyPublisher() }
}.sink { value in
publisher.send(value)
}.store(in: &self.cancellables)
}
} }
return publisher return publisher.eraseToAnyPublisher()
} }
public func getRecentEventsSortedByBlockNumber(for contract: AlphaWallet.Address, server: RPCServer, eventName: String, interpolatedFilter: String) -> [EventActivityInstance] { public func getRecentEventsSortedByBlockNumber(for contract: AlphaWallet.Address, server: RPCServer, eventName: String, interpolatedFilter: String) async -> [EventActivityInstance] {
let predicate = EventsActivityDataStore let predicate = EventsActivityDataStore
.functional .functional
.matchingEventPredicate(for: contract, server: server, eventName: eventName, interpolatedFilter: interpolatedFilter) .matchingEventPredicate(for: contract, server: server, eventName: eventName, interpolatedFilter: interpolatedFilter)
var eventActivities: [EventActivityInstance] = [] var eventActivities: [EventActivityInstance] = []
store.performSync { realm in await store.perform { realm in
eventActivities = realm.objects(EventActivity.self) eventActivities = realm.objects(EventActivity.self)
.filter(predicate) .filter(predicate)
.sorted(byKeyPath: "blockNumber", ascending: false) .sorted(byKeyPath: "blockNumber", ascending: false)
@ -60,13 +64,13 @@ public class EventsActivityDataStore: EventsActivityDataStoreProtocol {
return eventActivities return eventActivities
} }
public func getLastMatchingEventSortedByBlockNumber(for contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String) -> EventActivityInstance? { public func getLastMatchingEventSortedByBlockNumber(for contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String) async -> EventActivityInstance? {
let predicate = EventsActivityDataStore let predicate = EventsActivityDataStore
.functional .functional
.matchingEventPredicate(for: contract, tokenContract: tokenContract, server: server, eventName: eventName) .matchingEventPredicate(for: contract, tokenContract: tokenContract, server: server, eventName: eventName)
var eventActivity: EventActivityInstance? var eventActivity: EventActivityInstance?
store.performSync { realm in await store.perform { realm in
eventActivity = realm.objects(EventActivity.self) eventActivity = realm.objects(EventActivity.self)
.filter(predicate) .filter(predicate)
.sorted(byKeyPath: "blockNumber") .sorted(byKeyPath: "blockNumber")
@ -79,11 +83,13 @@ public class EventsActivityDataStore: EventsActivityDataStoreProtocol {
public func addOrUpdate(events: [EventActivityInstance]) { public func addOrUpdate(events: [EventActivityInstance]) {
guard !events.isEmpty else { return } guard !events.isEmpty else { return }
let eventsToSave = events.map { EventActivity(value: $0) } let eventsToSave = events.map { EventActivity(value: $0) }
store.performSync { realm in Task {
try? realm.safeWrite { await store.perform { realm in
realm.add(eventsToSave, update: .all) try? realm.safeWrite {
realm.add(eventsToSave, update: .all)
}
} }
} }
} }

@ -6,27 +6,28 @@ import AlphaWalletTokenScript
import RealmSwift import RealmSwift
public protocol NonActivityEventsDataStore { public protocol NonActivityEventsDataStore {
func getLastMatchingEventSortedByBlockNumber(for contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String) -> EventInstanceValue? func getLastMatchingEventSortedByBlockNumber(for contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String) async -> EventInstanceValue?
func addOrUpdate(events: [EventInstanceValue]) func addOrUpdate(events: [EventInstanceValue])
func deleteEvents(for contract: AlphaWallet.Address) func deleteEvents(for contract: AlphaWallet.Address)
func getMatchingEvent(for contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String, filterName: String, filterValue: String) -> EventInstanceValue? func getMatchingEvent(for contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String, filterName: String, filterValue: String) async -> EventInstanceValue?
func recentEventsChangeset(for contract: AlphaWallet.Address) -> AnyPublisher<ChangeSet<[EventInstanceValue]>, Never> func recentEventsChangeset(for contract: AlphaWallet.Address) -> AnyPublisher<ChangeSet<[EventInstanceValue]>, Never>
} }
open class NonActivityMultiChainEventsDataStore: NonActivityEventsDataStore { open class NonActivityMultiChainEventsDataStore: NonActivityEventsDataStore {
private let store: RealmStore private let store: RealmStore
private var cancellables = Set<AnyCancellable>()
public init(store: RealmStore) { public init(store: RealmStore) {
self.store = store self.store = store
} }
public func getMatchingEvent(for contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String, filterName: String, filterValue: String) -> EventInstanceValue? { public func getMatchingEvent(for contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String, filterName: String, filterValue: String) async -> EventInstanceValue? {
let predicate = NonActivityMultiChainEventsDataStore let predicate = NonActivityMultiChainEventsDataStore
.functional .functional
.matchingEventPredicate(for: contract, tokenContract: tokenContract, server: server, eventName: eventName, filterName: filterName, filterValue: filterValue) .matchingEventPredicate(for: contract, tokenContract: tokenContract, server: server, eventName: eventName, filterName: filterName, filterValue: filterValue)
var event: EventInstanceValue? var event: EventInstanceValue?
store.performSync { realm in await store.perform { realm in
event = realm.objects(EventInstance.self) event = realm.objects(EventInstance.self)
.filter(predicate) .filter(predicate)
.first .first
@ -36,46 +37,51 @@ open class NonActivityMultiChainEventsDataStore: NonActivityEventsDataStore {
} }
public func deleteEvents(for contract: AlphaWallet.Address) { public func deleteEvents(for contract: AlphaWallet.Address) {
store.performSync { realm in Task {
try? realm.safeWrite { await store.perform { realm in
let events = realm.objects(EventInstance.self) try? realm.safeWrite {
.filter("tokenContract = '\(contract.eip55String)'") let events = realm.objects(EventInstance.self)
realm.delete(events) .filter("tokenContract = '\(contract.eip55String)'")
realm.delete(events)
}
} }
} }
} }
public func recentEventsChangeset(for contract: AlphaWallet.Address) -> AnyPublisher<ChangeSet<[EventInstanceValue]>, Never> { public func recentEventsChangeset(for contract: AlphaWallet.Address) -> AnyPublisher<ChangeSet<[EventInstanceValue]>, Never> {
var publisher: AnyPublisher<ChangeSet<[EventInstanceValue]>, Never>! let publisher = PassthroughSubject<ChangeSet<[EventInstanceValue]>, Never>()
store.performSync { realm in Task {
publisher = realm.objects(EventInstance.self) await store.performSync { realm in
.filter("tokenContract = '\(contract.eip55String)'") realm.objects(EventInstance.self)
.changesetPublisher .filter("tokenContract = '\(contract.eip55String)'")
.freeze() .changesetPublisher
.receive(on: DispatchQueue.global()) .freeze()
.map { change in .receive(on: DispatchQueue.global())
switch change { .map { change in
case .initial(let eventActivities): switch change {
return .initial(Array(eventActivities.map { EventInstanceValue(event: $0) })) case .initial(let eventActivities):
case .update(let eventActivities, let deletions, let insertions, let modifications): return .initial(Array(eventActivities.map { EventInstanceValue(event: $0) }))
return .update(Array(eventActivities.map { EventInstanceValue(event: $0) }), deletions: deletions, insertions: insertions, modifications: modifications) case .update(let eventActivities, let deletions, let insertions, let modifications):
case .error(let error): return .update(Array(eventActivities.map { EventInstanceValue(event: $0) }), deletions: deletions, insertions: insertions, modifications: modifications)
return .error(error) case .error(let error):
} return .error(error)
} }
.eraseToAnyPublisher() }.sink { value in
publisher.send(value)
}.store(in: &self.cancellables)
}
} }
return publisher return publisher.eraseToAnyPublisher()
} }
public func getLastMatchingEventSortedByBlockNumber(for contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String) -> EventInstanceValue? { public func getLastMatchingEventSortedByBlockNumber(for contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String) async -> EventInstanceValue? {
let predicate = NonActivityMultiChainEventsDataStore let predicate = NonActivityMultiChainEventsDataStore
.functional .functional
.matchingEventPredicate(for: contract, tokenContract: tokenContract, server: server, eventName: eventName) .matchingEventPredicate(for: contract, tokenContract: tokenContract, server: server, eventName: eventName)
var event: EventInstanceValue? var event: EventInstanceValue?
store.performSync { realm in await store.perform { realm in
event = realm.objects(EventInstance.self) event = realm.objects(EventInstance.self)
.filter(predicate) .filter(predicate)
.sorted(byKeyPath: "blockNumber") .sorted(byKeyPath: "blockNumber")
@ -90,9 +96,11 @@ open class NonActivityMultiChainEventsDataStore: NonActivityEventsDataStore {
guard !events.isEmpty else { return } guard !events.isEmpty else { return }
let eventsToSave = events.map { EventInstance(event: $0) } let eventsToSave = events.map { EventInstance(event: $0) }
store.performSync { realm in Task {
try? realm.safeWrite { await store.perform { realm in
realm.add(eventsToSave, update: .all) try? realm.safeWrite {
realm.add(eventsToSave, update: .all)
}
} }
} }
} }

@ -22,6 +22,14 @@ public class AlphaWalletTokensService: TokensService {
private let fetchTokenScriptFiles: FetchTokenScriptFiles private let fetchTokenScriptFiles: FetchTokenScriptFiles
private lazy var tokenRepairService = TokenRepairService(tokensDataStore: tokensDataStore, sessionsProvider: sessionsProvider) private lazy var tokenRepairService = TokenRepairService(tokensDataStore: tokensDataStore, sessionsProvider: sessionsProvider)
public var tokens: [Token] {
get async {
let tokenProviders: ServerDictionary<TokenSourceProvider> = providers.value
let all: [Token] = await tokenProviders.asyncFlatMap { await $0.value.getTokens() }
return AlphaWalletTokensService.filterAwaySpuriousTokens(all)
}
}
public lazy var tokensPublisher: AnyPublisher<[Token], Never> = { public lazy var tokensPublisher: AnyPublisher<[Token], Never> = {
providers.map { $0.values } providers.map { $0.values }
.flatMapLatest { $0.map { $0.tokensPublisher }.combineLatest() } .flatMapLatest { $0.map { $0.tokensPublisher }.combineLatest() }
@ -37,10 +45,6 @@ public class AlphaWalletTokensService: TokensService {
tokensDataStore.tokensChangesetPublisher(for: servers, predicate: nil) tokensDataStore.tokensChangesetPublisher(for: servers, predicate: nil)
} }
public var tokens: [Token] {
AlphaWalletTokensService.filterAwaySpuriousTokens(providers.value.flatMap { $0.value.tokens })
}
public lazy var addedTokensPublisher: AnyPublisher<[Token], Never> = { public lazy var addedTokensPublisher: AnyPublisher<[Token], Never> = {
providers.map { $0.values } providers.map { $0.values }
.flatMapLatest { $0.map { $0.addedTokensPublisher }.merge() } .flatMapLatest { $0.map { $0.addedTokensPublisher }.merge() }
@ -73,21 +77,23 @@ public class AlphaWalletTokensService: TokensService {
sessionsProvider: sessionsProvider) sessionsProvider: sessionsProvider)
} }
public func tokens(for servers: [RPCServer]) -> [Token] { public func tokens(for servers: [RPCServer]) async -> [Token] {
return tokensDataStore.tokens(for: servers) return await tokensDataStore.tokens(for: servers)
} }
public func mark(token: TokenIdentifiable, isHidden: Bool) { public func mark(token: TokenIdentifiable, isHidden: Bool) {
let primaryKey = TokenObject.generatePrimaryKey(fromContract: token.contractAddress, server: token.server) let primaryKey = TokenObject.generatePrimaryKey(fromContract: token.contractAddress, server: token.server)
tokensDataStore.updateToken(primaryKey: primaryKey, action: .isHidden(isHidden)) Task {
await tokensDataStore.updateToken(primaryKey: primaryKey, action: .isHidden(isHidden))
}
} }
public func token(for contract: AlphaWallet.Address) -> Token? { public func token(for contract: AlphaWallet.Address) async -> Token? {
return tokensDataStore.token(for: contract) return await tokensDataStore.token(for: contract)
} }
public func token(for contract: AlphaWallet.Address, server: RPCServer) -> Token? { public func token(for contract: AlphaWallet.Address, server: RPCServer) async -> Token? {
return tokensDataStore.token(for: contract, server: server) return await tokensDataStore.token(for: contract, server: server)
} }
public func refresh() { public func refresh() {
@ -144,12 +150,12 @@ public class AlphaWalletTokensService: TokensService {
stop() stop()
} }
public func addOrUpdate(with actions: [AddOrUpdateTokenAction]) -> [Token] { public func addOrUpdate(with actions: [AddOrUpdateTokenAction]) async -> [Token] {
tokensDataStore.addOrUpdate(with: actions) await tokensDataStore.addOrUpdate(with: actions)
} }
public func updateToken(primaryKey: String, action: TokenFieldUpdate) -> Bool? { public func updateToken(primaryKey: String, action: TokenFieldUpdate) async -> Bool? {
tokensDataStore.updateToken(primaryKey: primaryKey, action: action) await tokensDataStore.updateToken(primaryKey: primaryKey, action: action)
} }
public func refreshBalance(updatePolicy: TokenBalanceFetcher.RefreshBalancePolicy) { public func refreshBalance(updatePolicy: TokenBalanceFetcher.RefreshBalancePolicy) {
@ -159,7 +165,10 @@ public class AlphaWalletTokensService: TokensService {
provider.refreshBalance(for: [token]) provider.refreshBalance(for: [token])
case .all: case .all:
for provider in providers.value.values { for provider in providers.value.values {
provider.refreshBalance(for: provider.tokens) Task { @MainActor in
let tokens = await provider.getTokens()
provider.refreshBalance(for: tokens)
}
} }
case .tokens(let tokens): case .tokens(let tokens):
for token in tokens { for token in tokens {
@ -182,7 +191,9 @@ public class AlphaWalletTokensService: TokensService {
public func update(token: TokenIdentifiable, value: TokenFieldUpdate) { public func update(token: TokenIdentifiable, value: TokenFieldUpdate) {
let primaryKey = TokenObject.generatePrimaryKey(fromContract: token.contractAddress, server: token.server) let primaryKey = TokenObject.generatePrimaryKey(fromContract: token.contractAddress, server: token.server)
tokensDataStore.updateToken(primaryKey: primaryKey, action: value) Task {
await tokensDataStore.updateToken(primaryKey: primaryKey, action: value)
}
} }
//Remove tokens that look unwanted in the Wallet tab //Remove tokens that look unwanted in the Wallet tab
@ -200,15 +211,21 @@ public class AlphaWalletTokensService: TokensService {
extension AlphaWalletTokensService { extension AlphaWalletTokensService {
public func setBalanceTestsOnly(balance: Balance, for token: Token) { public func setBalanceTestsOnly(balance: Balance, for token: Token) {
tokensDataStore.updateToken(addressAndRpcServer: token.addressAndRPCServer, action: .value(balance.value)) Task {
await tokensDataStore.updateToken(addressAndRpcServer: token.addressAndRPCServer, action: .value(balance.value))
}
} }
public func setNftBalanceTestsOnly(_ value: NonFungibleBalance, for token: Token) { public func setNftBalanceTestsOnly(_ value: NonFungibleBalance, for token: Token) {
tokensDataStore.updateToken(addressAndRpcServer: token.addressAndRPCServer, action: .nonFungibleBalance(value)) Task {
await tokensDataStore.updateToken(addressAndRpcServer: token.addressAndRPCServer, action: .nonFungibleBalance(value))
}
} }
public func addOrUpdateTokenTestsOnly(token: Token) { public func addOrUpdateTokenTestsOnly(token: Token) {
tokensDataStore.addOrUpdate(with: [.init(token)]) Task {
await tokensDataStore.addOrUpdate(with: [.init(token)])
}
} }
public func deleteTokenTestsOnly(token: Token) { public func deleteTokenTestsOnly(token: Token) {
@ -217,20 +234,19 @@ extension AlphaWalletTokensService {
} }
extension AlphaWalletTokensService { extension AlphaWalletTokensService {
public func alreadyAddedContracts(for server: RPCServer) async -> [AlphaWallet.Address] {
public func alreadyAddedContracts(for server: RPCServer) -> [AlphaWallet.Address] { await tokensDataStore.tokens(for: [server]).map { $0.contractAddress }
tokensDataStore.tokens(for: [server]).map { $0.contractAddress }
} }
public func deletedContracts(for server: RPCServer) -> [AlphaWallet.Address] { public func deletedContracts(for server: RPCServer) async -> [AlphaWallet.Address] {
tokensDataStore.deletedContracts(forServer: server).map { $0.address } await tokensDataStore.deletedContracts(forServer: server).map { $0.address }
} }
public func hiddenContracts(for server: RPCServer) -> [AlphaWallet.Address] { public func hiddenContracts(for server: RPCServer) async -> [AlphaWallet.Address] {
tokensDataStore.hiddenContracts(forServer: server).map { $0.address } await tokensDataStore.hiddenContracts(forServer: server).map { $0.address }
} }
public func delegateContracts(for server: RPCServer) -> [AlphaWallet.Address] { public func delegateContracts(for server: RPCServer) async -> [AlphaWallet.Address] {
tokensDataStore.delegateContracts(forServer: server).map { $0.address } await tokensDataStore.delegateContracts(forServer: server).map { $0.address }
} }
} }

@ -51,11 +51,13 @@ public class ClientSideTokenSourceProvider: TokenSourceProvider {
.eraseToAnyPublisher() .eraseToAnyPublisher()
}() }()
public var tokens: [Token] { tokensDataStore.tokens(for: [session.server]) }
public var tokensPublisher: AnyPublisher<[Token], Never> { public var tokensPublisher: AnyPublisher<[Token], Never> {
let initialOrForceSnapshot = Publishers.Merge(Just<Void>(()), refreshSubject) let initialOrForceSnapshot = Publishers.Merge(Just<Void>(()), refreshSubject)
.map { [tokensDataStore, session] _ in tokensDataStore.tokens(for: [session.server]) } .flatMap { [tokensDataStore, session] _ in
asFuture {
await tokensDataStore.tokens(for: [session.server])
}
}
.eraseToAnyPublisher() .eraseToAnyPublisher()
let addedOrChanged = tokensDataStore.enabledTokensPublisher(for: [session.server]) let addedOrChanged = tokensDataStore.enabledTokensPublisher(for: [session.server])
@ -82,7 +84,11 @@ public class ClientSideTokenSourceProvider: TokenSourceProvider {
tokensAutodetector tokensAutodetector
.detectedTokensOrContracts .detectedTokensOrContracts
.map { $0.map { AddOrUpdateTokenAction($0) } } .map { $0.map { AddOrUpdateTokenAction($0) } }
.sink { [tokensDataStore] in tokensDataStore.addOrUpdate(with: $0) } .sink { [tokensDataStore] action in
Task {
await tokensDataStore.addOrUpdate(with: action)
}
}
.store(in: &cancelable) .store(in: &cancelable)
//NOTE: disabled as delating instances from db caused crash //NOTE: disabled as delating instances from db caused crash
@ -107,11 +113,17 @@ public class ClientSideTokenSourceProvider: TokenSourceProvider {
public func refreshBalance(for tokens: [Token]) { public func refreshBalance(for tokens: [Token]) {
balanceFetcher.refreshBalance(for: tokens) balanceFetcher.refreshBalance(for: tokens)
} }
public func getTokens() async -> [Token] {
await tokensDataStore.tokens(for: [session.server])
}
} }
extension ClientSideTokenSourceProvider: TokenBalanceFetcherDelegate { extension ClientSideTokenSourceProvider: TokenBalanceFetcherDelegate {
public func didUpdateBalance(value actions: [AddOrUpdateTokenAction], in fetcher: TokenBalanceFetcher) { public func didUpdateBalance(value actions: [AddOrUpdateTokenAction], in fetcher: TokenBalanceFetcher) {
crashlytics.logLargeNftJsonFiles(for: actions, fileSizeThreshold: 10) crashlytics.logLargeNftJsonFiles(for: actions, fileSizeThreshold: 10)
tokensDataStore.addOrUpdate(with: actions) Task {
await tokensDataStore.addOrUpdate(with: actions)
}
} }
} }

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save