Speed up app launch #2374

pull/2451/head
Vladyslav shepitko 4 years ago committed by Hwee-Boon Yar
parent 875a48e7bd
commit 9af000976e
  1. 4
      AlphaWallet.xcodeproj/project.pbxproj
  2. 22
      AlphaWallet.xcworkspace/xcshareddata/xcschemes/AlphaWalletTests.xcscheme
  3. 95
      AlphaWallet/Activities/Coordinators/ActivitiesCoordinator.swift
  4. 11
      AlphaWallet/Activities/ViewControllers/ActivitiesViewController.swift
  5. 12
      AlphaWallet/Activities/ViewModels/ActivitiesViewModel.swift
  6. 8
      AlphaWallet/Core/Initializers/MigrationInitializerForOneChainPerDatabase.swift
  7. 32
      AlphaWallet/Core/Types/AlphaWalletAddress.swift
  8. 9
      AlphaWallet/Core/Types/AlphaWalletAddressExtension.swift
  9. 24
      AlphaWallet/EtherClient/TrustClient/Models/RawTransaction.swift
  10. 14
      AlphaWallet/InCoordinator.swift
  11. 2
      AlphaWallet/Settings/Types/RPCServers.swift
  12. 67
      AlphaWallet/TokenScriptClient/Coordinators/EventSourceCoordinator.swift
  13. 129
      AlphaWallet/TokenScriptClient/Coordinators/EventSourceCoordinatorForActivities.swift
  14. 23
      AlphaWallet/TokenScriptClient/Models/Activity.swift
  15. 71
      AlphaWallet/TokenScriptClient/Models/ActivityOrTransaction.swift
  16. 6
      AlphaWallet/TokenScriptClient/Models/AssetAttributeValues.swift
  17. 45
      AlphaWallet/TokenScriptClient/Models/EventsActivityDataStore.swift
  18. 52
      AlphaWallet/TokenScriptClient/Models/EventsDataStore.swift
  19. 51
      AlphaWallet/TokenScriptClient/Models/XMLHandler.swift
  20. 25
      AlphaWallet/Tokens/Coordinators/GetBlockTimestampCoordinator.swift
  21. 112
      AlphaWallet/Tokens/Coordinators/GetContractInteractions.swift
  22. 126
      AlphaWallet/Tokens/Coordinators/GetIsERC721ContractCoordinator.swift
  23. 6
      AlphaWallet/Tokens/Coordinators/SingleChainTokenCoordinator.swift
  24. 4
      AlphaWallet/Tokens/Coordinators/TokensCoordinator.swift
  25. 102
      AlphaWallet/Tokens/Helpers/CallSmartContractFunction.swift
  26. 85
      AlphaWallet/Tokens/Types/EventActivity.swift
  27. 24
      AlphaWallet/Tokens/Types/EventInstance.swift
  28. 62
      AlphaWallet/Tokens/Types/EventInstanceValue.swift
  29. 14
      AlphaWallet/Tokens/Types/TokenCollection.swift
  30. 32
      AlphaWallet/Tokens/Types/TokenObject.swift
  31. 23
      AlphaWallet/Tokens/Types/TokensDataStore.swift
  32. 13
      AlphaWallet/Tokens/Types/TransactionCollection.swift
  33. 2
      AlphaWallet/Tokens/ViewControllers/TokenViewController.swift
  34. 4
      AlphaWallet/Tokens/ViewModels/TokenViewControllerTransactionCellViewModel.swift
  35. 27
      AlphaWallet/Tokens/ViewModels/TokenViewControllerViewModel.swift
  36. 5
      AlphaWallet/Tokens/Views/OpenSea/OpenSeaNonFungibleTokenViewCell.swift
  37. 1
      AlphaWallet/Transactions/Coordinators/SingleChainTransactionDataCoordinator.swift
  38. 251
      AlphaWallet/Transactions/Coordinators/SingleChainTransactionEtherscanDataCoordinator.swift
  39. 2
      AlphaWallet/Transactions/Coordinators/TransactionCoordinator.swift
  40. 22
      AlphaWallet/Transactions/Coordinators/TransactionDataCoordinator.swift
  41. 122
      AlphaWallet/Transactions/Storage/Transaction.swift
  42. 130
      AlphaWallet/Transactions/Storage/TransactionsStorage.swift
  43. 84
      AlphaWallet/Transactions/Types/LocalizedOperationObject.swift
  44. 8
      AlphaWallet/Transactions/ViewControllers/TransactionViewController.swift
  45. 42
      AlphaWallet/Transactions/ViewControllers/TransactionsViewController.swift
  46. 12
      AlphaWallet/Transactions/ViewModels/TransactionRow.swift
  47. 2
      AlphaWallet/Transactions/ViewModels/TransactionRowCellViewModel.swift
  48. 19
      AlphaWallet/Transactions/ViewModels/TransactionsViewModel.swift
  49. 28
      AlphaWallet/UI/TokenObject+UI.swift
  50. 36
      AlphaWalletTests/Factories/Transaction.swift
  51. 13
      AlphaWalletTests/TokenScriptClient/FakeEventsDataStore.swift

@ -722,6 +722,7 @@
878EE952255BEC20000210DE /* ItemType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878EE950255BEC20000210DE /* ItemType.swift */; };
878EE954255BFFB9000210DE /* FeedbackGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878EE953255BFFB9000210DE /* FeedbackGenerator.swift */; };
8797362524E6C20C0042BBCC /* TransactionConfirmationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8797362424E6C20C0042BBCC /* TransactionConfirmationCoordinator.swift */; };
87A0C93225AEF1E400E73F60 /* EventInstanceValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87A0C93125AEF1E400E73F60 /* EventInstanceValue.swift */; };
87A3020924BEE243000DF32E /* TransactionInProgressViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87A3020824BEE243000DF32E /* TransactionInProgressViewController.swift */; };
87A3020B24BF04B6000DF32E /* TransactionInProgressViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87A3020A24BF04B6000DF32E /* TransactionInProgressViewModel.swift */; };
87A3022724C02212000DF32E /* TransactionConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87A3022624C02212000DF32E /* TransactionConfirmationViewController.swift */; };
@ -1575,6 +1576,7 @@
878EE950255BEC20000210DE /* ItemType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemType.swift; sourceTree = "<group>"; };
878EE953255BFFB9000210DE /* FeedbackGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackGenerator.swift; sourceTree = "<group>"; };
8797362424E6C20C0042BBCC /* TransactionConfirmationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionConfirmationCoordinator.swift; sourceTree = "<group>"; };
87A0C93125AEF1E400E73F60 /* EventInstanceValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventInstanceValue.swift; sourceTree = "<group>"; };
87A3020824BEE243000DF32E /* TransactionInProgressViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionInProgressViewController.swift; sourceTree = "<group>"; };
87A3020A24BF04B6000DF32E /* TransactionInProgressViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionInProgressViewModel.swift; sourceTree = "<group>"; };
87A3022624C02212000DF32E /* TransactionConfirmationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionConfirmationViewController.swift; sourceTree = "<group>"; };
@ -2351,6 +2353,7 @@
5E7C7B43816C35C3FE2EFFBE /* TokenInstanceAction.swift */,
5E7C74822F5F71748184F6C1 /* EventInstance.swift */,
5E7C737CC822F8143DE1FDC0 /* EventActivity.swift */,
87A0C93125AEF1E400E73F60 /* EventInstanceValue.swift */,
);
path = Types;
sourceTree = "<group>";
@ -4559,6 +4562,7 @@
29F114F21FA7966300114A29 /* PrivateKeyRule.swift in Sources */,
29F1C853200363B2003780D8 /* PassphraseViewController.swift in Sources */,
29E2E33A1F7A008C000CF94A /* UIView.swift in Sources */,
87A0C93225AEF1E400E73F60 /* EventInstanceValue.swift in Sources */,
299B5E491FD2C8900051361C /* ConfigureTransaction.swift in Sources */,
77872D25202505B70032D687 /* EnterPasswordViewController.swift in Sources */,
87E2554C24F52E0600F025F7 /* TextFieldTableViewCell.swift in Sources */,

@ -27,6 +27,15 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2912CCF41F6A830700C6CBE3"
BuildableName = "AlphaWallet.app"
BlueprintName = "AlphaWallet"
ReferencedContainer = "container:AlphaWallet.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO">
@ -39,17 +48,6 @@
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2912CCF41F6A830700C6CBE3"
BuildableName = "AlphaWallet.app"
BlueprintName = "AlphaWallet"
ReferencedContainer = "container:AlphaWallet.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@ -83,8 +81,6 @@
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
<AdditionalOptions>
</AdditionalOptions>
<LocationScenarioReference
identifier = "com.apple.dt.IDEFoundation.CurrentLocationScenarioIdentifier"
referenceType = "1">

@ -3,7 +3,7 @@
import UIKit
protocol ActivitiesCoordinatorDelegate: class {
func didPressTransaction(transaction: Transaction, in viewController: ActivitiesViewController)
func didPressTransaction(transaction: TransactionInstance, in viewController: ActivitiesViewController)
func show(tokenObject: TokenObject, fromCoordinator coordinator: ActivitiesCoordinator)
func show(transactionWithId transactionId: String, server: RPCServer, inViewController viewController: UIViewController, fromCoordinator coordinator: ActivitiesCoordinator)
func didPressViewContractWebPage(forContract contract: AlphaWallet.Address, server: RPCServer, fromCoordinator coordinator: ActivitiesCoordinator, inViewController viewController: UIViewController)
@ -20,8 +20,9 @@ class ActivitiesCoordinator: Coordinator {
//Dictionary for lookup. Using `.firstIndex` too many times is too slow (60s for 10k events)
private var activitiesIndexLookup: [Int: (index: Int, activity: Activity)] = .init()
private var activities: [Activity] = .init()
private var transactions: [Transaction] = .init()
private var tokensAndTokenHolders: [AlphaWallet.Address: (tokenObject: TokenObject, tokenHolders: [TokenHolder])] = .init()
private var transactions: [TransactionInstance] = .init()
private var tokensAndTokenHolders: [AlphaWallet.Address: (tokenObject: Activity.AssignedToken, tokenHolders: [TokenHolder])] = .init()
weak private var activityViewController: ActivityViewController?
private var rateLimitedUpdater: RateLimiter?
private var rateLimitedViewControllerReloader: RateLimiter?
@ -45,6 +46,7 @@ class ActivitiesCoordinator: Coordinator {
let navigationController: UINavigationController
var coordinators: [Coordinator] = []
private let queue = DispatchQueue(label: "com.activities.updateQueue")
init(
config: Config,
@ -91,7 +93,7 @@ class ActivitiesCoordinator: Coordinator {
}
@objc func dismiss() {
navigationController.dismiss(animated: true, completion: nil)
navigationController.dismiss(animated: true)
}
func stop() {
@ -104,7 +106,11 @@ class ActivitiesCoordinator: Coordinator {
func reload() {
if rateLimitedUpdater == nil {
rateLimitedUpdater = RateLimiter(name: "Fetch activity from events in database", limit: 5, autoRun: true) { [weak self] in
self?.reloadImpl()
guard let strongSelf = self else { return }
strongSelf.queue.async {
strongSelf.reloadImpl()
}
}
} else {
rateLimitedUpdater?.run()
@ -153,13 +159,14 @@ class ActivitiesCoordinator: Coordinator {
}
return contractAndCard
}
let contractsAndCards = contractsAndCardsOptional.flatMap { $0 }
fetchAndRefreshActivities(contractsAndCards: contractsAndCards)
}
private func fetchAndRefreshActivities(contractsAndCards: [(tokenContract: AlphaWallet.Address, server: RPCServer, card: TokenScriptCard, interpolatedFilter: String)]) {
let recentEvents = eventsActivityDataStore.getRecentEvents()
var activitiesAndTokens: [(Activity, TokenObject, TokenHolder)] = .init()
var activitiesAndTokens: [(Activity, Activity.AssignedToken, TokenHolder)] = .init()
for (eachContract, eachServer, card, interpolatedFilter) in contractsAndCards {
let activities = getActivities(recentEvents, forTokenContract: eachContract, server: eachServer, card: card, interpolatedFilter: interpolatedFilter)
activitiesAndTokens.append(contentsOf: activities)
@ -175,37 +182,41 @@ class ActivitiesCoordinator: Coordinator {
}
}
private func getActivities(_ allActivities: [EventActivity], forTokenContract contract: AlphaWallet.Address, server: RPCServer, card: TokenScriptCard, interpolatedFilter: String) -> [(Activity, TokenObject, TokenHolder)] {
let events = allActivities.filter { $0.contract == card.eventOrigin.contract.eip55String
private func getActivities(_ allActivities: [EventActivity], forTokenContract contract: AlphaWallet.Address, server: RPCServer, card: TokenScriptCard, interpolatedFilter: String) -> [(Activity, Activity.AssignedToken, TokenHolder)] {
let events = allActivities.filter {
$0.contract == card.eventOrigin.contract.eip55String
&& $0.server == server
&& $0.eventName == card.eventOrigin.eventName
&& $0.filter == interpolatedFilter
}
//Cache tokens lookup for performance
var tokensCache: [AlphaWallet.Address: TokenObject] = .init()
let activitiesForThisCard: [(activity: Activity, tokenObject: TokenObject, tokenHolder: TokenHolder)] = events.compactMap { eachEvent in
let token: TokenObject
var tokensCache: [AlphaWallet.Address: Activity.AssignedToken] = .init()
let activitiesForThisCard: [(activity: Activity, tokenObject: Activity.AssignedToken, tokenHolders: TokenHolder)] = events.compactMap { eachEvent in
let token: Activity.AssignedToken
if let t = tokensCache[contract] {
token = t
} else {
let tokensDatastore = tokensStorages[server]
guard let t = tokensDatastore.token(forContract: contract) else { return nil }
tokensCache[contract] = t
token = t
guard let t = tokensDatastore.tokenThreadSafe(forContract: contract) else { return nil }
let tt = Activity.AssignedToken(tokenObject: t)
tokensCache[contract] = tt
token = tt
}
let implicitAttributes = generateImplicitAttributesForToken(forContract: contract, server: server, symbol: token.symbol)
let tokenAttributes = implicitAttributes
var cardAttributes = generateImplicitAttributesForCard(forContract: contract, server: server, event: eachEvent)
cardAttributes.merge(eachEvent.data) { _, new in new }
for parameter in card.eventOrigin.parameters {
guard let originalValue = cardAttributes[parameter.name] else { continue }
guard let type = SolidityType(rawValue: parameter.type) else { continue }
let translatedValue = type.coerce(value: originalValue)
cardAttributes[parameter.name] = translatedValue
}
let tokenObject: TokenObject
let tokenObject: Activity.AssignedToken
let tokenHolders: [TokenHolder]
if let (o, h) = tokensAndTokenHolders[contract] {
tokenObject = o
@ -213,13 +224,22 @@ class ActivitiesCoordinator: Coordinator {
} else {
tokenObject = token
if tokenObject.contractAddress.sameContract(as: Constants.nativeCryptoAddressInDatabase) {
tokenHolders = [TokenHolder(tokens: [Token(tokenIdOrEvent: .tokenId(tokenId: .init(1)), tokenType: .nativeCryptocurrency, index: 0, name: "", symbol: "", status: .available, values: .init())], contractAddress: tokenObject.contractAddress, hasAssetDefinition: true)]
let token = Token(tokenIdOrEvent: .tokenId(tokenId: .init(1)), tokenType: .nativeCryptocurrency, index: 0, name: "", symbol: "", status: .available, values: .init())
tokenHolders = [TokenHolder(tokens: [token], contractAddress: tokenObject.contractAddress, hasAssetDefinition: true)]
} else {
tokenHolders = TokenAdaptor(token: tokenObject, assetDefinitionStore: assetDefinitionStore, eventsDataStore: eventsDataStore).getTokenHolders(forWallet: sessions.anyValue.account)
//NOTE: because this can be called from different threads we can use cache here, but we can cache Activity.AssignedToken
let tokensDatastore = tokensStorages[server]
guard let t = tokensDatastore.tokenThreadSafe(forContract: tokenObject.contractAddress) else { return nil }
tokenHolders = TokenAdaptor(token: t, assetDefinitionStore: assetDefinitionStore, eventsDataStore: eventsDataStore).getTokenHolders(forWallet: sessions.anyValue.account)
}
tokensAndTokenHolders[contract] = (tokenObject: tokenObject, tokenHolders: tokenHolders)
}
return (activity: .init(id: Int.random(in: 0..<Int.max), rowType: .standalone, tokenObject: tokenObject, server: eachEvent.server, name: card.name, eventName: eachEvent.eventName, blockNumber: eachEvent.blockNumber, transactionId: eachEvent.transactionId, transactionIndex: eachEvent.transactionIndex, logIndex: eachEvent.logIndex, date: eachEvent.date, values: (token: tokenAttributes, card: cardAttributes), view: card.view, itemView: card.itemView, isBaseCard: card.isBase, state: .completed), tokenObject: tokenObject, tokenHolder: tokenHolders[0])
let activity = Activity(id: Int.random(in: 0..<Int.max), rowType: .standalone, tokenObject: tokenObject, server: eachEvent.server, name: card.name, eventName: eachEvent.eventName, blockNumber: eachEvent.blockNumber, transactionId: eachEvent.transactionId, transactionIndex: eachEvent.transactionIndex, logIndex: eachEvent.logIndex, date: eachEvent.date, values: (token: tokenAttributes, card: cardAttributes), view: card.view, itemView: card.itemView, isBaseCard: card.isBase, state: .completed)
return (activity: activity, tokenObject: tokenObject, tokenHolders: tokenHolders[0])
}
//TODO fix for activities: special fix to filter out the event we don't want - need to doc this and have to handle with TokenScript design
@ -245,7 +265,11 @@ class ActivitiesCoordinator: Coordinator {
if hasLoadedActivitiesTheFirstTime {
if rateLimitedViewControllerReloader == nil {
rateLimitedViewControllerReloader = RateLimiter(name: "Reload activity/transactions in Activity tab", limit: 5, autoRun: true) { [weak self] in
self?.reloadViewControllerImpl()
guard let strongSelf = self else { return }
strongSelf.queue.async {
strongSelf.reloadViewControllerImpl()
}
}
} else {
rateLimitedViewControllerReloader?.run()
@ -254,13 +278,14 @@ class ActivitiesCoordinator: Coordinator {
reloadViewControllerImpl()
}
}
private func reloadViewControllerImpl() {
if !activities.isEmpty {
hasLoadedActivitiesTheFirstTime = true
}
let transactions: [Transaction]
let transactions: [TransactionInstance]
if activities.count == EventsActivityDataStore.numberOfActivitiesToUse, let blockNumberOfOldestActivity = activities.last?.blockNumber {
transactions = self.transactions.filter { $0.blockNumber >= blockNumberOfOldestActivity }
} else {
@ -268,13 +293,18 @@ class ActivitiesCoordinator: Coordinator {
}
let items = combine(activities: activities, withTransactions: transactions)
if let items = items {
rootViewController.configure(viewModel: .init(tokensStorages: tokensStorages, activities: items))
let activities = ActivitiesViewModel.sorted(activities: items)
DispatchQueue.main.async {
self.rootViewController.configure(viewModel: .init(tokensStorages: self.tokensStorages, activities: activities))
}
}
}
//Combining includes filtering around activities (from events) for ERC20 send/receive transctions which are already covered by transactions
private func combine(activities: [Activity], withTransactions: [Transaction]) -> [ActivityOrTransactionRow]? {
private func combine(activities: [Activity], withTransactions: [TransactionInstance]) -> [ActivityOrTransactionRow]? {
var transactionRows: [TransactionRow] = .init()
for each in transactions {
if each.localizedOperations.isEmpty {
@ -337,17 +367,20 @@ class ActivitiesCoordinator: Coordinator {
}
//Important to pass in the `TokenHolder` instance and not re-create so that we don't override the subscribable values for the token with ones that are not resolved yet
private func refreshActivity(tokenObject: TokenObject, tokenHolder: TokenHolder, activity: Activity, isFirstUpdate: Bool = true) {
private func refreshActivity(tokenObject: Activity.AssignedToken, tokenHolder: TokenHolder, activity: Activity, isFirstUpdate: Bool = true) {
let attributeValues = AssetAttributeValues(attributeValues: tokenHolder.values)
let resolvedAttributeNameValues = attributeValues.resolve { [weak self, weak tokenHolder] _ in
guard let strongSelf = self, let tokenHolder = tokenHolder, isFirstUpdate else { return }
strongSelf.refreshActivity(tokenObject: tokenObject, tokenHolder: tokenHolder, activity: activity, isFirstUpdate: false)
}
if let (index, oldActivity) = activitiesIndexLookup[activity.id] {
let updatedValues = (token: oldActivity.values.token.merging(resolvedAttributeNameValues) { _, new in new }, card: oldActivity.values.card)
let updatedActivity: Activity = .init(id: oldActivity.id, rowType: oldActivity.rowType, tokenObject: tokenObject, server: oldActivity.server, name: oldActivity.name, eventName: oldActivity.eventName, blockNumber: oldActivity.blockNumber, transactionId: oldActivity.transactionId, transactionIndex: oldActivity.transactionIndex, logIndex: oldActivity.logIndex, date: oldActivity.date, values: updatedValues, view: oldActivity.view, itemView: oldActivity.itemView, isBaseCard: oldActivity.isBaseCard, state: oldActivity.state)
activities[index] = updatedActivity
reloadViewController(reloadImmediately: false)
if let activityViewController = activityViewController, activityViewController.isForActivity(updatedActivity) {
activityViewController.configure(viewModel: .init(activity: updatedActivity))
}
@ -401,20 +434,24 @@ extension ActivitiesCoordinator: ActivitiesViewControllerDelegate {
showActivity(activity)
}
func didPressTransaction(transaction: Transaction, in viewController: ActivitiesViewController) {
func didPressTransaction(transaction: TransactionInstance, in viewController: ActivitiesViewController) {
delegate?.didPressTransaction(transaction: transaction, in: viewController)
}
}
extension ActivitiesCoordinator: ActivityViewControllerDelegate {
func reinject(viewController: ActivityViewController) {
guard let (tokenObject, tokenHolder) = tokensAndTokenHolders[viewController.viewModel.activity.tokenObject.contractAddress] else { return }
guard let (token, tokenHolder) = tokensAndTokenHolders[viewController.viewModel.activity.tokenObject.contractAddress] else { return }
let activity = viewController.viewModel.activity
refreshActivity(tokenObject: tokenObject, tokenHolder: tokenHolder[0], activity: activity)
refreshActivity(tokenObject: token, tokenHolder: tokenHolder[0], activity: activity)
}
func goToToken(viewController: ActivityViewController) {
delegate?.show(tokenObject: viewController.viewModel.activity.tokenObject, fromCoordinator: self)
let token = viewController.viewModel.activity.tokenObject
guard let tokenObject = tokensStorages[token.server].token(forContract: token.contractAddress) else { return }
delegate?.show(tokenObject: tokenObject, fromCoordinator: self)
}
func goToTransaction(viewController: ActivityViewController) {
@ -427,7 +464,7 @@ extension ActivitiesCoordinator: ActivityViewControllerDelegate {
}
extension ActivitiesCoordinator: TransactionDataCoordinatorDelegate {
func didUpdate(result: ResultResult<[Transaction], TransactionError>.t, reloadImmediately: Bool) {
func didUpdate(result: ResultResult<[TransactionInstance], TransactionError>.t, reloadImmediately: Bool) {
switch result {
case .success(let items):
transactions = items

@ -6,7 +6,7 @@ import StatefulViewController
protocol ActivitiesViewControllerDelegate: class {
func didPressActivity(activity: Activity, in viewController: ActivitiesViewController)
func didPressTransaction(transaction: Transaction, in viewController: ActivitiesViewController)
func didPressTransaction(transaction: TransactionInstance, in viewController: ActivitiesViewController)
}
class ActivitiesViewController: UIViewController {
@ -143,6 +143,7 @@ class ActivitiesViewController: UIViewController {
return nil
}
}
let t = Activity.AssignedToken(tokenObject: token)
let activityName: String
if wallet.sameContract(as: transactionRow.from) {
@ -201,7 +202,7 @@ class ActivitiesViewController: UIViewController {
//We only use this ID for refreshing the display of specific activity, since the display for ETH send/receives don't ever need to be refreshed, just need a number that don't clash with other activities
id: transactionRow.blockNumber + 10000000,
rowType: rowType,
tokenObject: token,
tokenObject: t,
server: transactionRow.server,
name: activityName,
eventName: activityName,
@ -301,16 +302,16 @@ extension ActivitiesViewController: UISearchResultsUpdating {
//At least on iOS 13 beta on a device. updateSearchResults(for:) is called when we set `searchController.isActive = false` to dismiss search (because user tapped on a filter), but the value of `searchController.isActive` remains `false` during the call, hence the async.
//This behavior is not observed in iOS 12, simulator
public func updateSearchResults(for searchController: UISearchController) {
DispatchQueue.main.async {
self.processSearchWithKeywords()
}
processSearchWithKeywords()
}
private func processSearchWithKeywords() {
let keyword = searchController.searchBar.text
viewModel.filter(.keyword(keyword))
tableView.reloadData()
}
}
extension ActivitiesViewController {

@ -18,12 +18,12 @@ struct ActivitiesViewModel {
private var filteredItems: [MappedToDateActivityOrTransaction] = []
private let tokensStorages: ServerDictionary<TokensDataStore>
init(tokensStorages: ServerDictionary<TokensDataStore>, activities: [ActivityOrTransactionRow] = []) {
items = ActivitiesViewModel.sorted(activities: activities)
init(tokensStorages: ServerDictionary<TokensDataStore>, activities: [MappedToDateActivityOrTransaction] = []) {
items = activities
self.tokensStorages = tokensStorages
}
private static func sorted(activities: [ActivityOrTransactionRow]) -> [MappedToDateActivityOrTransaction] {
static func sorted(activities: [ActivityOrTransactionRow]) -> [MappedToDateActivityOrTransaction] {
//Uses NSMutableArray instead of Swift array for performance. Really slow when dealing with 10k events, which is hardly a big wallet
var newItems: [String: NSMutableArray] = [:]
for each in activities {
@ -68,7 +68,11 @@ struct ActivitiesViewModel {
}
})
}.sorted { (object1, object2) -> Bool in
ActivitiesViewModel.formatter.date(from: object1.date)! > ActivitiesViewModel.formatter.date(from: object2.date)!
//NOTE: Remove force unwrap to prevent crash
guard let date1 = ActivitiesViewModel.formatter.date(from: object1.date), let date2 = ActivitiesViewModel.formatter.date(from: object2.date) else {
return false
}
return date1 > date2
}
}

@ -22,7 +22,9 @@ class MigrationInitializerForOneChainPerDatabase: Initializer {
// swiftlint:disable function_body_length
func perform() {
config.schemaVersion = 53
config.migrationBlock = { migration, oldSchemaVersion in
config.migrationBlock = { [weak self] migration, oldSchemaVersion in
guard let strongSelf = self else { return }
if oldSchemaVersion < 33 {
migration.enumerateObjects(ofType: TokenObject.className()) { oldObject, newObject in
guard let oldObject = oldObject else { return }
@ -57,7 +59,7 @@ class MigrationInitializerForOneChainPerDatabase: Initializer {
guard let oldObject = oldObject else { return }
guard let newObject = newObject else { return }
if let contract = (oldObject["contract"] as? String).flatMap({ AlphaWallet.Address(uncheckedAgainstNullAddress: $0) }), let type = (oldObject["rawType"] as? String).flatMap({ TokenType(rawValue: $0) }) {
let tokenTypeName = XMLHandler(contract: contract, tokenType: type, assetDefinitionStore: self.assetDefinitionStore).getLabel(fallback: "")
let tokenTypeName = XMLHandler(contract: contract, tokenType: type, assetDefinitionStore: strongSelf.assetDefinitionStore).getLabel(fallback: "")
if !tokenTypeName.isEmpty {
newObject["name"] = ""
}
@ -76,7 +78,7 @@ class MigrationInitializerForOneChainPerDatabase: Initializer {
migration.deleteData(forType: Transaction.className())
}
if oldSchemaVersion < 53 {
let chainId = self.server.chainID
let chainId = strongSelf.server.chainID
migration.enumerateObjects(ofType: TokenObject.className()) { _, newObject in
guard let newObject = newObject else { return }
guard let contract = newObject["contract"] as? String else {

@ -8,11 +8,35 @@ import WalletCore
///Use an enum as a namespace until Swift has proper namespaces
public enum AlphaWallet {}
extension AlphaWallet.Address {
private class TheadSafeAddressCache {
private var cache: [String: AlphaWallet.Address] = .init()
private let accessQueue = DispatchQueue(label: "SynchronizedArrayAccess", attributes: .concurrent)
subscript(key: String) -> AlphaWallet.Address? {
get {
var element: AlphaWallet.Address?
accessQueue.sync {
element = cache[key]
}
return element
}
set {
accessQueue.async(flags: .barrier) {
self.cache[key] = newValue
}
}
}
}
}
//TODO move this to a standard alone internal Pod with 0 external dependencies so main app and TokenScript can use it?
extension AlphaWallet {
public enum Address: Hashable, Codable {
//Computing EIP55 is really slow. Cache needed when we need to create many addresses, like parsing a whole lot of Ethereum event logs
private static var cache: [String: Address] = .init()
//there is cases when cache accessing from different treads, fro this case we need to use sync access for it
private static var cache: TheadSafeAddressCache = .init()
case ethereumAddress(eip55String: String)
@ -36,10 +60,16 @@ extension AlphaWallet {
//TODO not sure if we should keep this
init?(uncheckedAgainstNullAddress string: String) {
if let value = Self.cache[string] {
self = value
return
}
let string = string.add0x
guard string.count == 42 else { return nil }
guard let address = TrustKeystore.Address(uncheckedAgainstNullAddress: string) else { return nil }
self = .ethereumAddress(eip55String: address.eip55String)
Self.cache[string] = self
}
init(fromPrivateKey privateKey: Data) {

@ -22,8 +22,13 @@ extension AlphaWallet.Address {
extension EthereumAddress {
init(address: AlphaWallet.Address) {
//EthereumAddress(Data) is much faster than EthereumAddress(String). This is significant because we can make a few hundred calls
let data = Data.fromHex(address.eip55String)!
self.init(data)!
// let data = Data.fromHex(address.eip55String)!
// self.init(data)!
//During testing we found that EthereumAddress(address.eip55String) is faster then self.init(data)!
//approx time is 0.000980973243713379 while with using self.init(data)! is 2.8967857360839844e-05 seconds.
self.init(address.eip55String)!
}
}

@ -52,22 +52,25 @@ struct RawTransaction: Decodable {
let operationsLocalized: [LocalizedOperation]?
}
extension Transaction {
static func from(transaction: RawTransaction, tokensStorage: TokensDataStore) -> Promise<Transaction?> {
extension TransactionInstance {
static func from(transaction: RawTransaction, tokensStorage: TokensDataStore) -> Promise<TransactionInstance?> {
guard let from = AlphaWallet.Address(string: transaction.from) else {
return Promise.value(nil)
}
let state: TransactionState = {
if transaction.error?.isEmpty == false || transaction.isError == "1" {
return .error
}
return .completed
}()
let to = AlphaWallet.Address(string: transaction.to)?.eip55String ?? transaction.to
return firstly {
createOperationForTokenTransfer(forTransaction: transaction, tokensStorage: tokensStorage)
}.then { operations -> Promise<Transaction?> in
let result = Transaction(
}.then { operations -> Promise<TransactionInstance?> in
let result = TransactionInstance(
id: transaction.hash,
server: tokensStorage.server,
blockNumber: Int(transaction.blockNumber)!,
@ -84,11 +87,12 @@ extension Transaction {
state: state,
isErc20Interaction: false
)
return .value(result)
}
}
static private func createOperationForTokenTransfer(forTransaction transaction: RawTransaction, tokensStorage: TokensDataStore) -> Promise<[LocalizedOperationObject]> {
static private func createOperationForTokenTransfer(forTransaction transaction: RawTransaction, tokensStorage: TokensDataStore) -> Promise<[LocalizedOperationObjectInstance]> {
guard transaction.input != "0x" else {
return Promise.value([])
}
@ -105,21 +109,23 @@ extension Transaction {
let amount = BigInt(amount1, radix: 16)
//Extract the address and strip the first 12 (x2 = 24) characters of 0s
let to = "0x\(transaction.input[transaction.input.index(transaction.input.startIndex, offsetBy: 10 + 24)..<transaction.input.index(transaction.input.startIndex, offsetBy: 10 + 64)])"
if let amount = amount, let contract = transaction.toAddress, let to = AlphaWallet.Address(string: to)?.eip55String {
if let token = tokensStorage.token(forContract: contract) {
if let token = tokensStorage.tokenThreadSafe(forContract: contract) {
let operationType = mapTokenTypeToTransferOperationType(token.type)
let result = LocalizedOperationObject(from: transaction.from, to: to, contract: contract, type: operationType.rawValue, value: String(amount), symbol: token.symbol, name: token.name, decimals: token.decimals)
let result = LocalizedOperationObjectInstance(from: transaction.from, to: to, contract: contract, type: operationType.rawValue, value: String(amount), symbol: token.symbol, name: token.name, decimals: token.decimals)
return .value([result])
} else {
let getContractName = tokensStorage.getContractName(for: contract)
let getContractSymbol = tokensStorage.getContractSymbol(for: contract)
let getDecimals = tokensStorage.getDecimals(for: contract)
let getTokenType = tokensStorage.getTokenType(for: contract)
return firstly {
when(fulfilled: getContractName, getContractSymbol, getDecimals, getTokenType)
}.then { name, symbol, decimals, tokenType -> Promise<[LocalizedOperationObject]> in
}.then { name, symbol, decimals, tokenType -> Promise<[LocalizedOperationObjectInstance]> in
let operationType = mapTokenTypeToTransferOperationType(tokenType)
let result = LocalizedOperationObject(from: transaction.from, to: to, contract: contract, type: operationType.rawValue, value: String(amount), symbol: symbol, name: name, decimals: Int(decimals))
let result = LocalizedOperationObjectInstance(from: transaction.from, to: to, contract: contract, type: operationType.rawValue, value: String(amount), symbol: symbol, name: name, decimals: Int(decimals))
return .value([result])
}
}

@ -310,6 +310,7 @@ class InCoordinator: NSObject, Coordinator {
setupTokenDataStores()
setupNativeCryptoCurrencyPrices()
setupNativeCryptoCurrencyBalances()
setupEventsStorages()
setupTransactionsStorages()
setupEtherBalances()
setupWalletSessions()
@ -319,6 +320,12 @@ class InCoordinator: NSObject, Coordinator {
setUpEventSourceCoordinatorForActivities()
}
private func setupEventsStorages() {
let realm = self.realm(forAccount: wallet)
eventsDataStore = EventsDataStore(realm: realm)
eventsActivityDataStore = EventsActivityDataStore(realm: realm)
}
private func setupNativeCryptoCurrencyPrices() {
nativeCryptoCurrencyPrices = createEtherPricesSubscribablesForAllChains()
}
@ -857,7 +864,7 @@ extension InCoordinator: TokensCoordinatorDelegate {
showPaymentFlow(for: type, server: server)
}
func didTap(transaction: Transaction, inViewController viewController: UIViewController, in coordinator: TokensCoordinator) {
func didTap(transaction: TransactionInstance, inViewController viewController: UIViewController, in coordinator: TokensCoordinator) {
if transaction.localizedOperations.count > 1 {
transactionCoordinator?.showTransaction(.group(transaction), inViewController: viewController)
} else {
@ -940,7 +947,8 @@ extension InCoordinator: EventSourceCoordinatorForActivitiesDelegate {
}
extension InCoordinator: ActivitiesCoordinatorDelegate {
func didPressTransaction(transaction: Transaction, in viewController: ActivitiesViewController) {
func didPressTransaction(transaction: TransactionInstance, in viewController: ActivitiesViewController) {
if transaction.localizedOperations.count > 1 {
transactionCoordinator?.showTransaction(.group(transaction), inViewController: viewController)
} else {
@ -1001,4 +1009,4 @@ extension InCoordinator {
private func logTappedSwap(service: SwapTokenURLProviderType) {
analyticsCoordinator.log(navigation: Analytics.Navigation.tokenSwap, properties: [Analytics.Properties.name.rawValue: service.analyticsName])
}
}
}

@ -100,7 +100,7 @@ enum RPCServer: Hashable, CaseIterable {
case .custom: return nil
}
}
//TODO fix up all the networks
var getEtherscanURLERC20Events: String? {
switch self {

@ -14,6 +14,7 @@ class EventSourceCoordinator {
private let eventsDataStore: EventsDataStoreProtocol
private var isFetching = false
private var rateLimitedUpdater: RateLimiter?
private let queue = DispatchQueue(label: "com.eventSourceCoordinator.updateQueue")
init(wallet: Wallet, config: Config, tokensStorages: ServerDictionary<TokensDataStore>, assetDefinitionStore: AssetDefinitionStore, eventsDataStore: EventsDataStoreProtocol) {
self.wallet = wallet
@ -32,36 +33,40 @@ class EventSourceCoordinator {
for each in xmlHandler.attributesWithEventSource {
guard let eventOrigin = each.eventOrigin else { continue }
let tokenHolders = TokenAdaptor(token: token, assetDefinitionStore: assetDefinitionStore, eventsDataStore: eventsDataStore).getTokenHolders(forWallet: wallet, sourceFromEvents: false)
for eachTokenHolder in tokenHolders {
guard let tokenId = eachTokenHolder.tokenIds.first else { continue }
let promise = fetchEvents(forTokenId: tokenId, token: token, eventOrigin: eventOrigin)
fetchPromises.append(promise)
}
}
return fetchPromises
}
func fetchEthereumEvents() {
if rateLimitedUpdater == nil {
rateLimitedUpdater = RateLimiter(name: "Poll Ethereum events for instances", limit: 15, autoRun: true) { [weak self] in
self?.fetchEthereumEventsImpl()
guard let strongSelf = self else { return }
strongSelf.queue.async {
strongSelf.fetchEthereumEventsImpl()
}
}
} else {
rateLimitedUpdater?.run()
}
}
func fetchEthereumEventsImpl() {
private func fetchEthereumEventsImpl() {
guard !isFetching else { return }
isFetching = true
let tokensStoragesForEnabledServers = config.enabledServers.map { tokensStorages[$0] }
var fetchPromises = [Promise<Void>]()
for eachTokenStorage in tokensStoragesForEnabledServers {
for eachToken in eachTokenStorage.enabledObject {
let promises = fetchEventsByTokenId(forToken: eachToken)
fetchPromises.append(contentsOf: promises)
}
let fetchPromises = tokensStoragesForEnabledServers.flatMap {
$0.enabledObject.flatMap { fetchEventsByTokenId(forToken: $0) }
}
when(resolved: fetchPromises).done { _ in
self.isFetching = false
}
@ -69,38 +74,44 @@ class EventSourceCoordinator {
private func fetchEvents(forTokenId tokenId: TokenId, token: TokenObject, eventOrigin: EventOrigin) -> Promise<Void> {
let (filterName, filterValue) = eventOrigin.eventFilter
let filterParam: [(filter: [EventFilterable], textEquivalent: String)?] = eventOrigin.parameters
let filterParam = eventOrigin.parameters
.filter { $0.isIndexed }
.map { self.formFilterFrom(fromParameter: $0, tokenId: tokenId, filterName: filterName, filterValue: filterValue) }
let fromBlock: EventFilter.Block
let oldEvents = eventsDataStore.getMatchingEventsSortedByBlockNumber(forContract: eventOrigin.contract, tokenContract: token.contractAddress, server: token.server, eventName: eventOrigin.eventName)
if let newestEvent = oldEvents.last {
fromBlock = .blockNumber(UInt64(newestEvent.blockNumber + 1))
} else {
fromBlock = .blockNumber(0)
}
let eventFilter = EventFilter(fromBlock: fromBlock, toBlock: .latest, addresses: [EthereumAddress(address: eventOrigin.contract)], parameterFilters: filterParam.map { $0?.filter })
return firstly {
getEventLogs(withServer: token.server, contract: eventOrigin.contract, eventName: eventOrigin.eventName, abiString: eventOrigin.eventAbiString, filter: eventFilter)
}.map { result -> [EventInstance] in
result.compactMap { self.convertEventToDatabaseObject($0, filterParam: filterParam, eventOrigin: eventOrigin, token: token, server: token.server) }
}.map { events in
self.eventsDataStore.add(events: events, forTokenContract: token.contractAddress)
}
let contractAddress = token.contractAddress
let tokenServer = token.server
return eventsDataStore.getLastMatchingEventSortedByBlockNumber(forContract: eventOrigin.contract, tokenContract: contractAddress, server: tokenServer, eventName: eventOrigin.eventName).map(on: queue, { oldEvent -> EventFilter.Block in
if let newestEvent = oldEvent {
return .blockNumber(UInt64(newestEvent.blockNumber + 1))
} else {
return .blockNumber(0)
}
}).map(on: queue, { fromBlock -> EventFilter in
EventFilter(fromBlock: fromBlock, toBlock: .latest, addresses: [EthereumAddress(address: eventOrigin.contract)], parameterFilters: filterParam.map { $0?.filter })
}).then(on: queue, { eventFilter in
getEventLogs(withServer: tokenServer, contract: eventOrigin.contract, eventName: eventOrigin.eventName, abiString: eventOrigin.eventAbiString, filter: eventFilter, queue: self.queue)
}).map(on: queue, { result -> [EventInstanceValue] in
result.compactMap {
self.convertEventToDatabaseObject($0, filterParam: filterParam, eventOrigin: eventOrigin, contractAddress: contractAddress, server: tokenServer)
}
}).then(on: queue, { events -> Promise<Void> in
return self.eventsDataStore.add(events: events, forTokenContract: contractAddress)
})
}
private func convertEventToDatabaseObject(_ event: EventParserResultProtocol, filterParam: [(filter: [EventFilterable], textEquivalent: String)?], eventOrigin: EventOrigin, token: TokenObject, server: RPCServer) -> EventInstance? {
private func convertEventToDatabaseObject(_ event: EventParserResultProtocol, filterParam: [(filter: [EventFilterable], textEquivalent: String)?], eventOrigin: EventOrigin, contractAddress: AlphaWallet.Address, server: RPCServer) -> EventInstanceValue? {
guard let blockNumber = event.eventLog?.blockNumber else { return nil }
guard let logIndex = event.eventLog?.logIndex else { return nil }
let decodedResult = self.convertToJsonCompatible(dictionary: event.decodedResult)
let decodedResult = Self.convertToJsonCompatible(dictionary: event.decodedResult)
guard let json = decodedResult.jsonString else { return nil }
//TODO when TokenScript schema allows it, support more than 1 filter
let filterTextEquivalent = filterParam.compactMap({ $0?.textEquivalent }).first
let filterText = filterTextEquivalent ?? "\(eventOrigin.eventFilter.name)=\(eventOrigin.eventFilter.value)"
return EventInstance(contract: eventOrigin.contract, tokenContract: token.contractAddress, server: server, eventName: eventOrigin.eventName, blockNumber: Int(blockNumber), logIndex: Int(logIndex), filter: filterText, json: json)
return EventInstanceValue(contract: eventOrigin.contract, tokenContract: contractAddress, server: server, eventName: eventOrigin.eventName, blockNumber: Int(blockNumber), logIndex: Int(logIndex), filter: filterText, json: json)
}
private func convertToJsonCompatible(dictionary: [String: Any]) -> [String: Any] {
private static func convertToJsonCompatible(dictionary: [String: Any]) -> [String: Any] {
Dictionary(uniqueKeysWithValues: dictionary.compactMap { key, value -> (String, Any)? in
switch value {
case let address as EthereumAddress:

@ -17,8 +17,9 @@ class EventSourceCoordinatorForActivities {
private let eventsDataStore: EventsActivityDataStoreProtocol
private var isFetching = false
private var rateLimitedUpdater: RateLimiter?
private let queue = DispatchQueue(label: "com.EventSourceCoordinatorForActivities.updateQueue")
weak var delegate: EventSourceCoordinatorForActivitiesDelegate?
private let timestampCoordinator = GetBlockTimestampCoordinator()
init(wallet: Wallet, config: Config, tokensStorages: ServerDictionary<TokensDataStore>, assetDefinitionStore: AssetDefinitionStore, eventsDataStore: EventsActivityDataStoreProtocol) {
self.wallet = wallet
@ -29,92 +30,132 @@ class EventSourceCoordinatorForActivities {
}
func fetchEvents(forToken token: TokenObject) -> [Promise<Void>] {
let xmlHandler = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore)
guard xmlHandler.hasAssetDefinition else { return .init() }
var fetchPromises = [Promise<Void>]()
for each in xmlHandler.activityCards {
guard let promise = fetchEvents(token: token, card: each) else { continue }
fetchPromises.append(promise)
let xmlHandler = XMLHandler(contract: token.contractAddress, tokenType: token.type, assetDefinitionStore: assetDefinitionStore)
guard xmlHandler.hasAssetDefinition else { return [] }
return xmlHandler.activityCards.compactMap {
self.fetchEvents(tokenContract: token.contractAddress, server: token.server, card: $0)
}
}
func fetchEvents(contract: AlphaWallet.Address, tokenType: TokenType, rpcServer: RPCServer) -> [Promise<Void>] {
let xmlHandler = XMLHandler(contract: contract, tokenType: tokenType, assetDefinitionStore: assetDefinitionStore)
guard xmlHandler.hasAssetDefinition else { return [] }
return xmlHandler.activityCards.compactMap {
fetchEvents(tokenContract: contract, server: rpcServer, card: $0)
}
return fetchPromises
}
func fetchEthereumEvents() {
if rateLimitedUpdater == nil {
rateLimitedUpdater = RateLimiter(name: "Poll Ethereum events for Activities", limit: 60, autoRun: true) { [weak self] in
self?.fetchEthereumEventsImpl()
guard let strongSelf = self else { return }
strongSelf.queue.async {
strongSelf.fetchEthereumEventsImpl()
}
}
} else {
rateLimitedUpdater?.run()
}
}
func fetchEthereumEventsImpl() {
private func fetchEthereumEventsImpl() {
guard !isFetching else { return }
isFetching = true
let tokensStoragesForEnabledServers = config.enabledServers.map { tokensStorages[$0] }
var fetchPromises = [Promise<Void>]()
for eachTokenStorage in tokensStoragesForEnabledServers {
for eachToken in eachTokenStorage.enabledObject {
let promises = fetchEvents(forToken: eachToken)
fetchPromises.append(contentsOf: promises)
let promises = firstly {
tokensForEnabledRPCServers()
}.map(on: queue, { data -> [Promise<Void>] in
data.flatMap { data in
self.fetchEvents(contract: data.contract, tokenType: data.tokenType, rpcServer: data.server)
}
}
when(resolved: fetchPromises).done { _ in
})
when(resolved: promises).done(on: queue, { _ in
self.isFetching = false
})
}
typealias EnabledTokenAddreses = [(contract: AlphaWallet.Address, tokenType: TokenType, server: RPCServer)]
private func tokensForEnabledRPCServers() -> Promise<EnabledTokenAddreses> {
return Promise { seal in
let tokensStoragesForEnabledServers = self.config.enabledServers.map { self.tokensStorages[$0] }
let data = tokensStoragesForEnabledServers.flatMap {
$0.enabledObject
}.compactMap {
(contract: $0.contractAddress, tokenType: $0.type, server: $0.server)
}
seal.fulfill(data)
}
}
private func fetchEvents(token: TokenObject, card: TokenScriptCard) -> Promise<Void>? {
private func fetchEvents(tokenContract: AlphaWallet.Address, server: RPCServer, card: TokenScriptCard) -> Promise<Void>? {
let eventOrigin = card.eventOrigin
let (filterName, filterValue) = eventOrigin.eventFilter
let filterParam: [(filter: [EventFilterable], textEquivalent: String)?] = eventOrigin.parameters
.filter { $0.isIndexed }
.map { self.formFilterFrom(fromParameter: $0, filterName: filterName, filterValue: filterValue) }
let fromBlock: EventFilter.Block
let oldEvents = eventsDataStore.getMatchingEventsSortedByBlockNumber(forContract: eventOrigin.contract, tokenContract: token.contractAddress, server: token.server, eventName: eventOrigin.eventName)
//TODO have to start from 0 if the TokenScript file changes?
if let newestEvent = oldEvents.last {
fromBlock = .blockNumber(UInt64(newestEvent.blockNumber + 1))
} else {
fromBlock = .blockNumber(0)
let filterParam = eventOrigin.parameters.filter {
$0.isIndexed
}.map {
self.formFilterFrom(fromParameter: $0, filterName: filterName, filterValue: filterValue)
}
let eventFilter = EventFilter(fromBlock: fromBlock, toBlock: .latest, addresses: [EthereumAddress(address: eventOrigin.contract)], parameterFilters: filterParam.map { $0?.filter })
if filterParam.allSatisfy({ $0 == nil }) {
//TODO log to console as diagnostic
NSLog("TokenScript filter parameters for Activity \"\(card.name)\" are all nil. Ignoring this Activity. \(filterParam.map { $0?.filter })")
return nil
}
return firstly {
getEventLogs(withServer: token.server, contract: eventOrigin.contract, eventName: eventOrigin.eventName, abiString: eventOrigin.eventAbiString, filter: eventFilter)
}.thenMap { event -> Promise<(EventParserResultProtocol, Date)?> in
return eventsDataStore.getMatchingEventsSortedByBlockNumber(forContract: eventOrigin.contract, tokenContract: tokenContract, server: server, eventName: eventOrigin.eventName).map(on: queue, { oldEvent -> EventFilter.Block in
if let newestEvent = oldEvent {
return .blockNumber(UInt64(newestEvent.blockNumber + 1))
} else {
return .blockNumber(0)
}
}).map(on: queue, { fromBlock -> EventFilter in
let parameterFilters = filterParam.map { $0?.filter }
let addresses = [EthereumAddress(address: eventOrigin.contract)]
return EventFilter(fromBlock: fromBlock, toBlock: .latest, addresses: addresses, parameterFilters: parameterFilters)
}).then(on: queue, { eventFilter in
getEventLogs(withServer: server, contract: eventOrigin.contract, eventName: eventOrigin.eventName, abiString: eventOrigin.eventAbiString, filter: eventFilter, queue: self.queue)
}).thenMap(on: queue, { event -> Promise<(EventParserResultProtocol, Date)?> in
guard let blockNumber = event.eventLog?.blockNumber else { return .value(nil) }
return GetBlockTimestampCoordinator().getBlockTimestamp(blockNumber, onServer: token.server).map { date in (event, date) }
}.compactMapValues {
return self.timestampCoordinator.getBlockTimestamp(blockNumber, onServer: server).map(on: self.queue, { date in (event, date) })
}).compactMapValues(on: queue, {
$0
}.compactMapValues { event, date in
self.convertEventToDatabaseObject(event, date: date, filterParam: filterParam, eventOrigin: eventOrigin, token: token, server: token.server)
}.map { events in
self.eventsDataStore.add(events: events, forTokenContract: token.contractAddress)
}).compactMapValues(on: queue, { event, date in
return self.convertEventToDatabaseObject(event, date: date, filterParam: filterParam, eventOrigin: eventOrigin, tokenContract: tokenContract, server: server)
}).then(on: queue, { events -> Promise<Bool> in
return self.eventsDataStore.add(events: events, forTokenContract: tokenContract).map { _ -> Bool in
!events.isEmpty
}
}).map(on: queue, { shouldNotify in
guard shouldNotify else { return }
self.delegate?.didUpdate(inCoordinator: self)
}
})
}
private func convertEventToDatabaseObject(_ event: EventParserResultProtocol, date: Date, filterParam: [(filter: [EventFilterable], textEquivalent: String)?], eventOrigin: EventOrigin, token: TokenObject, server: RPCServer) -> EventActivity? {
private func convertEventToDatabaseObject(_ event: EventParserResultProtocol, date: Date, filterParam: [(filter: [EventFilterable], textEquivalent: String)?], eventOrigin: EventOrigin, tokenContract: AlphaWallet.Address, server: RPCServer) -> EventActivityInstance? {
guard let blockNumber = event.eventLog?.blockNumber else { return nil }
guard let logIndex = event.eventLog?.logIndex else { return nil }
guard let transactionHash = event.eventLog?.transactionHash else { return nil }
guard let transactionIndex = event.eventLog?.transactionIndex else { return nil }
let transactionId = transactionHash.hexEncoded
let decodedResult = self.convertToJsonCompatible(dictionary: event.decodedResult)
let decodedResult = Self.convertToJsonCompatible(dictionary: event.decodedResult)
guard let json = decodedResult.jsonString else { return nil }
//TODO when TokenScript schema allows it, support more than 1 filter
let filterTextEquivalent = filterParam.compactMap({ $0?.textEquivalent }).first
let filterText = filterTextEquivalent ?? "\(eventOrigin.eventFilter.name)=\(eventOrigin.eventFilter.value)"
return EventActivity(contract: eventOrigin.contract, tokenContract: token.contractAddress, server: server, date: date, eventName: eventOrigin.eventName, blockNumber: Int(blockNumber), transactionId: transactionId, transactionIndex: Int(transactionIndex), logIndex: Int(logIndex), filter: filterText, json: json)
return EventActivityInstance(contract: eventOrigin.contract, tokenContract: tokenContract, server: server, date: date, eventName: eventOrigin.eventName, blockNumber: Int(blockNumber), transactionId: transactionId, transactionIndex: Int(transactionIndex), logIndex: Int(logIndex), filter: filterText, json: json)
}
private func convertToJsonCompatible(dictionary: [String: Any]) -> [String: Any] {
private static func convertToJsonCompatible(dictionary: [String: Any]) -> [String: Any] {
Dictionary(uniqueKeysWithValues: dictionary.compactMap { key, value -> (String, Any)? in
switch value {
case let address as EthereumAddress:

@ -28,7 +28,7 @@ struct Activity {
let id: Int
let rowType: ActivityRowType
//TODO safe to have TokenObject here? Maybe a struct is better
let tokenObject: TokenObject
let tokenObject: AssignedToken
let server: RPCServer
let name: String
let eventName: String
@ -43,6 +43,25 @@ struct Activity {
let isBaseCard: Bool
let state: State
init(id: Int, rowType: ActivityRowType, tokenObject: AssignedToken, server: RPCServer, name: String, eventName: String, blockNumber: Int, transactionId: String, transactionIndex: Int, logIndex: Int, date: Date, values: (token: [AttributeId: AssetInternalValue], card: [AttributeId: AssetInternalValue]), view: (html: String, style: String), itemView: (html: String, style: String), isBaseCard: Bool, state: State) {
self.id = id
self.tokenObject = tokenObject
self.server = server
self.name = name
self.eventName = eventName
self.blockNumber = blockNumber
self.transactionId = transactionId
self.transactionIndex = transactionIndex
self.logIndex = logIndex
self.date = date
self.values = values
self.view = view
self.itemView = itemView
self.isBaseCard = isBaseCard
self.state = state
self.rowType = rowType
}
var viewHtml: (html: String, hash: Int) {
let hash = "\(view.style)\(view.html)".hashForCachingHeight
return (html: wrapWithHtmlViewport(html: view.html, style: view.style, forTokenId: .init(id)), hash: hash)
@ -102,4 +121,4 @@ struct Activity {
return .none
}
}
}
}

@ -0,0 +1,71 @@
// Copyright © 2020 Stormbird PTE. LTD.
import Foundation
enum ActivityOrTransaction {
case activity(Activity)
case transaction(TransactionInstance)
var activityName: String? {
switch self {
case .activity(let activity):
return activity.name
case .transaction:
return nil
}
}
var date: Date {
switch self {
case .activity(let activity):
return activity.date
case .transaction(let transaction):
return transaction.date
}
}
var blockNumber: Int {
switch self {
case .activity(let activity):
return activity.blockNumber
case .transaction(let transaction):
return transaction.blockNumber
}
}
var transactionIndex: Int {
switch self {
case .activity(let activity):
return activity.transactionIndex
case .transaction(let transaction):
return transaction.transactionIndex
}
}
func getTokenSymbol(fromTokensStorages tokensStorages: ServerDictionary<TokensDataStore>) -> String? {
switch self {
case .activity(let activity):
return activity.tokenObject.symbol
case .transaction(let transaction):
return getSymbol(fromTransaction: transaction, tokensStorages: tokensStorages)
}
}
private func getSymbol(fromTransaction transaction: TransactionInstance, tokensStorages: ServerDictionary<TokensDataStore>) -> String? {
if transaction.operation == nil {
let token = TokensDataStore.etherToken(forServer: transaction.server)
return token.symbol
} else {
switch (transaction.state, transaction.operation?.operationType) {
case (.pending, .erc20TokenTransfer), (.pending, .erc721TokenTransfer), (.pending, .erc875TokenTransfer):
let token = transaction.operation?.contractAddress.flatMap { tokensStorages[transaction.server].tokenThreadSafe(forContract: $0) }
return token?.symbol
//Explicitly listing out combinations so future changes to enums will be caught by compiler
case (.pending, .nativeCurrencyTokenTransfer), (.pending, .unknown), (.pending, nil):
return nil
case (.unknown, _), (.error, _), (.failed, _), (.completed, _):
return nil
}
}
}
}

@ -29,10 +29,10 @@ class AssetAttributeValues {
if !subscribedAttributes.contains(where: { $0 === subscribable }) {
subscribedAttributes.append(subscribable)
subscribable.subscribe { [weak self] value in
guard let strongSelf = self, let value = value else { return }
guard let stongSelf = self, let value = value else { return }
strongSelf.resolvedAttributeValues[name] = value
block(strongSelf.resolvedAttributeValues)
stongSelf.resolvedAttributeValues[name] = value
block(stongSelf.resolvedAttributeValues)
}
}
}

@ -2,12 +2,12 @@
import Foundation
import RealmSwift
import PromiseKit
protocol EventsActivityDataStoreProtocol {
func add(events: [EventActivity], forTokenContract contract: AlphaWallet.Address)
func getMatchingEventsSortedByBlockNumber(forContract contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String) -> [EventActivity]
func getEvents(forContract contract: AlphaWallet.Address, forEventName eventName: String, filter: String, server: RPCServer) -> [EventActivity]
func getRecentEvents() -> [EventActivity]
func getMatchingEventsSortedByBlockNumber(forContract contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String) -> Promise <EventActivityInstance?>
func add(events: [EventActivityInstance], forTokenContract contract: AlphaWallet.Address) -> Promise<Void>
}
class EventsActivityDataStore: EventsActivityDataStoreProtocol {
@ -20,13 +20,20 @@ class EventsActivityDataStore: EventsActivityDataStoreProtocol {
self.realm = realm
}
func getMatchingEventsSortedByBlockNumber(forContract contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String) -> [EventActivity] {
Array(realm.objects(EventActivity.self)
func getMatchingEventsSortedByBlockNumber(forContract contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String) -> Promise <EventActivityInstance?> {
return Promise { seal in
let objects = realm.threadSafe.objects(EventActivity.self)
.filter("contract = '\(contract.eip55String)'")
.filter("tokenContract = '\(tokenContract.eip55String)'")
.filter("chainId = \(server.chainID)")
.filter("eventName = '\(eventName)'")
.sorted(byKeyPath: "blockNumber"))
.sorted(byKeyPath: "blockNumber")
.last
.map { EventActivityInstance(event: $0) }
seal.fulfill(objects)
}
}
func getEvents(forContract contract: AlphaWallet.Address, forEventName eventName: String, filter: String, server: RPCServer) -> [EventActivity] {
@ -38,7 +45,7 @@ class EventsActivityDataStore: EventsActivityDataStoreProtocol {
}
func getRecentEvents() -> [EventActivity] {
return Array(realm.objects(EventActivity.self)
return Array(realm.threadSafe.objects(EventActivity.self)
.sorted(byKeyPath: "date", ascending: false)
.prefix(Self.numberOfActivitiesToUse)
)
@ -50,12 +57,24 @@ class EventsActivityDataStore: EventsActivityDataStoreProtocol {
}
}
func add(events: [EventActivity], forTokenContract contract: AlphaWallet.Address) {
guard !events.isEmpty else { return }
try! realm.write {
for each in events {
realm.add(each, update: .all)
func add(events: [EventActivityInstance], forTokenContract contract: AlphaWallet.Address) -> Promise<Void> {
if events.isEmpty {
return .value(())
} else {
return Promise { seal in
let eventsToSave = events.map { EventActivity(value: $0) }
do {
let realm = self.realm.threadSafe
try realm.write {
realm.add(eventsToSave, update: .all)
seal.fulfill(())
}
} catch {
seal.reject(error)
}
}
}
}
}
}

@ -2,12 +2,13 @@
import Foundation
import RealmSwift
import PromiseKit
protocol EventsDataStoreProtocol {
func add(events: [EventInstance], forTokenContract contract: AlphaWallet.Address)
func getLastMatchingEventSortedByBlockNumber(forContract contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String) -> Promise<EventInstance?>
func add(events: [EventInstanceValue], forTokenContract contract: AlphaWallet.Address) -> Promise<Void>
func deleteEvents(forTokenContract contract: AlphaWallet.Address)
func getMatchingEvents(forContract contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String, filterName: String, filterValue: String) -> [EventInstance]
func getMatchingEventsSortedByBlockNumber(forContract contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String) -> [EventInstance]
func subscribe(_ subscribe: @escaping (_ contract: AlphaWallet.Address) -> Void)
}
@ -36,16 +37,7 @@ class EventsDataStore: EventsDataStoreProtocol {
.filter("eventName = '\(eventName)'")
//Filter stored as string, so we do a string comparison
.filter("filter = '\(filterName)=\(filterValue)'"))
}
func getMatchingEventsSortedByBlockNumber(forContract contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String) -> [EventInstance] {
Array(realm.objects(EventInstance.self)
.filter("contract = '\(contract.eip55String)'")
.filter("tokenContract = '\(tokenContract.eip55String)'")
.filter("chainId = \(server.chainID)")
.filter("eventName = '\(eventName)'")
.sorted(byKeyPath: "blockNumber"))
}
}
func deleteEvents(forTokenContract contract: AlphaWallet.Address) {
let events = getEvents(forTokenContract: contract)
@ -63,13 +55,37 @@ class EventsDataStore: EventsDataStoreProtocol {
}
}
func add(events: [EventInstance], forTokenContract contract: AlphaWallet.Address) {
guard !events.isEmpty else { return }
try! realm.write {
for each in events {
realm.add(each, update: .all)
func getLastMatchingEventSortedByBlockNumber(forContract contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String) -> Promise<EventInstance?> {
return Promise { seal in
let event = Array(realm.threadSafe.objects(EventInstance.self)
.filter("contract = '\(contract.eip55String)'")
.filter("tokenContract = '\(tokenContract.eip55String)'")
.filter("chainId = \(server.chainID)")
.filter("eventName = '\(eventName)'")
.sorted(byKeyPath: "blockNumber"))
.last
seal.fulfill(event)
}
}
func add(events: [EventInstanceValue], forTokenContract contract: AlphaWallet.Address) -> Promise<Void> {
if events.isEmpty {
return .value(())
}
return Promise { seal in
do {
let realm = self.realm.threadSafe
try realm.write {
let eventsToSave = events.map { EventInstance(event: $0) }
realm.add(eventsToSave, update: .all)
seal.fulfill(())
}
} catch {
seal.reject(error)
}
}
triggerSubscribers(forContract: contract)
}
}

@ -666,12 +666,59 @@ private class PrivateXMLHandler {
}
// swiftlint:enable type_body_length
private class ThreadSafeBaseXmlHandlersCache {
fileprivate var cache: [String: PrivateXMLHandler] = [:]
private let queue = DispatchQueue(label: "SynchronizedArrayAccess", attributes: .concurrent)
subscript(key: String) -> PrivateXMLHandler? {
get {
var element: PrivateXMLHandler?
queue.sync {
element = cache[key]
}
return element
}
set {
queue.async(flags: .barrier) {
self.cache[key] = newValue
}
}
}
}
private class ThreadSafeXmlHandlersCache {
fileprivate var cache: [AlphaWallet.Address: PrivateXMLHandler] = [:]
private let queue = DispatchQueue(label: "SynchronizedArrayAccess", attributes: .concurrent)
subscript(key: AlphaWallet.Address) -> PrivateXMLHandler? {
get {
var element: PrivateXMLHandler?
queue.sync {
element = cache[key]
}
return element
}
set {
queue.async(flags: .barrier) {
self.cache[key] = newValue
}
}
}
func removeAll() {
queue.async(flags: .barrier) {
self.cache.removeAll()
}
}
}
/// This class delegates all the functionality to a singleton of the actual XML parser. 1 for each contract. So we just parse the XML file 1 time only for each contract
public class XMLHandler {
//TODO not the best thing to have, especially because it's an optional
static var callForAssetAttributeCoordinators: ServerDictionary<CallForAssetAttributeCoordinator>?
fileprivate static var xmlHandlers: [AlphaWallet.Address: PrivateXMLHandler] = [:]
fileprivate static var baseXmlHandlers: [String: PrivateXMLHandler] = [:]
fileprivate static var xmlHandlers = ThreadSafeXmlHandlersCache()
fileprivate static var baseXmlHandlers = ThreadSafeBaseXmlHandlersCache()
private let privateXMLHandler: PrivateXMLHandler
private let baseXMLHandler: PrivateXMLHandler?

@ -7,7 +7,7 @@ import web3swift
class GetBlockTimestampCoordinator {
//TODO persist?
private static var blockTimestampCache: [RPCServer: [BigUInt: Promise<Date>]] = .init()
private static var blockTimestampCache = TheadSafeBlockTimestampCache()
func getBlockTimestamp(_ blockNumber: BigUInt, onServer server: RPCServer) -> Promise<Date> {
var cacheForServer = Self.blockTimestampCache[server] ?? .init()
@ -33,7 +33,28 @@ class GetBlockTimestampCoordinator {
cacheForServer[blockNumber] = promise
Self.blockTimestampCache[server] = cacheForServer
return promise
}
private class TheadSafeBlockTimestampCache {
private var cache: [RPCServer: [BigUInt: Promise<Date>]] = .init()
private let accessQueue = DispatchQueue(label: "SynchronizedArrayAccess", attributes: .concurrent)
subscript(key: RPCServer) -> [BigUInt: Promise<Date>]? {
get {
var element: [BigUInt: Promise<Date>]?
accessQueue.sync {
element = cache[key]
}
return element
}
set {
accessQueue.async(flags: .barrier) {
self.cache[key] = newValue
}
}
}
}
}

@ -8,73 +8,71 @@ import Alamofire
import SwiftyJSON
class GetContractInteractions {
func getErc20Interactions(contractAddress: AlphaWallet.Address? = nil, address: AlphaWallet.Address, server: RPCServer, startBlock: Int? = nil, completion: @escaping ([Transaction]) -> Void) {
private let queue: DispatchQueue
init(queue: DispatchQueue) {
self.queue = queue
}
func getErc20Interactions(contractAddress: AlphaWallet.Address? = nil, address: AlphaWallet.Address, server: RPCServer, startBlock: Int? = nil, completion: @escaping ([TransactionInstance]) -> Void) {
guard let etherscanURL = server.etherscanAPIURLForERC20TxList(for: address, startBlock: startBlock) else { return }
Alamofire.request(etherscanURL).validate().responseJSON { response in
Alamofire.request(etherscanURL).validate().responseJSON(queue: queue, options: [], completionHandler: { response in
switch response.result {
case .success(let value):
//Performance: process in background so UI don't have a chance of blocking if there's a long list of contracts
DispatchQueue.global().async {
let json = JSON(value)
let filteredResult: [(String, JSON)]
if let contractAddress = contractAddress {
//filter based on what contract you are after
filteredResult = json["result"].filter {
$0.1["contractAddress"].stringValue == contractAddress.eip55String.lowercased()
}
} else {
filteredResult = json["result"].filter {
$0.1["to"].stringValue.hasPrefix("0x")
}
let json = JSON(value)
let filteredResult: [(String, JSON)]
if let contractAddress = contractAddress {
//filter based on what contract you are after
filteredResult = json["result"].filter {
$0.1["contractAddress"].stringValue == contractAddress.eip55String.lowercased()
}
let transactions: [Transaction] = filteredResult.map { result in
let transactionJson = result.1
let localizedTokenObj = LocalizedOperationObject(
from: transactionJson["from"].stringValue,
to: transactionJson["to"].stringValue,
contract: AlphaWallet.Address(uncheckedAgainstNullAddress: transactionJson["contractAddress"].stringValue),
type: OperationType.erc20TokenTransfer.rawValue,
value: transactionJson["value"].stringValue,
symbol: transactionJson["tokenSymbol"].stringValue,
name: transactionJson["tokenName"].stringValue,
decimals: transactionJson["tokenDecimal"].intValue
)
return Transaction(
id: transactionJson["hash"].stringValue,
server: server,
blockNumber: transactionJson["blockNumber"].intValue,
transactionIndex: transactionJson["transactionIndex"].intValue,
from: transactionJson["from"].stringValue,
to: transactionJson["to"].stringValue,
//Must not set the value of the ERC20 token transferred as the native crypto value transferred
value: "0",
gas: transactionJson["gas"].stringValue,
gasPrice: transactionJson["gasPrice"].stringValue,
gasUsed: transactionJson["gasUsed"].stringValue,
nonce: transactionJson["nonce"].stringValue,
date: Date(timeIntervalSince1970: transactionJson["timeStamp"].doubleValue),
localizedOperations: [localizedTokenObj],
//The API only returns successful transactions
state: .completed,
isErc20Interaction: true
)
}
var results: [Transaction] = .init()
for each in transactions {
if let found: Transaction = results.first(where: { $0.blockNumber == each.blockNumber }) {
found.localizedOperations.append(objectsIn: each.localizedOperations)
} else {
results.append(each)
}
}
DispatchQueue.main.async {
completion(results)
} else {
filteredResult = json["result"].filter {
$0.1["to"].stringValue.hasPrefix("0x")
}
}
let transactions: [TransactionInstance] = filteredResult.map { result in
let transactionJson = result.1
let localizedTokenObj = LocalizedOperationObjectInstance(
from: transactionJson["from"].stringValue,
to: transactionJson["to"].stringValue,
contract: AlphaWallet.Address(uncheckedAgainstNullAddress: transactionJson["contractAddress"].stringValue),
type: OperationType.erc20TokenTransfer.rawValue,
value: transactionJson["value"].stringValue,
symbol: transactionJson["tokenSymbol"].stringValue,
name: transactionJson["tokenName"].stringValue,
decimals: transactionJson["tokenDecimal"].intValue
)
return TransactionInstance(
id: transactionJson["hash"].stringValue,
server: server,
blockNumber: transactionJson["blockNumber"].intValue,
transactionIndex: transactionJson["transactionIndex"].intValue,
from: transactionJson["from"].stringValue,
to: transactionJson["to"].stringValue,
value: transactionJson["value"].stringValue,
gas: transactionJson["gas"].stringValue,
gasPrice: transactionJson["gasPrice"].stringValue,
gasUsed: transactionJson["gasUsed"].stringValue,
nonce: transactionJson["nonce"].stringValue,
date: Date(timeIntervalSince1970: transactionJson["timeStamp"].doubleValue),
localizedOperations: [localizedTokenObj],
//The API only returns successful transactions
state: .completed,
isErc20Interaction: true
)
}
completion(transactions)
case .failure:
completion([])
}
}
})
}
func getContractList(address: AlphaWallet.Address, server: RPCServer, startBlock: Int? = nil, erc20: Bool, completion: @escaping ([AlphaWallet.Address], Int?) -> Void) {

@ -24,77 +24,91 @@ class GetIsERC721ContractCoordinator {
//Using "kat" instead of "cryptokitties" to avoid being mistakenly detected by app review as supporting CryptoKitties
static let onlyKat = "0x9a20483d"
}
init(forServer server: RPCServer) {
private let queue: DispatchQueue
init(forServer server: RPCServer, queue: DispatchQueue = .global()) {
self.server = server
self.queue = queue
}
func getIsERC721Contract(
for contract: AlphaWallet.Address,
completion: @escaping (ResultResult<Bool, AnyError>.t) -> Void
) {
if contract.sameContract(as: DoesNotSupportERC165Querying.bitizen) {
completion(.success(true))
return
}
if contract.sameContract(as: DoesNotSupportERC165Querying.cryptoSaga) {
completion(.success(true))
return
}
let server = self.server
queue.async {
if contract.sameContract(as: DoesNotSupportERC165Querying.bitizen) {
completion(.success(true))
return
}
if contract.sameContract(as: DoesNotSupportERC165Querying.cryptoSaga) {
completion(.success(true))
return
}
//TODO use callSmartContract() instead
//TODO use callSmartContract() instead
guard let webProvider = Web3HttpProvider(server.rpcURL, network: server.web3Network) else {
completion(.failure(AnyError(Web3Error(description: "Error creating web provider for: \(server.rpcURL) + \(server.web3Network)"))))
return
}
guard let webProvider = Web3HttpProvider(server.rpcURL, network: server.web3Network) else {
completion(.failure(AnyError(Web3Error(description: "Error creating web provider for: \(server.rpcURL) + \(server.web3Network)"))))
return
}
let configuration = webProvider.session.configuration
configuration.timeoutIntervalForRequest = TokensDataStore.fetchContractDataTimeout
configuration.timeoutIntervalForResource = TokensDataStore.fetchContractDataTimeout
let session = URLSession(configuration: configuration)
webProvider.session = session
let configuration = webProvider.session.configuration
configuration.timeoutIntervalForRequest = TokensDataStore.fetchContractDataTimeout
configuration.timeoutIntervalForResource = TokensDataStore.fetchContractDataTimeout
let session = URLSession(configuration: configuration)
webProvider.session = session
let contractAddress = EthereumAddress(address: contract)
let web3 = web3swift.web3(provider: webProvider)
let function = GetInterfaceSupported165Encode()
guard let contractInstance = web3swift.web3.web3contract(web3: web3, abiString: function.abi, at: contractAddress, options: web3.options) else {
completion(.failure(AnyError(Web3Error(description: "Error creating web3swift contract instance to call \(function.name)()"))))
return
}
let contractAddress = EthereumAddress(address: contract)
let web3 = web3swift.web3(provider: webProvider)
let function = GetInterfaceSupported165Encode()
guard let contractInstance = web3swift.web3.web3contract(web3: web3, abiString: function.abi, at: contractAddress, options: web3.options) else {
completion(.failure(AnyError(Web3Error(description: "Error creating web3swift contract instance to call \(function.name)()"))))
return
}
guard let cryptoKittyPromise = contractInstance.method(function.name, parameters: [ERC165Hash.onlyKat] as [AnyObject], options: nil)?.callPromise(options: nil) else {
completion(.failure(AnyError(Web3Error(description: "Error calling \(function.name)() on \(contract.eip55String) with params: \(ERC165Hash.onlyKat)"))))
return
}
guard let cryptoKittyPromise = contractInstance.method(function.name, parameters: [ERC165Hash.onlyKat] as [AnyObject], options: nil)?.callPromise(options: nil) else {
completion(.failure(AnyError(Web3Error(description: "Error calling \(function.name)() on \(contract.eip55String) with params: \(ERC165Hash.onlyKat)"))))
return
}
guard let nonCryptoKittyERC721Promise = contractInstance.method(function.name, parameters: [ERC165Hash.official] as [AnyObject], options: nil)?.callPromise(options: nil) else {
completion(.failure(AnyError(Web3Error(description: "Error calling \(function.name)() on \(contract.eip55String) with params: \(ERC165Hash.official)"))))
return
}
guard let nonCryptoKittyERC721Promise = contractInstance.method(function.name, parameters: [ERC165Hash.official] as [AnyObject], options: nil)?.callPromise(options: nil) else {
completion(.failure(AnyError(Web3Error(description: "Error calling \(function.name)() on \(contract.eip55String) with params: \(ERC165Hash.official)"))))
return
}
guard let nonCryptoKittyERC721WithOldInterfaceHashPromise = contractInstance.method(function.name, parameters: [ERC165Hash.old] as [AnyObject], options: nil)?.callPromise(options: nil) else {
completion(.failure(AnyError(Web3Error(description: "Error calling \(function.name)() on \(contract.eip55String) with params: \(ERC165Hash.old)"))))
return
}
guard let nonCryptoKittyERC721WithOldInterfaceHashPromise = contractInstance.method(function.name, parameters: [ERC165Hash.old] as [AnyObject], options: nil)?.callPromise(options: nil) else {
completion(.failure(AnyError(Web3Error(description: "Error calling \(function.name)() on \(contract.eip55String) with params: \(ERC165Hash.old)"))))
return
}
//Slower than theoretically possible because we wait for every promise to be resolved. In theory we can stop when any promise is fulfilled with true. But code is much less elegant
firstly {
when(resolved: cryptoKittyPromise, nonCryptoKittyERC721Promise, nonCryptoKittyERC721WithOldInterfaceHashPromise)
}.done { _ in
let isCryptoKitty = cryptoKittyPromise.value?["0"] as? Bool
let isNonCryptoKittyERC721 = nonCryptoKittyERC721Promise.value?["0"] as? Bool
let isNonCryptoKittyERC721WithOldInterfaceHash = nonCryptoKittyERC721WithOldInterfaceHashPromise.value?["0"] as? Bool
if let isCryptoKitty = isCryptoKitty, isCryptoKitty {
completion(.success(true))
} else if let isNonCryptoKittyERC721 = isNonCryptoKittyERC721, isNonCryptoKittyERC721 {
completion(.success(true))
} else if let isNonCryptoKittyERC721WithOldInterfaceHash = isNonCryptoKittyERC721WithOldInterfaceHash, isNonCryptoKittyERC721WithOldInterfaceHash {
completion(.success(true))
} else if isCryptoKitty != nil, isNonCryptoKittyERC721 != nil, isNonCryptoKittyERC721WithOldInterfaceHash != nil {
completion(.success(false))
} else {
completion(.failure(AnyError(Web3Error(description: "Error extracting result from \(contract.eip55String).\(function.name)()"))))
//Slower than theoretically possible because we wait for every promise to be resolved. In theory we can stop when any promise is fulfilled with true. But code is much less elegant
firstly {
when(resolved: cryptoKittyPromise, nonCryptoKittyERC721Promise, nonCryptoKittyERC721WithOldInterfaceHashPromise)
}.done(on: self.queue) { _ in
let isCryptoKitty = cryptoKittyPromise.value?["0"] as? Bool
let isNonCryptoKittyERC721 = nonCryptoKittyERC721Promise.value?["0"] as? Bool
let isNonCryptoKittyERC721WithOldInterfaceHash = nonCryptoKittyERC721WithOldInterfaceHashPromise.value?["0"] as? Bool
if let isCryptoKitty = isCryptoKitty, isCryptoKitty {
DispatchQueue.main.async {
completion(.success(true))
}
} else if let isNonCryptoKittyERC721 = isNonCryptoKittyERC721, isNonCryptoKittyERC721 {
DispatchQueue.main.async {
completion(.success(true))
}
} else if let isNonCryptoKittyERC721WithOldInterfaceHash = isNonCryptoKittyERC721WithOldInterfaceHash, isNonCryptoKittyERC721WithOldInterfaceHash {
DispatchQueue.main.async {
completion(.success(true))
}
} else if isCryptoKitty != nil, isNonCryptoKittyERC721 != nil, isNonCryptoKittyERC721WithOldInterfaceHash != nil {
DispatchQueue.main.async {
completion(.success(false))
}
} else {
DispatchQueue.main.async {
completion(.failure(AnyError(Web3Error(description: "Error extracting result from \(contract.eip55String).\(function.name)()"))))
}
}
}
}
}

@ -30,7 +30,7 @@ protocol SingleChainTokenCoordinatorDelegate: class, CanOpenURL {
func didTapSwap(forTransactionType transactionType: TransactionType, service: SwapTokenURLProviderType, in coordinator: SingleChainTokenCoordinator)
func shouldOpen(url: URL, onServer server: RPCServer, forTransactionType transactionType: TransactionType, in coordinator: SingleChainTokenCoordinator)
func didPress(for type: PaymentFlow, inCoordinator coordinator: SingleChainTokenCoordinator)
func didTap(transaction: Transaction, inViewController viewController: UIViewController, in coordinator: SingleChainTokenCoordinator)
func didTap(transaction: TransactionInstance, inViewController viewController: UIViewController, in coordinator: SingleChainTokenCoordinator)
func didPostTokenScriptTransaction(_ transaction: SentTransaction, in coordinator: SingleChainTokenCoordinator)
}
@ -109,7 +109,7 @@ class SingleChainTokenCoordinator: Coordinator {
} else {
startBlock = Config.getLastFetchedAutoDetectedTransactedTokenNonErc20BlockNumber(session.server, wallet: wallet).flatMap { $0 + 1 }
}
GetContractInteractions().getContractList(address: wallet, server: session.server, startBlock: startBlock, erc20: erc20) { [weak self] contracts, maxBlockNumber in
GetContractInteractions(queue: .main).getContractList(address: wallet, server: session.server, startBlock: startBlock, erc20: erc20) { [weak self] contracts, maxBlockNumber in
guard let strongSelf = self else { return }
defer {
seal.fulfill(())
@ -574,7 +574,7 @@ extension SingleChainTokenCoordinator: TokenViewControllerDelegate {
delegate?.didPress(for: .request, inCoordinator: self)
}
func didTap(transaction: Transaction, inViewController viewController: TokenViewController) {
func didTap(transaction: TransactionInstance, inViewController viewController: TokenViewController) {
delegate?.didTap(transaction: transaction, inViewController: viewController, in: self)
}

@ -8,7 +8,7 @@ protocol TokensCoordinatorDelegate: class, CanOpenURL {
func didTapSwap(forTransactionType transactionType: TransactionType, service: SwapTokenURLProviderType, in coordinator: TokensCoordinator)
func shouldOpen(url: URL, onServer server: RPCServer, forTransactionType transactionType: TransactionType, in coordinator: TokensCoordinator)
func didPress(for type: PaymentFlow, server: RPCServer, in coordinator: TokensCoordinator)
func didTap(transaction: Transaction, inViewController viewController: UIViewController, in coordinator: TokensCoordinator)
func didTap(transaction: TransactionInstance, inViewController viewController: UIViewController, in coordinator: TokensCoordinator)
func openConsole(inCoordinator coordinator: TokensCoordinator)
func didPostTokenScriptTransaction(_ transaction: SentTransaction, in coordinator: TokensCoordinator)
}
@ -401,7 +401,7 @@ extension TokensCoordinator: SingleChainTokenCoordinatorDelegate {
delegate?.didPress(for: type, server: coordinator.session.server, in: self)
}
func didTap(transaction: Transaction, inViewController viewController: UIViewController, in coordinator: SingleChainTokenCoordinator) {
func didTap(transaction: TransactionInstance, inViewController viewController: UIViewController, in coordinator: SingleChainTokenCoordinator) {
delegate?.didTap(transaction: transaction, inViewController: viewController, in: self)
}

@ -8,10 +8,50 @@ import web3swift
//TODO wrap callSmartContract() and cache into a type
// swiftlint:disable private_over_fileprivate
fileprivate var smartContractCallsCache = [String: (promise: Promise<[String: Any]>, timestamp: Date)]()
fileprivate var web3s = [RPCServer: [TimeInterval: web3]]()
fileprivate var smartContractCallsCache = ThreadSafeContractCallsCache()
fileprivate var web3s = ThreadSafeWeb3sCache()
// swiftlint:enable private_over_fileprivate
private class ThreadSafeWeb3sCache {
fileprivate var cache = [RPCServer: [TimeInterval: web3]]()
private let queue = DispatchQueue(label: "SynchronizedArrayAccess", attributes: .concurrent)
subscript(server: RPCServer) -> [TimeInterval: web3]? {
get {
var element: [TimeInterval: web3]?
queue.sync {
element = cache[server]
}
return element
}
set {
queue.async(flags: .barrier) {
self.cache[server] = newValue
}
}
}
}
private class ThreadSafeContractCallsCache {
fileprivate var cache = [String: (promise: Promise<[String: Any]>, timestamp: Date)]()
private let queue = DispatchQueue(label: "SynchronizedArrayAccess", attributes: .concurrent)
subscript(key: String) -> (promise: Promise<[String: Any]>, timestamp: Date)? {
get {
var element: (promise: Promise<[String: Any]>, timestamp: Date)?
queue.sync {
element = cache[key]
}
return element
}
set {
queue.async(flags: .barrier) {
self.cache[key] = newValue
}
}
}
}
func getCachedWeb3(forServer server: RPCServer, timeout: TimeInterval) throws -> web3 {
if let result = web3s[server]?[timeout] {
return result
@ -37,6 +77,7 @@ func getCachedWeb3(forServer server: RPCServer, timeout: TimeInterval) throws ->
}
}
private let callSmartContractQueue = DispatchQueue(label: "com.callSmartContractQueue.updateQueue")
func callSmartContract(withServer server: RPCServer, contract: AlphaWallet.Address, functionName: String, abiString: String, parameters: [AnyObject] = [AnyObject](), timeout: TimeInterval? = nil) -> Promise<[String: Any]> {
let timeout: TimeInterval = 60
//We must include the ABI string in the key because the order of elements in a dictionary when serialized in the string is not ordered. Parameters (which is ordered) should ensure it's the same function
@ -48,10 +89,10 @@ func callSmartContract(withServer server: RPCServer, contract: AlphaWallet.Addre
if diff < ttlForCache {
//HACK: We can't return the cachedPromise directly and immediately because if we use the value as a TokenScript attribute in a TokenScript view, timing issues will cause the webview to not load properly or for the injection with updates to fail
return Promise { seal in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
cachedPromise.done {
callSmartContractQueue.asyncAfter(deadline: .now() + 0.7) {
cachedPromise.done(on: .main) {
seal.fulfill($0)
}.catch {
}.catch(on: .main) {
seal.reject($0)
}
}
@ -60,28 +101,33 @@ func callSmartContract(withServer server: RPCServer, contract: AlphaWallet.Addre
}
let result: Promise<[String: Any]> = Promise { seal in
guard let web3 = try? getCachedWeb3(forServer: server, timeout: timeout) else {
throw Web3Error(description: "Error creating web3 for: \(server.rpcURL) + \(server.web3Network)")
}
callSmartContractQueue.async {
guard let web3 = try? getCachedWeb3(forServer: server, timeout: timeout) else {
seal.reject( Web3Error(description: "Error creating web3 for: \(server.rpcURL) + \(server.web3Network)"))
return
}
let contractAddress = EthereumAddress(address: contract)
let contractAddress = EthereumAddress(address: contract)
guard let contractInstance = web3swift.web3.web3contract(web3: web3, abiString: abiString, at: contractAddress, options: web3.options) else {
throw Web3Error(description: "Error creating web3swift contract instance to call \(functionName)()")
}
guard let promiseCreator = contractInstance.method(functionName, parameters: parameters, options: nil) else {
throw Web3Error(description: "Error calling \(contract.eip55String).\(functionName)() with parameters: \(parameters)")
}
guard let contractInstance = web3swift.web3.web3contract(web3: web3, abiString: abiString, at: contractAddress, options: web3.options) else {
seal.reject( Web3Error(description: "Error creating web3swift contract instance to call \(functionName)()"))
return
}
guard let promiseCreator = contractInstance.method(functionName, parameters: parameters, options: nil) else {
seal.reject( Web3Error(description: "Error calling \(contract.eip55String).\(functionName)() with parameters: \(parameters)"))
return
}
//callPromise() creates a promise. It doesn't "call" a promise. Bad name
promiseCreator.callPromise(options: nil).done {
seal.fulfill($0)
}.catch {
seal.reject($0)
//callPromise() creates a promise. It doesn't "call" a promise. Bad name
promiseCreator.callPromise(options: nil).done(on: .main) { d in
seal.fulfill(d)
}.catch(on: .main) { e in
seal.reject(e)
}
}
}
smartContractCallsCache[cacheKey] = (result, now)
smartContractCallsCache[cacheKey] = (result, now)
return result
}
@ -90,12 +136,13 @@ func getEventLogs(
contract: AlphaWallet.Address,
eventName: String,
abiString: String,
filter: EventFilter
filter: EventFilter,
queue: DispatchQueue
) -> Promise<[EventParserResultProtocol]> {
firstly { () -> Promise<(EthereumAddress)> in
firstly { () -> Promise<EthereumAddress> in
let contractAddress = EthereumAddress(address: contract)
return .value(contractAddress)
}.then { contractAddress -> Promise<[EventParserResultProtocol]> in
}.then(on: queue) { contractAddress -> Promise<[EventParserResultProtocol]> in
guard let web3 = try? getCachedWeb3(forServer: server, timeout: 60) else {
throw Web3Error(description: "Error creating web3 for: \(server.rpcURL) + \(server.web3Network)")
}
@ -104,9 +151,6 @@ func getEventLogs(
return Promise(error: Web3Error(description: "Error creating web3swift contract instance to call \(eventName)()"))
}
return contractInstance.getIndexedEventsPromise(
eventName: eventName,
filter: filter
)
}
return contractInstance.getIndexedEventsPromise(eventName: eventName, filter: filter)
}
}

@ -63,6 +63,23 @@ class EventActivity: Object {
self._data = EventActivity.convertJsonToDictionary(json)
}
convenience init(value: EventActivityInstance) {
self.init()
self.primaryKey = value.primaryKey
self.contract = value.contract.eip55String
self.tokenContract = value.tokenContract.eip55String
self.chainId = value.server.chainID
self.date = value.date
self.eventName = value.eventName
self.blockNumber = value.blockNumber
self.transactionId = value.transactionId
self.transactionIndex = value.transactionIndex
self.logIndex = value.logIndex
self.filter = value.filter
self.json = value.json
self._data = value._data
}
override static func primaryKey() -> String? {
return "primaryKey"
}
@ -86,3 +103,71 @@ class EventActivity: Object {
}
}
struct EventActivityInstance {
static func generatePrimaryKey(fromContract contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String, blockNumber: Int, transactionId: String, logIndex: Int, filter: String) -> String {
"\(contract.eip55String)-\(tokenContract.eip55String)-\(server.chainID)-\(eventName)-\(blockNumber)-\(transactionId)-\(logIndex)-\(filter)"
}
var primaryKey: String = ""
var contract: AlphaWallet.Address
var tokenContract: AlphaWallet.Address
var server: RPCServer
var date = Date()
var eventName: String = ""
var blockNumber: Int = 0
var transactionId: String = ""
var transactionIndex: Int = 0
var logIndex: Int = 0
var filter: String = ""
var json: String = "{}"
//Needed because Realm objects' properties (`json`) don't fire didSet after the object has been written to the database
var _data: [String: AssetInternalValue]?
init(event: EventActivity) {
self.primaryKey = event.primaryKey
self.contract = AlphaWallet.Address(uncheckedAgainstNullAddress: event.contract)!
self.tokenContract = AlphaWallet.Address(uncheckedAgainstNullAddress: event.tokenContract)!
self.server = RPCServer(chainID: event.chainId)
self.date = event.date
self.eventName = event.eventName
self.blockNumber = event.blockNumber
self.transactionId = event.transactionId
self.transactionIndex = event.transactionIndex
self.logIndex = event.logIndex
self.filter = event.filter
self.json = event.json
self._data = event._data
}
init(contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, date: Date, eventName: String, blockNumber: Int, transactionId: String, transactionIndex: Int, logIndex: Int, filter: String, json: String) {
self.primaryKey = EventActivity.generatePrimaryKey(fromContract: contract, tokenContract: tokenContract, server: server, eventName: eventName, blockNumber: blockNumber, transactionId: transactionId, logIndex: logIndex, filter: filter)
self.contract = contract
self.tokenContract = tokenContract
self.server = server
self.date = date
self.eventName = eventName
self.blockNumber = blockNumber
self.transactionId = transactionId
self.transactionIndex = transactionIndex
self.logIndex = logIndex
self.filter = filter
self.json = json
self._data = EventActivityInstance.convertJsonToDictionary(json)
}
private static func convertJsonToDictionary(_ json: String) -> [String: AssetInternalValue] {
let dict = json.data(using: .utf8).flatMap({ (try? JSONSerialization.jsonObject(with: $0, options: [])) as? [String: Any] }) ?? .init()
return Dictionary(uniqueKeysWithValues: dict.compactMap { key, value -> (String, AssetInternalValue)? in
switch value {
case let string as String:
return (key, .string(string))
case let number as NSNumber:
return (key, .string(String(describing: number)))
default:
return nil
}
})
}
}

@ -34,18 +34,19 @@ class EventInstance: Object {
}
}
convenience init(contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String, blockNumber: Int, logIndex: Int, filter: String, json: String) {
convenience init(event: EventInstanceValue) {
self.init()
self.primaryKey = EventInstance.generatePrimaryKey(fromContract: contract, tokenContract: tokenContract, server: server, eventName: eventName, blockNumber: blockNumber, logIndex: logIndex, filter: filter)
self.contract = contract.eip55String
self.tokenContract = tokenContract.eip55String
self.chainId = server.chainID
self.eventName = eventName
self.blockNumber = blockNumber
self.logIndex = logIndex
self.filter = filter
self.json = json
self._data = EventInstance.convertJsonToDictionary(json)
self.primaryKey = event.primaryKey
self.contract = event.contract
self.tokenContract = event.tokenContract
self.chainId = event.chainId
self.eventName = event.eventName
self.blockNumber = event.blockNumber
self.logIndex = event.logIndex
self.filter = event.filter
self.json = event.json
self._data = event._data
}
override static func primaryKey() -> String? {
@ -70,4 +71,3 @@ class EventInstance: Object {
})
}
}

@ -0,0 +1,62 @@
//
// EventInstanceValue.swift
// AlphaWallet
//
// Created by Vladyslav Shepitko on 13.01.2021.
//
import UIKit
struct EventInstanceValue {
var primaryKey: String
var contract: String
var tokenContract: String
var chainId: Int
var eventName: String
var blockNumber: Int
var logIndex: Int
var filter: String
var json: String
var _data: [String: AssetInternalValue]?
init(contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String, blockNumber: Int, logIndex: Int, filter: String, json: String) {
self.primaryKey = EventInstance.generatePrimaryKey(fromContract: contract, tokenContract: tokenContract, server: server, eventName: eventName, blockNumber: blockNumber, logIndex: logIndex, filter: filter)
self.contract = contract.eip55String
self.tokenContract = tokenContract.eip55String
self.chainId = server.chainID
self.eventName = eventName
self.blockNumber = blockNumber
self.logIndex = logIndex
self.filter = filter
self.json = json
self._data = EventInstanceValue.convertJsonToDictionary(json)
}
init(event: EventInstance) {
self.primaryKey = event.primaryKey
self.contract = event.contract
self.tokenContract = event.tokenContract
self.chainId = event.chainId
self.eventName = event.eventName
self.blockNumber = event.blockNumber
self.logIndex = event.logIndex
self.filter = event.filter
self.json = event.json
self._data = event._data
}
private static func convertJsonToDictionary(_ json: String) -> [String: AssetInternalValue] {
let dict = json.data(using: .utf8).flatMap({ (try? JSONSerialization.jsonObject(with: $0, options: [])) as? [String: Any] }) ?? .init()
return Dictionary(uniqueKeysWithValues: dict.compactMap { key, value -> (String, AssetInternalValue)? in
switch value {
case let string as String:
return (key, .string(string))
case let number as NSNumber:
return (key, .string(String(describing: number)))
default:
return nil
}
})
}
}

@ -33,16 +33,24 @@ class TokenCollection {
extension TokenCollection: TokensDataStoreDelegate {
func didUpdate(result: Result<TokensViewModel, TokenError>, refreshImmediately: Bool = false) {
if refreshImmediately {
notifySubscribersOfUpdatedTokens()
DispatchQueue.main.async {
self.notifySubscribersOfUpdatedTokens()
}
return
}
//The first time, we notify the subscribers and hence load the data in the UI immediately, otherwise the list of tokens in the Wallet tab will be empty for a few seconds after launch
if rateLimitedUpdater == nil {
rateLimitedUpdater = RateLimiter(limit: 2) { [weak self] in
self?.notifySubscribersOfUpdatedTokens()
DispatchQueue.main.async {
self?.notifySubscribersOfUpdatedTokens()
}
}
DispatchQueue.main.async {
self.notifySubscribersOfUpdatedTokens()
}
notifySubscribersOfUpdatedTokens()
} else {
rateLimitedUpdater?.run()
}

@ -4,6 +4,28 @@ import Foundation
import RealmSwift
import BigInt
extension Activity {
struct AssignedToken {
var primaryKey: String
var contractAddress: AlphaWallet.Address
var symbol: String
var decimals: Int
var server: RPCServer
var icon: Subscribable<TokenImage>
var type: TokenType
init(tokenObject: TokenObject) {
primaryKey = tokenObject.primaryKey
server = tokenObject.server
contractAddress = tokenObject.contractAddress
symbol = tokenObject.symbol
decimals = tokenObject.decimals
icon = tokenObject.icon
type = tokenObject.type
}
}
}
class TokenObject: Object {
static func generatePrimaryKey(fromContract contract: AlphaWallet.Address, server: RPCServer) -> String {
return "\(contract.eip55String)-\(server.chainID)"
@ -57,14 +79,9 @@ class TokenObject: Object {
self.isDisabled = isDisabled
self.type = type
}
var contractAddress: AlphaWallet.Address {
get {
AlphaWallet.Address(uncheckedAgainstNullAddress: contract)!
}
set {
contract = newValue.eip55String
}
return AlphaWallet.Address(uncheckedAgainstNullAddress: contract)!
}
var valueBigInt: BigInt {
@ -81,6 +98,7 @@ class TokenObject: Object {
override func isEqual(_ object: Any?) -> Bool {
guard let object = object as? TokenObject else { return false }
//NOTE: to improve perfomance seems like we can use check for primary key instead of checking contracts
return object.contractAddress.sameContract(as: contractAddress)
}

@ -81,6 +81,7 @@ class TokensDataStore {
private var isFetchingPrices = false
private let config: Config
private let openSea: OpenSea
private let queue = DispatchQueue.global()
let server: RPCServer
weak var delegate: TokensDataStoreDelegate?
@ -98,23 +99,23 @@ class TokensDataStore {
//TODO might be good to change `enabledObject` to just return the streaming list from Realm instead of a Swift native Array and other properties/callers can convert to Array if necessary
var enabledObject: [TokenObject] {
return Array(realm.objects(TokenObject.self)
return Array(realm.threadSafe.objects(TokenObject.self)
.filter("chainId = \(self.chainId)")
.filter("isDisabled = false"))
}
var deletedContracts: [DeletedContract] {
return Array(realm.objects(DeletedContract.self)
return Array(realm.threadSafe.objects(DeletedContract.self)
.filter("chainId = \(self.chainId)"))
}
var delegateContracts: [DelegateContract] {
return Array(realm.objects(DelegateContract.self)
return Array(realm.threadSafe.objects(DelegateContract.self)
.filter("chainId = \(self.chainId)"))
}
var hiddenContracts: [HiddenContract] {
return Array(realm.objects(HiddenContract.self)
return Array(realm.threadSafe.objects(HiddenContract.self)
.filter("chainId = \(self.chainId)"))
}
@ -437,6 +438,12 @@ class TokensDataStore {
}
}
func tokenThreadSafe(forContract contract: AlphaWallet.Address) -> TokenObject? {
realm.threadSafe.objects(TokenObject.self)
.filter("contract = '\(contract.eip55String)'")
.filter("chainId = \(chainId)").first
}
func token(forContract contract: AlphaWallet.Address) -> TokenObject? {
realm.objects(TokenObject.self)
.filter("contract = '\(contract.eip55String)'")
@ -765,7 +772,7 @@ class TokensDataStore {
case .nonFungibleBalance(let balance):
//Performance: if we use realm.write {} directly, the UI will block for a few seconds because we are reading from Realm, appending to an array and writing back to Realm many times (once for each token) in the main thread. Instead, we do this for each token in a background thread
let primaryKey = token.primaryKey
DispatchQueue.global().async {
queue.async {
let realmInBackground = try! Realm(configuration: self.realm.configuration)
let token = realmInBackground.object(ofType: TokenObject.self, forPrimaryKey: primaryKey)!
var newBalance = [TokenBalance]()
@ -859,3 +866,9 @@ class TokensDataStore {
}
}
// swiftlint:enable type_body_length
extension Realm {
var threadSafe: Realm {
try! Realm(configuration: self.configuration)
}
}

@ -16,18 +16,17 @@ class TransactionCollection {
guard let server = items.first?.server else { return [] }
guard let storage = transactionsStorages.first(where: { $0.server == server }) else { return [] }
return storage.add(items)
}
}
var objects: [Transaction] {
var transactions = [Transaction]()
var objects: [TransactionInstance] {
//Concatenate arrays of hundreds/thousands of elements. Room for speed improvement, but it seems good enough so far. It'll be much more efficient if we do a single read from Realm directly
for each in transactionsStorages {
transactions.append(contentsOf: Array(each.objects))
return transactionsStorages.flatMap {
return $0.objects.map { TransactionInstance(transaction: $0) }
}
return transactions
}
func transaction(withTransactionId transactionId: String, server: RPCServer) -> Transaction? {
func transaction(withTransactionId transactionId: String, server: RPCServer) -> TransactionInstance? {
guard let storage = transactionsStorages.first(where: { $0.server == server }) else { return nil }
return storage.transaction(withTransactionId: transactionId)
}

@ -10,7 +10,7 @@ protocol TokenViewControllerDelegate: class, CanOpenURL {
func shouldOpen(url: URL, onServer server: RPCServer, forTransactionType transactionType: TransactionType, inViewController viewController: TokenViewController)
func didTapSend(forTransactionType transactionType: TransactionType, inViewController viewController: TokenViewController)
func didTapReceive(forTransactionType transactionType: TransactionType, inViewController viewController: TokenViewController)
func didTap(transaction: Transaction, inViewController viewController: TokenViewController)
func didTap(transaction: TransactionInstance, inViewController viewController: TokenViewController)
func didTap(action: TokenInstanceAction, transactionType: TransactionType, viewController: TokenViewController)
}

@ -4,11 +4,11 @@ import Foundation
import UIKit
struct TokenViewControllerTransactionCellViewModel {
private let transaction: Transaction
private let transaction: TransactionInstance
private let transactionViewModel: TransactionViewModel
init(
transaction: Transaction,
transaction: TransactionInstance,
config: Config,
chainState: ChainState,
currentWallet: Wallet

@ -25,7 +25,7 @@ struct TokenViewControllerViewModel {
}
}
let recentTransactions: [Transaction]
let recentTransactions: [TransactionInstance]
var actions: [TokenInstanceAction] {
guard let token = token else { return [] }
@ -124,22 +124,31 @@ struct TokenViewControllerViewModel {
switch transactionType {
case .nativeCryptocurrency:
self.recentTransactions = Array(transactionsStore.objects.lazy
.filter({ $0.state == .completed || $0.state == .pending })
.filter({ $0.operation == nil })
.filter({ $0.value != "" && $0.value != "0" })
.prefix(3))
.filter({ TokenViewControllerViewModel.filterTransactionsForNativeCryptocurrency(transaction: $0) })
.prefix(3)
.map { TransactionInstance(transaction: $0) })
case .ERC20Token(let token, _, _):
self.recentTransactions = Array(transactionsStore.objects.lazy
.filter({ $0.state == .completed || $0.state == .pending })
.filter({
$0.localizedOperations.contains(where: { op in op.operationType == .erc20TokenTransfer && (op.contract.flatMap({ token.contractAddress.sameContract(as: $0) }) ?? false) })
})
.filter({ TokenViewControllerViewModel.filterTransactionsForERC20Token(transaction: $0, tokenObject: token) })
.prefix(3))
.map { TransactionInstance(transaction: $0) }
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .dapp, .tokenScript, .claimPaidErc875MagicLink:
self.recentTransactions = []
}
}
private static func filterTransactionsForNativeCryptocurrency(transaction: Transaction) -> Bool {
(transaction.state == .completed || transaction.state == .pending) && (transaction.operation == nil) && (transaction.value != "" && transaction.value != "0")
}
private static func filterTransactionsForERC20Token(transaction: Transaction, tokenObject token: TokenObject) -> Bool {
(transaction.state == .completed || transaction.state == .pending) && transaction.localizedOperations.contains(where: { op in
op.operationType == .erc20TokenTransfer && (op.contract.flatMap({ token.contractAddress.sameContract(as: $0) }) ?? false)
})
}
var destinationAddress: AlphaWallet.Address {
return transactionType.contract
}

@ -47,7 +47,10 @@ class OpenSeaNonFungibleTokenViewCell: UICollectionViewCell {
override func layoutSubviews() {
super.layoutSubviews()
setupParallaxEffect(forView: imageView, max: 20)
DispatchQueue.main.async {
self.setupParallaxEffect(forView: self.imageView, max: 20)
}
}
func configure(viewModel: OpenSeaNonFungibleTokenViewCellViewModel) {

@ -11,6 +11,7 @@ protocol SingleChainTransactionDataCoordinator: Coordinator {
var delegate: SingleChainTransactionDataCoordinatorDelegate? { get set }
var session: WalletSession { get }
func start()
func stopTimers()
func runScheduledTimers()

@ -11,11 +11,12 @@ import UserNotifications
class SingleChainTransactionEtherscanDataCoordinator: SingleChainTransactionDataCoordinator {
private let storage: TransactionsStorage
private let session: WalletSession
let session: WalletSession
private let keystore: Keystore
private let tokensStorage: TokensDataStore
private let promptBackupCoordinator: PromptBackupCoordinator
private let fetchLatestTransactionsQueue: OperationQueue
private let queue = DispatchQueue(label: "com.SingleChainTransaction.updateQueue")
private var timer: Timer?
private var updateTransactionsTimer: Timer?
private lazy var transactionsTracker: TransactionsTracker = {
@ -23,7 +24,8 @@ class SingleChainTransactionEtherscanDataCoordinator: SingleChainTransactionData
}()
private let alphaWalletProvider = AlphaWalletProviderFactory.makeProvider()
var isFetchingLatestTransactions = false
private var isAutoDetectingERC20Transactions: Bool = false
private var isFetchingLatestTransactions = false
var coordinators: [Coordinator] = []
weak var delegate: SingleChainTransactionDataCoordinatorDelegate?
@ -62,52 +64,65 @@ class SingleChainTransactionEtherscanDataCoordinator: SingleChainTransactionData
guard timer == nil, updateTransactionsTimer == nil else {
return
}
timer = Timer.scheduledTimer(timeInterval: 5, target: BlockOperation { [weak self] in
self?.fetchPending()
guard let strongSelf = self else { return }
strongSelf.queue.async {
strongSelf.fetchPendingTransactions()
}
}, selector: #selector(Operation.main), userInfo: nil, repeats: true)
updateTransactionsTimer = Timer.scheduledTimer(timeInterval: 15, target: BlockOperation { [weak self] in
self?.fetchLatestTransactions()
self?.autoDetectERC20Transactions()
}, selector: #selector(Operation.main), userInfo: nil, repeats: true)
}
guard let strongSelf = self else { return }
private func fetchPending() {
fetchPendingTransactions()
strongSelf.queue.async {
strongSelf.fetchLatestTransactions()
strongSelf.autoDetectERC20Transactions()
}
}, selector: #selector(Operation.main), userInfo: nil, repeats: true)
}
//TODO should this be added to the queue?
private func autoDetectERC20Transactions() {
guard !isAutoDetectingERC20Transactions else { return }
isAutoDetectingERC20Transactions = true
let server = session.server
let wallet = session.account.address
let startBlock = Config.getLastFetchedErc20InteractionBlockNumber(session.server, wallet: wallet).flatMap { $0 + 1 }
GetContractInteractions().getErc20Interactions(
address: wallet,
server: session.server,
startBlock: startBlock
) { [weak self] result in
GetContractInteractions(queue: self.queue).getErc20Interactions(address: wallet, server: server, startBlock: startBlock) { [weak self] result in
guard let strongSelf = self else { return }
let blockNumbers = result.map(\.blockNumber)
if let minBlockNumber = blockNumbers.min(), let maxBlockNumber = blockNumbers.max() {
firstly {
strongSelf.backFillErc20TransactionGroup(result, startBlock: minBlockNumber, endBlock: maxBlockNumber)
}.done { backFilledTransactions in
Config.setLastFetchedErc20InteractionBlockNumber(maxBlockNumber, server: strongSelf.session.server, wallet: wallet)
}.done(on: strongSelf.queue) { backFilledTransactions in
Config.setLastFetchedErc20InteractionBlockNumber(maxBlockNumber, server: server, wallet: wallet)
strongSelf.update(items: backFilledTransactions)
}.cauterize()
.finally {
strongSelf.isAutoDetectingERC20Transactions = false
}
} else {
strongSelf.isAutoDetectingERC20Transactions = false
strongSelf.update(items: result)
}
}
}
private func backFillErc20TransactionGroup(_ transactionsToFill: [Transaction], startBlock: Int, endBlock: Int) -> Promise<[Transaction]> {
private func backFillErc20TransactionGroup(_ transactionsToFill: [TransactionInstance], startBlock: Int, endBlock: Int) -> Promise<[TransactionInstance]> {
return firstly {
fetchTransactions(for: session.account.address, startBlock: startBlock, endBlock: endBlock, sortOrder: .asc)
}.map { fillerTransactions -> [Transaction] in
var results: [Transaction] = .init()
}.map(on: self.queue) { fillerTransactions -> [TransactionInstance] in
var results: [TransactionInstance] = .init()
for each in transactionsToFill {
//ERC20 transactions are expected to have operations because of the API we use to retrieve them from
guard !each.localizedOperations.isEmpty else { continue }
if let transaction = fillerTransactions.first(where: { $0.blockNumber == each.blockNumber }) {
if var transaction = fillerTransactions.first(where: { $0.blockNumber == each.blockNumber }) {
transaction.isERC20Interaction = true
transaction.localizedOperations = each.localizedOperations
results.append(transaction)
@ -120,94 +135,116 @@ class SingleChainTransactionEtherscanDataCoordinator: SingleChainTransactionData
}
func fetch() {
session.refresh(.balance)
fetchLatestTransactions()
fetchPendingTransactions()
self.queue.async {
DispatchQueue.main.async {
self.session.refresh(.balance)
}
self.fetchLatestTransactions()
self.fetchPendingTransactions()
}
}
private func update(items: [Transaction]) {
filterTransactionsToPullContractsFrom(items).done { transactionsToPullContractsFrom, contractsAndTokenTypes in
private func update(items: [TransactionInstance]) {
guard !items.isEmpty else { return }
filterTransactionsToPullContractsFrom(items).done(on: self.queue, { transactionsToPullContractsFrom, contractsAndTokenTypes in
self.storage.add(transactions: items, transactionsToPullContractsFrom: transactionsToPullContractsFrom, contractsAndTokenTypes: contractsAndTokenTypes)
self.delegate?.handleUpdateItems(inCoordinator: self)
}.cauterize()
}).cauterize()
}
private func filterTransactionsToPullContractsFrom(_ transactions: [Transaction]) -> Promise<(transactions: [Transaction], contractTypes: [AlphaWallet.Address: TokenType])> {
private var contractsToAvoid: [AlphaWallet.Address] {
let alreadyAddedContracts = tokensStorage.enabledObject.map { $0.contractAddress }
let deletedContracts = tokensStorage.deletedContracts.map { $0.contractAddress }
let hiddenContracts = tokensStorage.hiddenContracts.map { $0.contractAddress }
let delegateContracts = tokensStorage.delegateContracts.map { $0.contractAddress }
let contractsToAvoid = alreadyAddedContracts + deletedContracts + hiddenContracts + delegateContracts
let filteredTransactions = transactions.filter {
if let toAddressToCheck = AlphaWallet.Address(string: $0.to) {
if contractsToAvoid.contains(toAddressToCheck) {
return alreadyAddedContracts + deletedContracts + hiddenContracts + delegateContracts
}
private func filterTransactionsToPullContractsFrom(_ transactions: [TransactionInstance]) -> Promise<(transactions: [TransactionInstance], contractTypes: [AlphaWallet.Address: TokenType])> {
return Promise { seal in
let contractsToAvoid = self.contractsToAvoid
let filteredTransactions = transactions.filter {
if let toAddressToCheck = AlphaWallet.Address(string: $0.to), contractsToAvoid.contains(toAddressToCheck) {
return false
}
}
if let contractAddressToCheck = $0.operation?.contractAddress {
if contractsToAvoid.contains(contractAddressToCheck) {
if let contractAddressToCheck = $0.operation?.contractAddress, contractsToAvoid.contains(contractAddressToCheck) {
return false
}
return true
}
return true
}
//The fetch ERC20 transactions endpoint from Etherscan returns only ERC20 token transactions but the Blockscout version also includes ERC721 transactions too (so it's likely other types that it can detect will be returned too); thus we check the token type rather than assume that they are all ERC20
switch session.server {
case .xDai, .poa:
let contracts = Array(Set(filteredTransactions.compactMap { $0.localizedOperations.first?.contractAddress }))
let tokenTypePromises = contracts.map { tokensStorage.getTokenType(for: $0) }
return when(fulfilled: tokenTypePromises).map { tokenTypes in
let contractsToTokenTypes = Dictionary(uniqueKeysWithValues: zip(contracts, tokenTypes))
return (transactions: filteredTransactions, contractTypes: contractsToTokenTypes)
//The fetch ERC20 transactions endpoint from Etherscan returns only ERC20 token transactions but the Blockscout version also includes ERC721 transactions too (so it's likely other types that it can detect will be returned too); thus we check the token type rather than assume that they are all ERC20
switch self.session.server {
case .xDai, .poa:
let contracts = Array(Set(filteredTransactions.compactMap { $0.localizedOperations.first?.contractAddress }))
let tokenTypePromises = contracts.map { self.tokensStorage.getTokenType(for: $0) }
when(fulfilled: tokenTypePromises).map { tokenTypes in
let contractsToTokenTypes = Dictionary(uniqueKeysWithValues: zip(contracts, tokenTypes))
return (transactions: filteredTransactions, contractTypes: contractsToTokenTypes)
}.done { val in
seal.fulfill(val)
}.catch { error in
seal.reject(error)
}
case .main, .classic, .kovan, .ropsten, .rinkeby, .sokol, .callisto, .goerli, .artis_sigma1, .artis_tau1, .binance_smart_chain, .binance_smart_chain_testnet, .custom, .heco, .heco_testnet:
seal.fulfill((transactions: filteredTransactions, contractTypes: .init()))
}
case .main, .classic, .kovan, .ropsten, .rinkeby, .sokol, .callisto, .goerli, .artis_sigma1, .artis_tau1, .binance_smart_chain, .binance_smart_chain_testnet, .custom, .heco, .heco_testnet:
return .value((transactions: filteredTransactions, contractTypes: .init()))
}
}
private func fetchPendingTransactions() {
storage.pendingObjects.forEach { updatePendingTransaction($0) }
storage.pendingObjects.forEach {
self.updatePendingTransaction($0)
}
}
private func updatePendingTransaction(_ transaction: Transaction) {
private func updatePendingTransaction(_ transaction: TransactionInstance) {
let request = GetTransactionRequest(hash: transaction.id)
firstly {
Session.send(EtherServiceRequest(server: session.server, batch: BatchFactory().create(request)))
}.done { _ in
if transaction.date > Date().addingTimeInterval(TransactionDataCoordinator.delayedTransactionInternalSeconds) {
self.update(state: .completed, for: transaction)
//NOTE: We dont want to call function handleUpdateItems: twice because it will be updated in update(items:
self.update(state: .completed, for: transaction, shouldUpdateItems: false)
self.update(items: [transaction])
}
}.catch { error in
guard let error = error as? SessionTaskError else { return }
switch error {
switch error as? SessionTaskError {
case .responseError(let error):
// TODO: Think about the logic to handle pending transactions.
guard let error = error as? JSONRPCError else { return }
switch error {
switch error as? JSONRPCError {
case .responseError:
self.delete(transactions: [transaction])
case .resultObjectParseError:
if transaction.date > Date().addingTimeInterval(TransactionDataCoordinator.deleteMissingInternalSeconds) {
self.update(state: .failed, for: transaction)
}
case .responseNotFound, .errorObjectParseError, .unsupportedVersion, .unexpectedTypeObject, .missingBothResultAndError, .nonArrayResponse:
case .responseNotFound, .errorObjectParseError, .unsupportedVersion, .unexpectedTypeObject, .missingBothResultAndError, .nonArrayResponse, .none:
break
}
case .connectionError, .requestError:
case .connectionError, .requestError, .none:
break
}
}
}
private func delete(transactions: [Transaction]) {
storage.delete(transactions)
delegate?.handleUpdateItems(inCoordinator: self)
private func delete(transactions: [TransactionInstance]) {
storage.delete(transactions: transactions).done(on: self.queue, { _ in
self.delegate?.handleUpdateItems(inCoordinator: self)
}).cauterize()
}
private func update(state: TransactionState, for transaction: Transaction) {
storage.update(state: state, for: transaction)
delegate?.handleUpdateItems(inCoordinator: self)
private func update(state: TransactionState, for transaction: TransactionInstance, shouldUpdateItems: Bool = true) {
storage.update(state: state, for: transaction.primaryKey).done(on: self.queue, { _ in
guard shouldUpdateItems else { return }
self.delegate?.handleUpdateItems(inCoordinator: self)
}).cauterize()
}
///Fetching transactions might take a long time, we use a flag to make sure we only pull the latest transactions 1 "page" at a time, otherwise we'd end up pulling the same "page" multiple times
@ -215,16 +252,20 @@ class SingleChainTransactionEtherscanDataCoordinator: SingleChainTransactionData
guard !isFetchingLatestTransactions else { return }
isFetchingLatestTransactions = true
let value = storage.transactionObjectsThatDoNotComeFromEventLogs()
let startBlock: Int
let sortOrder: AlphaWalletService.SortOrder
if let newestCachedTransaction = storage.transactionObjectsThatDoNotComeFromEventLogs.first {
if let newestCachedTransaction = value {
startBlock = newestCachedTransaction.blockNumber + 1
sortOrder = .asc
} else {
startBlock = 1
sortOrder = .desc
}
let operation = FetchLatestTransactionsOperation(forSession: session, coordinator: self, startBlock: startBlock, sortOrder: sortOrder)
let operation = FetchLatestTransactionsOperation(forSession: session, coordinator: self, startBlock: startBlock, sortOrder: sortOrder, queue: self.queue)
fetchLatestTransactionsQueue.addOperation(operation)
}
@ -234,47 +275,56 @@ class SingleChainTransactionEtherscanDataCoordinator: SingleChainTransactionData
}
//TODO notify user of received tokens too
private func notifyUserEtherReceived(inNewTransactions transactions: [Transaction]) {
private func notifyUserEtherReceived(inNewTransactions transactions: [TransactionInstance]) {
guard !transactions.isEmpty else { return }
let wallet = keystore.currentWallet
var toNotify: [Transaction]
if let newestCached = storage.objects.first {
let objects = storage.transactions
var toNotify: [TransactionInstance]
if let newestCached = objects.first {
toNotify = transactions.filter { $0.blockNumber > newestCached.blockNumber }
} else {
toNotify = transactions
}
//Beyond a certain number, it's too noisy and a performance nightmare. Eg. the first time we fetch transactions for a newly imported wallet, we might get 10,000 of them
let maximumNumberOfNotifications = 10
if toNotify.count > maximumNumberOfNotifications {
toNotify = Array(toNotify[0..<maximumNumberOfNotifications])
}
let toNotifyUnique: [Transaction] = filterUniqueTransactions(toNotify)
let toNotifyUnique: [TransactionInstance] = filterUniqueTransactions(toNotify)
let newIncomingEthTransactions = toNotifyUnique.filter { wallet.address.sameContract(as: $0.to) }
let formatter = EtherNumberFormatter.short
let thresholdToShowNotification = Date.yesterday
for each in newIncomingEthTransactions {
let amount = formatter.string(from: BigInt(each.value) ?? BigInt(), decimals: 18)
if each.date > thresholdToShowNotification {
notifyUserEtherReceived(for: each.id, amount: amount)
self.notifyUserEtherReceived(for: each.id, amount: amount)
}
}
let etherReceivedUsedForBackupPrompt = newIncomingEthTransactions
.last { wallet.address.sameContract(as: $0.to) }
.flatMap { BigInt($0.value) }
switch session.server {
//TODO make this work for other mainnets
case .main:
etherReceivedUsedForBackupPrompt.flatMap { promptBackupCoordinator.showCreateBackupAfterReceiveNativeCryptoCurrencyPrompt(nativeCryptoCurrency: $0) }
etherReceivedUsedForBackupPrompt.flatMap {
self.promptBackupCoordinator.showCreateBackupAfterReceiveNativeCryptoCurrencyPrompt(nativeCryptoCurrency: $0)
}
case .classic, .xDai:
break
case .kovan, .ropsten, .rinkeby, .poa, .sokol, .callisto, .goerli, .artis_sigma1, .artis_tau1, .binance_smart_chain, .binance_smart_chain_testnet, .custom, .heco, .heco_testnet:
break
}
}
//Etherscan for Ropsten returns the same transaction twice. Normally Realm will take care of this, but since we are showing user a notification, we don't want to show duplicates
private func filterUniqueTransactions(_ transactions: [Transaction]) -> [Transaction] {
var results = [Transaction]()
private func filterUniqueTransactions(_ transactions: [TransactionInstance]) -> [TransactionInstance] {
var results = [TransactionInstance]()
for each in transactions {
if !results.contains(where: { each.id == $0.id }) {
results.append(each)
@ -295,10 +345,13 @@ class SingleChainTransactionEtherscanDataCoordinator: SingleChainTransactionData
content.sound = .default
let identifier = Constants.etherReceivedNotificationIdentifier
let request = UNNotificationRequest(identifier: "\(identifier):\(transactionId)", content: content, trigger: nil)
notificationCenter.add(request)
DispatchQueue.main.async {
notificationCenter.add(request)
}
}
private func fetchTransactions(for address: AlphaWallet.Address, startBlock: Int, endBlock: Int = 999_999_999, sortOrder: AlphaWalletService.SortOrder) -> Promise<[Transaction]> {
private func fetchTransactions(for address: AlphaWallet.Address, startBlock: Int, endBlock: Int = 999_999_999, sortOrder: AlphaWalletService.SortOrder) -> Promise<[TransactionInstance]> {
return alphaWalletProvider.request(.getTransactions(
config: session.config,
@ -307,12 +360,13 @@ class SingleChainTransactionEtherscanDataCoordinator: SingleChainTransactionData
startBlock: startBlock,
endBlock: endBlock,
sortOrder: sortOrder
)).map {
))
.map(on: self.queue) {
try $0.map(ArrayResponse<RawTransaction>.self).result.map {
Transaction.from(transaction: $0, tokensStorage: self.tokensStorage)
TransactionInstance.from(transaction: $0, tokensStorage: self.tokensStorage)
}
}.then {
when(fulfilled: $0).compactMap {
}.then(on: self.queue) {
when(fulfilled: $0).compactMap(on: self.queue) {
$0.compactMap { $0 }
}
}
@ -322,19 +376,23 @@ class SingleChainTransactionEtherscanDataCoordinator: SingleChainTransactionData
guard let oldestCachedTransaction = storage.completedObjects.last else { return }
let promise = fetchTransactions(for: address, startBlock: 1, endBlock: oldestCachedTransaction.blockNumber - 1, sortOrder: .desc)
promise.done { [weak self] transactions in
self?.update(items: transactions)
promise.done(on: self.queue, { [weak self] transactions in
guard let strongSelf = self else { return }
if !transactions.isEmpty {
strongSelf.update(items: transactions)
if transactions.isEmpty {
strongSelf.transactionsTracker.fetchingState = .done
} else {
let timeout = DispatchTime.now() + .milliseconds(300)
DispatchQueue.main.asyncAfter(deadline: timeout) { [weak self] in
self?.fetchOlderTransactions(for: address)
strongSelf.queue.asyncAfter(deadline: timeout) {
strongSelf.fetchOlderTransactions(for: address)
}
} else {
self?.transactionsTracker.fetchingState = .done
}
}.catch { [weak self] _ in
self?.transactionsTracker.fetchingState = .failed
}).catch(on: self.queue) { [weak self] _ in
guard let strongSelf = self else { return }
strongSelf.transactionsTracker.fetchingState = .failed
}
}
@ -365,31 +423,38 @@ class SingleChainTransactionEtherscanDataCoordinator: SingleChainTransactionData
override var isAsynchronous: Bool {
return true
}
private let queue: DispatchQueue
init(forSession session: WalletSession, coordinator: SingleChainTransactionEtherscanDataCoordinator, startBlock: Int, sortOrder: AlphaWalletService.SortOrder) {
init(forSession session: WalletSession, coordinator: SingleChainTransactionEtherscanDataCoordinator, startBlock: Int, sortOrder: AlphaWalletService.SortOrder, queue: DispatchQueue) {
self.session = session
self.coordinator = coordinator
self.startBlock = startBlock
self.sortOrder = sortOrder
self.queue = queue
super.init()
self.queuePriority = session.server.networkRequestsQueuePriority
}
override func main() {
guard let coordinator = self.coordinator else { return }
firstly {
coordinator.fetchTransactions(for: session.account.address, startBlock: startBlock, sortOrder: sortOrder)
}.done { transactions in
}.done(on: queue, { transactions in
coordinator.notifyUserEtherReceived(inNewTransactions: transactions)
coordinator.update(items: transactions)
}.catch { e in
}).catch { e in
coordinator.handleError(error: e)
}.finally { [weak self] in
self?.willChangeValue(forKey: "isExecuting")
self?.willChangeValue(forKey: "isFinished")
guard let strongSelf = self else { return }
strongSelf.willChangeValue(forKey: "isExecuting")
strongSelf.willChangeValue(forKey: "isFinished")
coordinator.isFetchingLatestTransactions = false
self?.didChangeValue(forKey: "isExecuting")
self?.didChangeValue(forKey: "isFinished")
strongSelf.didChangeValue(forKey: "isExecuting")
strongSelf.didChangeValue(forKey: "isFinished")
}
}
}

@ -125,4 +125,4 @@ extension TransactionCoordinator: CanOpenURL {
}
extension TransactionCoordinator: TransactionViewControllerDelegate {
}
}

@ -8,7 +8,7 @@ enum TransactionError: Error {
}
protocol TransactionDataCoordinatorDelegate: class {
func didUpdate(result: ResultResult<[Transaction], TransactionError>.t, reloadImmediately: Bool)
func didUpdate(result: ResultResult<[TransactionInstance], TransactionError>.t, reloadImmediately: Bool)
}
class TransactionDataCoordinator: Coordinator {
@ -72,11 +72,10 @@ class TransactionDataCoordinator: Coordinator {
for each in singleChainTransactionDataCoordinators {
each.start()
}
//Since start() is called at launch, and user don't see the Transactions tab immediately, we don't want it to block launching
DispatchQueue.global().async {
DispatchQueue.main.async { [weak self] in
self?.handleUpdateItems(reloadImmediately: false)
}
self.handleUpdateItems(reloadImmediately: false)
}
}
@ -87,11 +86,8 @@ class TransactionDataCoordinator: Coordinator {
}
@objc private func restartTimers() {
runScheduledTimers()
}
private func runScheduledTimers() {
guard !config.isAutoFetchingDisabled else { return }
for each in singleChainTransactionDataCoordinators {
each.runScheduledTimers()
}
@ -99,6 +95,7 @@ class TransactionDataCoordinator: Coordinator {
func fetch() {
guard !config.isAutoFetchingDisabled else { return }
for each in singleChainTransactionDataCoordinators {
each.fetch()
}
@ -110,6 +107,7 @@ class TransactionDataCoordinator: Coordinator {
let tokensDataStore = tokensStorages[transaction.original.server]
let transaction = Transaction.from(from: session.account.address, transaction: transaction, tokensDataStore: tokensDataStore)
transactionCollection.add([transaction])
handleUpdateItems(reloadImmediately: true)
}
@ -124,13 +122,15 @@ class TransactionDataCoordinator: Coordinator {
}
private func handleUpdateItems(reloadImmediately: Bool) {
delegate?.didUpdate(result: .success(transactionCollection.objects), reloadImmediately: reloadImmediately)
delegate2?.didUpdate(result: .success(transactionCollection.objects), reloadImmediately: reloadImmediately)
let objects = transactionCollection.objects
delegate?.didUpdate(result: .success(objects), reloadImmediately: reloadImmediately)
delegate2?.didUpdate(result: .success(objects), reloadImmediately: reloadImmediately)
}
}
extension TransactionDataCoordinator: SingleChainTransactionDataCoordinatorDelegate {
func handleUpdateItems(inCoordinator: SingleChainTransactionDataCoordinator) {
func handleUpdateItems(inCoordinator coordinator: SingleChainTransactionDataCoordinator) {
handleUpdateItems(reloadImmediately: false)
}
}

@ -39,7 +39,6 @@ class Transaction: Object {
state: TransactionState,
isErc20Interaction: Bool
) {
self.init()
self.primaryKey = "\(id)-\(server.chainID)"
self.id = id
@ -65,6 +64,34 @@ class Transaction: Object {
self.localizedOperations = list
}
convenience init(object: TransactionInstance) {
self.init()
self.primaryKey = object.primaryKey
self.id = object.id
self.chainId = object.server.chainID
self.blockNumber = object.blockNumber
self.transactionIndex = object.transactionIndex
self.from = object.from
self.to = object.to
self.value = object.value
self.gas = object.gas
self.gasPrice = object.gasPrice
self.gasUsed = object.gasUsed
self.nonce = object.nonce
self.date = object.date
self.internalState = object.state.rawValue
self.isERC20Interaction = object.isERC20Interaction
let list = List<LocalizedOperationObject>()
object.localizedOperations.forEach { element in
let value = LocalizedOperationObject(object: element)
list.append(value)
}
self.localizedOperations = list
}
override static func primaryKey() -> String? {
return "primaryKey"
}
@ -87,6 +114,7 @@ extension Transaction {
extension Transaction {
static func from(from: AlphaWallet.Address, transaction: SentTransaction, tokensDataStore: TokensDataStore) -> Transaction {
let (operations: operations, isErc20Interaction: isErc20Interaction) = decodeOperations(fromData: transaction.original.data, from: transaction.original.account, contractOrRecipient: transaction.original.to, tokensDataStore: tokensDataStore)
return Transaction(
id: transaction.id,
server: transaction.original.server,
@ -108,7 +136,7 @@ extension Transaction {
//TODO add support for more types of pending transactions
fileprivate static func decodeOperations(fromData data: Data, from: AlphaWallet.Address, contractOrRecipient: AlphaWallet.Address?, tokensDataStore: TokensDataStore) -> (operations: [LocalizedOperationObject], isErc20Interaction: Bool) {
if let functionCallMetaData = DecodedFunctionCall(data: data), let contract = contractOrRecipient, let token = tokensDataStore.token(forContract: contract) {
if let functionCallMetaData = DecodedFunctionCall(data: data), let contract = contractOrRecipient, let token = tokensDataStore.tokenThreadSafe(forContract: contract) {
switch functionCallMetaData.type {
case .erc20Transfer(let recipient, let value):
return (operations: [LocalizedOperationObject(from: from.eip55String, to: recipient.eip55String, contract: contract, type: OperationType.erc20TokenTransfer.rawValue, value: String(value), symbol: token.symbol, name: token.name, decimals: token.decimals)], isErc20Interaction: true)
@ -118,4 +146,92 @@ extension Transaction {
}
return (operations: .init(), isErc20Interaction: false)
}
}
}
struct TransactionInstance {
var primaryKey: String = ""
var chainId: Int = 0
var id: String = ""
var blockNumber: Int = 0
var transactionIndex: Int = 0
var from = ""
var to = ""
var value = ""
var gas = ""
var gasPrice = ""
var gasUsed = ""
var nonce: String = ""
var date = Date()
var internalState: Int = TransactionState.completed.rawValue
var isERC20Interaction: Bool = false
var localizedOperations: [LocalizedOperationObjectInstance] = []
init(
id: String,
server: RPCServer,
blockNumber: Int,
transactionIndex: Int,
from: String,
to: String,
value: String,
gas: String,
gasPrice: String,
gasUsed: String,
nonce: String,
date: Date,
localizedOperations: [LocalizedOperationObjectInstance],
state: TransactionState,
isErc20Interaction: Bool
) {
self.primaryKey = "\(id)-\(server.chainID)"
self.id = id
self.chainId = server.chainID
self.blockNumber = blockNumber
self.transactionIndex = transactionIndex
self.from = from
self.to = to
self.value = value
self.gas = gas
self.gasPrice = gasPrice
self.gasUsed = gasUsed
self.nonce = nonce
self.date = date
self.internalState = state.rawValue
self.isERC20Interaction = isErc20Interaction
self.localizedOperations = localizedOperations
}
init(transaction: Transaction) {
self.primaryKey = transaction.primaryKey
self.id = transaction.id
self.chainId = transaction.server.chainID
self.blockNumber = transaction.blockNumber
self.transactionIndex = transaction.transactionIndex
self.from = transaction.from
self.to = transaction.to
self.value = transaction.value
self.gas = transaction.gas
self.gasPrice = transaction.gasPrice
self.gasUsed = transaction.gasUsed
self.nonce = transaction.nonce
self.date = transaction.date
self.internalState = transaction.state.rawValue
self.isERC20Interaction = transaction.isERC20Interaction
self.localizedOperations = transaction.localizedOperations.map {
LocalizedOperationObjectInstance(object: $0)
}
}
var state: TransactionState {
return TransactionState(int: internalState)
}
var operation: LocalizedOperationObjectInstance? {
return localizedOperations.first
}
var server: RPCServer {
return .init(chainID: chainId)
}
}

@ -1,5 +1,6 @@
import Foundation
import RealmSwift
import PromiseKit
protocol TransactionsStorageDelegate: class {
func didAddTokensWith(contracts: [AlphaWallet.Address], inTransactionsStorage: TransactionsStorage)
@ -21,35 +22,35 @@ class TransactionsStorage {
return objects.count
}
var objects: [Transaction] {
return Array(realm.objects(Transaction.self)
.sorted(byKeyPath: "date", ascending: false)
.filter("chainId = \(self.server.chainID)")
.filter("id != ''"))
var objects: Results<Transaction> {
realm.threadSafe.objects(Transaction.self)
.sorted(byKeyPath: "date", ascending: false)
.filter("chainId = \(self.server.chainID)")
.filter("id != ''")
}
var completedObjects: [Transaction] {
return objects.filter { $0.state == .completed }
objects.filter { $0.state == .completed }
}
var transactionObjectsThatDoNotComeFromEventLogs: Results<Transaction> {
return realm.objects(Transaction.self)
.sorted(byKeyPath: "date", ascending: false)
.filter("chainId = \(self.server.chainID)")
.filter("id != ''")
.filter("internalState == \(TransactionState.completed.rawValue)")
.filter("isERC20Interaction == false")
var pendingObjects: [TransactionInstance] {
objects.filter { $0.state == TransactionState.pending }.map { TransactionInstance(transaction: $0) }
}
var pendingObjects: [Transaction] {
return objects.filter { $0.state == TransactionState.pending }
func transaction(withTransactionId transactionId: String) -> TransactionInstance? {
realm.threadSafe.objects(Transaction.self)
.filter("id = '\(transactionId)'")
.filter("chainId = \(server.chainID)")
.map { TransactionInstance(transaction: $0) }
.first
}
func transaction(withTransactionId transactionId: String) -> Transaction? {
realm.objects(Transaction.self)
.filter("id = '\(transactionId)'")
.filter("chainId = \(server.chainID)")
.first
private func addTokensWithContractAddresses(fromTransactions transactions: [Transaction], contractsAndTokenTypes: [AlphaWallet.Address: TokenType], realm: Realm) {
let tokens = self.tokens(from: transactions, contractsAndTokenTypes: contractsAndTokenTypes)
delegate?.didAddTokensWith(contracts: Array(Set(tokens.map { $0.address })), inTransactionsStorage: self)
if !tokens.isEmpty {
TokensDataStore.update(in: realm, tokens: tokens)
}
}
private func addTokensWithContractAddresses(fromTransactions transactions: [Transaction], contractsAndTokenTypes: [AlphaWallet.Address: TokenType]) {
@ -62,15 +63,100 @@ class TransactionsStorage {
func add(transactions: [Transaction], transactionsToPullContractsFrom: [Transaction], contractsAndTokenTypes: [AlphaWallet.Address: TokenType]) {
guard !transactions.isEmpty else { return }
let transactionsToCommit = filterTransactionsToNotOverrideERC20Transactions(transactions)
let transactionsToCommit = filterTransactionsToNotOverrideERC20Transactions(transactions, realm: realm)
realm.beginWrite()
realm.add(transactionsToCommit, update: .all)
try! realm.commitWrite()
addTokensWithContractAddresses(fromTransactions: transactionsToPullContractsFrom, contractsAndTokenTypes: contractsAndTokenTypes)
}
func transactionObjectsThatDoNotComeFromEventLogs() -> TransactionInstance? {
return realm.threadSafe.objects(Transaction.self)
.sorted(byKeyPath: "date", ascending: false)
.filter("chainId = \(self.server.chainID)")
.filter("id != ''")
.filter("internalState == \(TransactionState.completed.rawValue)")
.filter("isERC20Interaction == false")
.map { TransactionInstance(transaction: $0) }
.first
}
var transactions: [TransactionInstance] {
realm.threadSafe.objects(Transaction.self)
.sorted(byKeyPath: "date", ascending: false)
.filter("chainId = \(self.server.chainID)")
.filter("id != ''")
.map { TransactionInstance(transaction: $0) }
}
func delete(transactions: [TransactionInstance]) -> Promise<Void> {
return Promise { seal in
let realm = self.realm.threadSafe
let objects = transactions.compactMap {
realm.object(ofType: Transaction.self, forPrimaryKey: $0.primaryKey)
}
do {
try realm.write {
realm.delete(objects)
}
seal.fulfill(())
} catch {
seal.reject(error)
}
}
}
func update(state: TransactionState, for primaryKey: String) -> Promise<TransactionInstance> {
enum AnyError: Error {
case invalid
}
return Promise { seal in
let realm = self.realm.threadSafe
if let value = realm.object(ofType: Transaction.self, forPrimaryKey: primaryKey) {
realm.beginWrite()
value.internalState = state.rawValue
do {
try realm.commitWrite()
let transaction = TransactionInstance(transaction: value)
seal.fulfill(transaction)
} catch {
seal.reject(error)
}
} else {
seal.reject(AnyError.invalid)
}
}
}
func add(transactions: [TransactionInstance], transactionsToPullContractsFrom: [TransactionInstance], contractsAndTokenTypes: [AlphaWallet.Address: TokenType]) {
guard !transactions.isEmpty else { return }
let newTransactions = transactions.map { Transaction(object: $0) }
let newTransactionsToPullContractsFrom = transactionsToPullContractsFrom.map { Transaction(object: $0) }
let realm = self.realm.threadSafe
let transactionsToCommit = self.filterTransactionsToNotOverrideERC20Transactions(newTransactions, realm: realm)
realm.beginWrite()
for transaction in transactionsToCommit {
realm.add(transaction, update: .all)
}
try! realm.commitWrite()
self.addTokensWithContractAddresses(fromTransactions: newTransactionsToPullContractsFrom, contractsAndTokenTypes: contractsAndTokenTypes, realm: realm)
}
//We pull transactions data from the normal transactions API as well as ERC20 event log. For the same transaction, we only want data from the latter. Otherwise the UI will show the cell display switching between data from the 2 source as we fetch (or re-fetch)
private func filterTransactionsToNotOverrideERC20Transactions(_ transactions: [Transaction]) -> [Transaction] {
private func filterTransactionsToNotOverrideERC20Transactions(_ transactions: [Transaction], realm: Realm) -> [Transaction] {
return transactions.filter { each in
if each.isERC20Interaction {
return true

@ -35,6 +35,88 @@ class LocalizedOperationObject: Object {
self.decimals = decimals
}
convenience init(object: LocalizedOperationObjectInstance) {
self.init()
self.from = object.from
self.to = object.to
self.contract = object.contract
self.type = object.type
self.value = object.value
self.symbol = object.symbol
self.name = object.name
self.decimals = object.decimals
}
var operationType: OperationType {
return OperationType(string: type)
}
var contractAddress: AlphaWallet.Address? {
return contract.flatMap { AlphaWallet.Address(uncheckedAgainstNullAddress: $0) }
}
}
extension LocalizedOperationObject {
static func from(operations: [LocalizedOperation]?) -> [LocalizedOperationObject] {
guard let operations = operations else { return [] }
return operations.compactMap { operation in
guard let from = operation.fromAddress, let to = operation.toAddress else { return nil }
return LocalizedOperationObject(
from: from.description,
to: to.description,
contract: operation.contract.contractAddress,
type: operation.type.rawValue,
value: operation.value,
symbol: operation.contract.symbol,
name: operation.contract.name,
decimals: operation.contract.decimals
)
}
}
}
struct LocalizedOperationObjectInstance {
//TODO good to have getters/setter computed properties for `from` and `to` too that is typed AlphaWallet.Address. But have to be careful and check if they can be empty or "0x"
var from: String = ""
var to: String = ""
var contract: String? = .none
var type: String = ""
var value: String = ""
var name: String? = .none
var symbol: String? = .none
var decimals: Int = 18
init(object: LocalizedOperationObject) {
self.from = object.from
self.to = object.to
self.contract = object.contract
self.type = object.type
self.value = object.value
self.symbol = object.symbol
self.name = object.name
self.decimals = object.decimals
}
init(
from: String,
to: String,
contract: AlphaWallet.Address?,
type: String,
value: String,
symbol: String?,
name: String?,
decimals: Int
) {
self.from = from
self.to = to
self.contract = contract?.eip55String
self.type = type
self.value = value
self.symbol = symbol
self.name = name
self.decimals = decimals
}
var operationType: OperationType {
return OperationType(string: type)
}
@ -42,4 +124,4 @@ class LocalizedOperationObject: Object {
var contractAddress: AlphaWallet.Address? {
return contract.flatMap { AlphaWallet.Address(uncheckedAgainstNullAddress: $0) }
}
}
}

@ -25,11 +25,7 @@ class TransactionViewController: UIViewController {
weak var delegate: TransactionViewControllerDelegate?
init(
session: WalletSession,
transactionRow: TransactionRow,
delegate: TransactionViewControllerDelegate?
) {
init(session: WalletSession, transactionRow: TransactionRow, delegate: TransactionViewControllerDelegate?) {
self.session = session
self.transactionRow = transactionRow
self.delegate = delegate
@ -172,7 +168,7 @@ class TransactionViewController: UIViewController {
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
return nil
}
@objc func dismiss() {

@ -84,12 +84,7 @@ class TransactionsViewController: UIViewController {
func fetch() {
startLoading()
//Since this is called at launch, we don't want it to block launching
DispatchQueue.global().async {
DispatchQueue.main.async { [weak self] in
self?.dataCoordinator.fetch()
}
}
dataCoordinator.fetch()
}
func configure(viewModel: TransactionsViewModel) {
@ -97,8 +92,9 @@ class TransactionsViewController: UIViewController {
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
return nil
}
fileprivate func headerView(for section: Int) -> UIView {
let container = UIView()
container.backgroundColor = viewModel.headerBackgroundColor
@ -109,6 +105,7 @@ class TransactionsViewController: UIViewController {
title.font = viewModel.headerTitleFont
container.addSubview(title)
title.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
title.anchorsConstraint(to: container, edgeInsets: .init(top: 18, left: 20, bottom: 16, right: 0))
])
@ -130,15 +127,26 @@ extension TransactionsViewController: UITableViewDelegate {
}
extension TransactionsViewController: TransactionDataCoordinatorDelegate {
func didUpdate(result: Result<[Transaction], TransactionError>, reloadImmediately: Bool) {
func didUpdate(result: Result<[TransactionInstance], TransactionError>, reloadImmediately: Bool) {
switch result {
case .success(let items):
let viewModel = TransactionsViewModel(transactions: items)
configure(viewModel: viewModel)
endLoading()
//NOTE: avoid filtering events on main queue
let values = TransactionsViewModel.mapTransactions(transactions: items)
DispatchQueue.main.async {
self.configure(viewModel: .init(transactions: values))
self.endLoading()
self.reloadTableViewAndEndRefreshing()
}
case .failure(let error):
endLoading(error: error)
DispatchQueue.main.async {
self.endLoading(error: error)
self.reloadTableViewAndEndRefreshing()
}
}
}
private func reloadTableViewAndEndRefreshing() {
tableView.reloadData()
if refreshControl.isRefreshing {
@ -156,13 +164,9 @@ extension TransactionsViewController: UITableViewDataSource {
let transactionRow = viewModel.item(for: indexPath.row, section: indexPath.section)
let cell: TransactionViewCell = tableView.dequeueReusableCell(for: indexPath)
let session = sessions[transactionRow.server]
cell.configure(viewModel: .init(
transactionRow: transactionRow,
chainState: session.chainState,
currentWallet: session.account,
server: transactionRow.server
)
)
let viewModel: TransactionRowCellViewModel = .init(transactionRow: transactionRow, chainState: session.chainState, currentWallet: session.account, server: transactionRow.server)
cell.configure(viewModel: viewModel)
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

@ -2,11 +2,11 @@
import Foundation
enum TransactionRow {
case standalone(Transaction)
case group(Transaction)
case item(transaction: Transaction, operation: LocalizedOperationObject)
case standalone(TransactionInstance)
case group(TransactionInstance)
case item(transaction: TransactionInstance, operation: LocalizedOperationObjectInstance)
var transaction: Transaction {
var transaction: TransactionInstance {
switch self {
case .standalone(let transaction), .group(let transaction), .item(transaction: let transaction, _):
return transaction
@ -74,7 +74,7 @@ enum TransactionRow {
transaction.server
}
var operation: LocalizedOperationObject? {
var operation: LocalizedOperationObjectInstance? {
switch self {
case .standalone(let transaction):
return transaction.operation
@ -84,4 +84,4 @@ enum TransactionRow {
return operation
}
}
}
}

@ -29,7 +29,7 @@ struct TransactionRowCellViewModel {
}
private var operationTitle: String? {
let operation: LocalizedOperationObject?
let operation: LocalizedOperationObjectInstance?
switch transactionRow {
case .standalone(let transaction):
operation = transaction.operation

@ -4,12 +4,16 @@ import Foundation
import UIKit
struct TransactionsViewModel {
private var formatter: DateFormatter {
private static var formatter: DateFormatter {
return Date.formatter(with: "dd MMM yyyy")
}
private var items: [(date: String, transactionRows: [TransactionRow])] = []
init(transactions: [Transaction] = []) {
init(transactions: [(date: String, transactionRows: [TransactionRow])] = []) {
self.items = transactions
}
static func mapTransactions(transactions: [TransactionInstance]) -> [(date: String, transactionRows: [TransactionRow])] {
//Uses NSMutableArray instead of Swift array for performance. Really slow when dealing with 10k events, which is hardly a big wallet
var newItems: [String: NSMutableArray] = [:]
for transaction in transactions {
@ -19,12 +23,13 @@ struct TransactionsViewModel {
newItems[date] = currentItems
}
let tuple = newItems.map { each in
(date: each.key, transactions: (each.value as! [Transaction]).sorted { $0.date > $1.date })
(date: each.key, transactions: (each.value as! [TransactionInstance]).sorted { $0.date > $1.date })
}
let collapsedTransactions: [(date: String, transactions: [Transaction])] = tuple.sorted { (object1, object2) -> Bool in
let collapsedTransactions: [(date: String, transactions: [TransactionInstance])] = tuple.sorted { (object1, object2) -> Bool in
return formatter.date(from: object1.date)! > formatter.date(from: object2.date)!
}
items = collapsedTransactions.map { date, transactions in
return collapsedTransactions.map { date, transactions in
var items: [TransactionRow] = .init()
for each in transactions {
if each.localizedOperations.isEmpty {
@ -70,7 +75,7 @@ struct TransactionsViewModel {
func titleForHeader(in section: Int) -> String {
let value = items[section].date
let date = formatter.date(from: value)!
let date = Self.formatter.date(from: value)!
if NSCalendar.current.isDateInToday(date) {
return R.string.localizable.today().localizedUppercase
}
@ -79,4 +84,4 @@ struct TransactionsViewModel {
}
return value.localizedUppercase
}
}
}

@ -74,27 +74,31 @@ private class TokenImageFetcher {
subscribable.value = programmaticallyGenerateIcon(forToken: tokenObject)
return subscribable
}
let githubAssetsSource = tokenObject.server.githubAssetsSource
let contractAddress = tokenObject.contractAddress
let balance = tokenObject.balance.first?.balance
let generatedImage = programmaticallyGenerateIcon(forToken: tokenObject)
fetchFromOpenSea(tokenObject).done {
fetchFromOpenSea(tokenObject.type, balance: balance).done {
subscribable.value = (image: $0, symbol: "")
}.catch { [weak self] _ in
guard let strongSelf = self else { return }
strongSelf.fetchFromAssetGitHubRepo(tokenObject).done {
strongSelf.fetchFromAssetGitHubRepo(githubAssetsSource, contractAddress: contractAddress).done {
subscribable.value = (image: $0, symbol: "")
}.catch { [weak self] _ in
guard let strongSelf = self else { return }
subscribable.value = strongSelf.programmaticallyGenerateIcon(forToken: tokenObject)
}.catch { _ in
subscribable.value = generatedImage
}
}
return subscribable
}
private func fetchFromOpenSea(_ tokenObject: TokenObject) -> Promise<UIImage> {
private func fetchFromOpenSea(_ type: TokenType, balance: String?) -> Promise<UIImage> {
Promise { seal in
switch tokenObject.type {
switch type {
case .erc721:
if let json = tokenObject.balance.first?.balance, let data = json.data(using: .utf8), let openSeaNonFungible = try? JSONDecoder().decode(OpenSeaNonFungible.self, from: data), !openSeaNonFungible.contractImageUrl.isEmpty {
if let json = balance, let data = json.data(using: .utf8), let openSeaNonFungible = try? JSONDecoder().decode(OpenSeaNonFungible.self, from: data), !openSeaNonFungible.contractImageUrl.isEmpty {
let request = URLRequest(url: URL(string: openSeaNonFungible.contractImageUrl)!)
fetch(request: request).done { image in
seal.fulfill(image)
@ -108,8 +112,8 @@ private class TokenImageFetcher {
}
}
private func fetchFromAssetGitHubRepo(_ tokenObject: TokenObject) -> Promise<UIImage> {
return GithubAssetsURLResolver().resolve(for: tokenObject).then { request -> Promise<UIImage> in
private func fetchFromAssetGitHubRepo(_ githubAssetsSource: GithubAssetsURLResolver.Source, contractAddress: AlphaWallet.Address) -> Promise<UIImage> {
return GithubAssetsURLResolver().resolve(for: githubAssetsSource, contractAddress: contractAddress).then { request -> Promise<UIImage> in
self.fetch(request: request)
}
}
@ -145,8 +149,8 @@ class GithubAssetsURLResolver {
case case1
}
func resolve(for tokenObject: TokenObject) -> Promise<URLRequest> {
let value = tokenObject.server.githubAssetsSource.rawValue + tokenObject.contractAddress.eip55String + "/" + GithubAssetsURLResolver.file
func resolve(for githubAssetsSource: GithubAssetsURLResolver.Source, contractAddress: AlphaWallet.Address) -> Promise<URLRequest> {
let value = githubAssetsSource.rawValue + contractAddress.eip55String + "/" + GithubAssetsURLResolver.file
guard let url = URL(string: value) else {
return .init(error: AnyError.case1)

@ -38,3 +38,39 @@ extension Transaction {
)
}
}
extension TransactionInstance {
static func make(
id: String = "0x1",
blockNumber: Int = 1,
transactionIndex: Int = 0,
from: String = "0x1",
to: String = "0x1",
value: String = "1",
gas: String = "0x1",
gasPrice: String = "0x1",
gasUsed: String = "0x1",
nonce: String = "0",
date: Date = Date(),
localizedOperations: [LocalizedOperationObjectInstance] = [],
state: TransactionState = .completed
) -> TransactionInstance {
return TransactionInstance(
id: id,
server: .main,
blockNumber: blockNumber,
transactionIndex: transactionIndex,
from: from,
to: to,
value: value,
gas: gas,
gasPrice: gasPrice,
gasUsed: gasUsed,
nonce: nonce,
date: date,
localizedOperations: localizedOperations,
state: state,
isErc20Interaction: false
)
}
}

@ -1,20 +1,23 @@
// Copyright © 2020 Stormbird PTE. LTD.
import Foundation
import PromiseKit
@testable import AlphaWallet
class FakeEventsDataStore: EventsDataStoreProtocol {
func add(events: [EventInstance], forTokenContract contract: AlphaWallet.Address) {
func getLastMatchingEventSortedByBlockNumber(forContract contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String) -> Promise<EventInstance?> {
return .value(nil)
}
func deleteEvents(forTokenContract contract: AlphaWallet.Address) {
func add(events: [EventInstanceValue], forTokenContract contract: AlphaWallet.Address) -> Promise<Void> {
return .init()
}
func getMatchingEvents(forContract contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String, filterName: String, filterValue: String) -> [EventInstance] {
.init()
func deleteEvents(forTokenContract contract: AlphaWallet.Address) {
}
func getMatchingEventsSortedByBlockNumber(forContract contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String) -> [EventInstance] {
func getMatchingEvents(forContract contract: AlphaWallet.Address, tokenContract: AlphaWallet.Address, server: RPCServer, eventName: String, filterName: String, filterValue: String) -> [EventInstance] {
.init()
}

Loading…
Cancel
Save