Refactor `EventSourceCoordinator` and `EventSourceCoordinatorForActivities` with `.functional` extension, fix crash: converting `Int` from `BigUInt`, fixed with replacing `Int` with `String` #3011

pull/3063/head
Vladyslav shepitko 3 years ago
parent 3c2b675e11
commit cc29815112
  1. 2
      AlphaWallet/Activities/ActivitiesService.swift
  2. 91
      AlphaWallet/TokenScriptClient/Coordinators/EventSourceCoordinator.swift
  3. 67
      AlphaWallet/TokenScriptClient/Coordinators/EventSourceCoordinatorForActivities.swift
  4. 2
      AlphaWallet/Tokens/Helpers/TokenAdaptor.swift

@ -217,7 +217,7 @@ class ActivitiesService: NSObject, ActivitiesServiceType {
for card in xmlHandler.activityCards {
let (filterName, filterValue) = card.eventOrigin.eventFilter
let interpolatedFilter: String
if let implicitAttribute = EventSourceCoordinatorForActivities.convertToImplicitAttribute(string: filterValue) {
if let implicitAttribute = EventSourceCoordinator.functional.convertToImplicitAttribute(string: filterValue) {
switch implicitAttribute {
case .tokenId:
continue

@ -41,7 +41,7 @@ class EventSourceCoordinator: EventSourceCoordinatorType {
for eachTokenHolder in tokenHolders {
guard let tokenId = eachTokenHolder.tokenIds.first else { continue }
let promise = fetchEvents(forTokenId: tokenId, token: token, eventOrigin: eventOrigin)
let promise = EventSourceCoordinator.functional.fetchEvents(forTokenId: tokenId, token: token, eventOrigin: eventOrigin, wallet: wallet, eventsDataStore: eventsDataStore, queue: queue)
fetchPromises.append(promise)
}
}
@ -74,44 +74,59 @@ class EventSourceCoordinator: EventSourceCoordinatorType {
self.isFetching = false
}
}
}
extension EventSourceCoordinator {
class functional {}
}
extension EventSourceCoordinator.functional {
private func fetchEvents(forTokenId tokenId: TokenId, token: TokenObject, eventOrigin: EventOrigin) -> Promise<Void> {
static func fetchEvents(forTokenId tokenId: TokenId, token: TokenObject, eventOrigin: EventOrigin, wallet: Wallet, eventsDataStore: EventsDataStoreProtocol, queue: DispatchQueue) -> Promise<Void> {
//Important to not access `token` in the queue or another thread. Do it outside
//TODO better to pass in a non-Realm representation of the TokenObject instead
let contractAddress = token.contractAddress
let tokenServer = token.server
return Promise<Void> { seal in
self.queue.async {
queue.async {
let (filterName, filterValue) = eventOrigin.eventFilter
let filterParam = eventOrigin.parameters
.filter { $0.isIndexed }
.map { self.formFilterFrom(fromParameter: $0, tokenId: tokenId, filterName: filterName, filterValue: filterValue) }
self.eventsDataStore.getLastMatchingEventSortedByBlockNumber(forContract: eventOrigin.contract, tokenContract: contractAddress, server: tokenServer, eventName: eventOrigin.eventName).map(on: self.queue, { oldEvent -> EventFilter.Block in
.map { Self.formFilterFrom(fromParameter: $0, tokenId: tokenId, filterName: filterName, filterValue: filterValue, wallet: wallet) }
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: self.queue, { fromBlock -> EventFilter in
}).map(on: queue, { fromBlock -> EventFilter in
EventFilter(fromBlock: fromBlock, toBlock: .latest, addresses: [EthereumAddress(address: eventOrigin.contract)], parameterFilters: filterParam.map { $0?.filter })
}).then(on: self.queue, { eventFilter in
getEventLogs(withServer: tokenServer, contract: eventOrigin.contract, eventName: eventOrigin.eventName, abiString: eventOrigin.eventAbiString, filter: eventFilter, queue: self.queue)
}).map(on: self.queue, { result -> [EventInstanceValue] in
}).then(on: queue, { eventFilter in
getEventLogs(withServer: tokenServer, contract: eventOrigin.contract, eventName: eventOrigin.eventName, abiString: eventOrigin.eventAbiString, filter: eventFilter, queue: queue)
}).map(on: queue, { result -> [EventInstanceValue] in
result.compactMap {
self.convertEventToDatabaseObject($0, filterParam: filterParam, eventOrigin: eventOrigin, contractAddress: contractAddress, server: tokenServer)
Self.convertEventToDatabaseObject($0, filterParam: filterParam, eventOrigin: eventOrigin, contractAddress: contractAddress, server: tokenServer)
}
}).then(on: self.queue, { events -> Promise<Void> in
self.eventsDataStore.add(events: events, forTokenContract: contractAddress)
}).done(on: self.queue, { _ in
}).then(on: queue, { events -> Promise<Void> in
eventsDataStore.add(events: events, forTokenContract: contractAddress)
}).done(on: queue, { _ in
seal.fulfill(())
}).catch(on: self.queue, { e in
}).catch(on: queue, { e in
seal.reject(e)
})
}
}
}
private func convertEventToDatabaseObject(_ event: EventParserResultProtocol, filterParam: [(filter: [EventFilterable], textEquivalent: String)?], eventOrigin: EventOrigin, contractAddress: AlphaWallet.Address, server: RPCServer) -> EventInstanceValue? {
static func convertToImplicitAttribute(string: String) -> AssetImplicitAttributes? {
let prefix = "${"
let suffix = "}"
guard string.hasPrefix(prefix) && string.hasSuffix(suffix) else { return nil }
let value = string.substring(with: prefix.count..<(string.count - suffix.count))
return AssetImplicitAttributes(rawValue: value)
}
private static 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)
@ -123,29 +138,11 @@ class EventSourceCoordinator: EventSourceCoordinatorType {
return EventInstanceValue(contract: eventOrigin.contract, tokenContract: contractAddress, server: server, eventName: eventOrigin.eventName, blockNumber: Int(blockNumber), logIndex: Int(logIndex), filter: filterText, json: json)
}
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:
return (key, address.address)
case let data as Data:
return (key, data.hexEncoded)
case let string as String:
return (key, string)
case let bigUInt as BigUInt:
return (key, Int(bigUInt))
default:
//We only accept known types, otherwise serializing to JSON will crash
return nil
}
})
}
private func formFilterFrom(fromParameter parameter: EventParameter, tokenId: TokenId, filterName: String, filterValue: String) -> (filter: [EventFilterable], textEquivalent: String)? {
private static func formFilterFrom(fromParameter parameter: EventParameter, tokenId: TokenId, filterName: String, filterValue: String, wallet: Wallet) -> (filter: [EventFilterable], textEquivalent: String)? {
guard parameter.name == filterName else { return nil }
guard let parameterType = SolidityType(rawValue: parameter.type) else { return nil }
let optionalFilter: (filter: AssetAttributeValueUsableAsFunctionArguments, textEquivalent: String)?
if let implicitAttribute = EventSourceCoordinator.convertToImplicitAttribute(string: filterValue) {
if let implicitAttribute = Self.convertToImplicitAttribute(string: filterValue) {
switch implicitAttribute {
case .tokenId:
optionalFilter = AssetAttributeValueUsableAsFunctionArguments(assetAttribute: .uint(tokenId)).flatMap { (filter: $0, textEquivalent: "\(filterName)=\(tokenId)") }
@ -163,11 +160,23 @@ class EventSourceCoordinator: EventSourceCoordinatorType {
return (filter: [filterValueTypedForEventFilters], textEquivalent: textEquivalent)
}
static func convertToImplicitAttribute(string: String) -> AssetImplicitAttributes? {
let prefix = "${"
let suffix = "}"
guard string.hasPrefix(prefix) && string.hasSuffix(suffix) else { return nil }
let value = string.substring(with: prefix.count..<(string.count - suffix.count))
return AssetImplicitAttributes(rawValue: value)
static func convertToJsonCompatible(dictionary: [String: Any]) -> [String: Any] {
Dictionary(uniqueKeysWithValues: dictionary.compactMap { key, value -> (String, Any)? in
switch value {
case let address as EthereumAddress:
return (key, address.address)
case let data as Data:
return (key, data.hexEncoded)
case let string as String:
return (key, string)
case let bigUInt as BigUInt:
//Must not do `Int(bigUInt)` because it crashes upon overflow
return (key, String(bigUInt))
default:
//We only accept known types, otherwise serializing to JSON will crash
return nil
}
})
}
}

@ -33,7 +33,7 @@ class EventSourceCoordinatorForActivities: EventSourceCoordinatorForActivitiesTy
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)
EventSourceCoordinatorForActivities.functional.fetchEvents(tokenContract: token.contractAddress, server: token.server, card: $0, eventsDataStore: eventsDataStore, queue: queue, wallet: wallet)
}
}
@ -41,7 +41,7 @@ class EventSourceCoordinatorForActivities: EventSourceCoordinatorForActivitiesTy
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)
EventSourceCoordinatorForActivities.functional.fetchEvents(tokenContract: contract, server: rpcServer, card: $0, eventsDataStore: eventsDataStore, queue: queue, wallet: wallet)
}
}
@ -89,18 +89,24 @@ class EventSourceCoordinatorForActivities: EventSourceCoordinatorForActivitiesTy
seal.fulfill(data)
}
}
}
}
private func fetchEvents(tokenContract: AlphaWallet.Address, server: RPCServer, card: TokenScriptCard) -> Promise<Void>? {
extension EventSourceCoordinatorForActivities {
class functional {}
}
extension EventSourceCoordinatorForActivities.functional {
static func fetchEvents(tokenContract: AlphaWallet.Address, server: RPCServer, card: TokenScriptCard, eventsDataStore: EventsActivityDataStoreProtocol, queue: DispatchQueue, wallet: Wallet) -> Promise<Void>? {
return Promise { seal in
self.queue.async {
queue.async {
let eventOrigin = card.eventOrigin
let (filterName, filterValue) = eventOrigin.eventFilter
let filterParam = eventOrigin.parameters.filter {
$0.isIndexed
}.map {
self.formFilterFrom(fromParameter: $0, filterName: filterName, filterValue: filterValue)
EventSourceCoordinatorForActivities.functional.formFilterFrom(fromParameter: $0, filterName: filterName, filterValue: filterValue, wallet: wallet)
}
if filterParam.allSatisfy({ $0 == nil }) {
@ -109,14 +115,14 @@ class EventSourceCoordinatorForActivities: EventSourceCoordinatorForActivitiesTy
return
}
self.eventsDataStore.getMatchingEventsSortedByBlockNumber(forContract: eventOrigin.contract, tokenContract: tokenContract, server: server, eventName: eventOrigin.eventName).map(on: self.queue, { oldEvent -> (EventFilter.Block, UInt64) in
eventsDataStore.getMatchingEventsSortedByBlockNumber(forContract: eventOrigin.contract, tokenContract: tokenContract, server: server, eventName: eventOrigin.eventName).map(on: queue, { oldEvent -> (EventFilter.Block, UInt64) in
if let newestEvent = oldEvent {
let value = UInt64(newestEvent.blockNumber + 1)
return (.blockNumber(value), value)
} else {
return (.blockNumber(0), 0)
}
}).map(on: self.queue, { fromBlock -> EventFilter in
}).map(on: queue, { fromBlock -> EventFilter in
let parameterFilters = filterParam.map { $0?.filter }
let addresses = [EthereumAddress(address: eventOrigin.contract)]
@ -128,9 +134,9 @@ class EventSourceCoordinatorForActivities: EventSourceCoordinatorForActivitiesTy
toBlock = .latest
}
return EventFilter(fromBlock: fromBlock.0, toBlock: toBlock, addresses: addresses, parameterFilters: parameterFilters)
}).then(on: self.queue, { eventFilter in
getEventLogs(withServer: server, contract: eventOrigin.contract, eventName: eventOrigin.eventName, abiString: eventOrigin.eventAbiString, filter: eventFilter, queue: self.queue)
}).then(on: self.queue, { events -> Promise<[EventActivityInstance]> in
}).then(on: queue, { eventFilter in
getEventLogs(withServer: server, contract: eventOrigin.contract, eventName: eventOrigin.eventName, abiString: eventOrigin.eventAbiString, filter: eventFilter, queue: queue)
}).then(on: queue, { events -> Promise<[EventActivityInstance]> in
let promises = events.compactMap { event -> Promise<EventActivityInstance?> in
guard let blockNumber = event.eventLog?.blockNumber else {
return .value(nil)
@ -138,21 +144,21 @@ class EventSourceCoordinatorForActivities: EventSourceCoordinatorForActivitiesTy
return GetBlockTimestampCoordinator()
.getBlockTimestamp(blockNumber, onServer: server)
.map(on: self.queue, { date in
.map(on: queue, { date in
Self.convertEventToDatabaseObject(event, date: date, filterParam: filterParam, eventOrigin: eventOrigin, tokenContract: tokenContract, server: server)
}).recover(on: self.queue, { _ -> Promise<EventActivityInstance?> in
}).recover(on: queue, { _ -> Promise<EventActivityInstance?> in
return .value(nil)
})
}
return when(resolved: promises).map(on: self.queue, { res -> [EventActivityInstance] in
return when(resolved: promises).map(on: queue, { res -> [EventActivityInstance] in
promises.compactMap { $0.value }.compactMap { $0 }
})
}).then(on: self.queue, { events -> Promise<Void> in
}).then(on: queue, { events -> Promise<Void> in
if events.isEmpty {
return .value(())
} else {
return self.eventsDataStore.add(events: events, forTokenContract: tokenContract).then(on: self.queue, { _ -> Promise<Void> in
return eventsDataStore.add(events: events, forTokenContract: tokenContract).then(on: queue, { _ -> Promise<Void> in
return .value(())
})
}
@ -169,7 +175,7 @@ class EventSourceCoordinatorForActivities: EventSourceCoordinatorForActivitiesTy
guard let eventLog = event.eventLog else { return nil }
let transactionId = eventLog.transactionHash.hexEncoded
let decodedResult = Self.convertToJsonCompatible(dictionary: event.decodedResult)
let decodedResult = EventSourceCoordinator.functional.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
@ -178,30 +184,11 @@ class EventSourceCoordinatorForActivities: EventSourceCoordinatorForActivitiesTy
return EventActivityInstance(contract: eventOrigin.contract, tokenContract: tokenContract, server: server, date: date, eventName: eventOrigin.eventName, blockNumber: Int(eventLog.blockNumber), transactionId: transactionId, transactionIndex: Int(eventLog.transactionIndex), logIndex: Int(eventLog.logIndex), filter: filterText, json: json)
}
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:
return (key, address.address)
case let data as Data:
return (key, data.hexEncoded)
case let string as String:
return (key, string)
case let bigUInt as BigUInt:
//Must not do `Int(bigUInt)` because it crashes upon overflow
return (key, String(bigUInt))
default:
//We only accept known types, otherwise serializing to JSON will crash
return nil
}
})
}
private func formFilterFrom(fromParameter parameter: EventParameter, filterName: String, filterValue: String) -> (filter: [EventFilterable], textEquivalent: String)? {
private static func formFilterFrom(fromParameter parameter: EventParameter, filterName: String, filterValue: String, wallet: Wallet) -> (filter: [EventFilterable], textEquivalent: String)? {
guard parameter.name == filterName else { return nil }
guard let parameterType = SolidityType(rawValue: parameter.type) else { return nil }
let optionalFilter: (filter: AssetAttributeValueUsableAsFunctionArguments, textEquivalent: String)?
if let implicitAttribute = EventSourceCoordinatorForActivities.convertToImplicitAttribute(string: filterValue) {
if let implicitAttribute = EventSourceCoordinator.functional.convertToImplicitAttribute(string: filterValue) {
switch implicitAttribute {
case .tokenId:
optionalFilter = nil
@ -218,8 +205,4 @@ class EventSourceCoordinatorForActivities: EventSourceCoordinatorForActivitiesTy
guard let filterValueTypedForEventFilters = filterValue.coerceToArgumentTypeForEventFilter(parameterType) else { return nil }
return (filter: [filterValueTypedForEventFilters], textEquivalent: textEquivalent)
}
static func convertToImplicitAttribute(string: String) -> AssetImplicitAttributes? {
EventSourceCoordinator.convertToImplicitAttribute(string: string)
}
}

@ -164,7 +164,7 @@ class TokenAdaptor {
if isSourcedFromEvents, let attributeWithEventSource = xmlHandler.attributesWithEventSource.first, let eventFilter = attributeWithEventSource.eventOrigin?.eventFilter, let eventName = attributeWithEventSource.eventOrigin?.eventName, let eventContract = attributeWithEventSource.eventOrigin?.contract {
let filterName = eventFilter.name
let filterValue: String
if let implicitAttribute = EventSourceCoordinator.convertToImplicitAttribute(string: eventFilter.value) {
if let implicitAttribute = EventSourceCoordinator.functional.convertToImplicitAttribute(string: eventFilter.value) {
switch implicitAttribute {
case .tokenId:
filterValue = eventFilter.value.replacingOccurrences(of: "${tokenId}", with: nonFungible.tokenId)

Loading…
Cancel
Save