diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Modules-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Modules-Package.xcscheme index dcb8e093..c47c0684 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/Modules-Package.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Modules-Package.xcscheme @@ -771,6 +771,20 @@ ReferencedContainer = "container:"> + + + + { /// Returns item if bounded as associated value. - var item: T? { + public var item: T? { switch self { case let .insert(newItem): return newItem @@ -46,6 +46,41 @@ public enum DataProviderChange { } } +enum DataProviderDiff { + /// New items has been added. + /// Item is passed as associated value. + case insert(newItem: T) + + /// Existing item has been updated + /// Item is passed as associated value. + case update(newItem: T) + + /// New remote items. + case remote(newItem: T) + + /// Existing local item + case local(newItem: T) + + /// Existing item has been removed. + /// Identifier of the item is passed as associated value. + case delete(deletedIdentifier: String) + + var item: T? { + switch self { + case let .remote(newItem): + return newItem + case let .local(newItem): + return nil + case .delete: + return nil + case let .insert(newItem): + return newItem + case let .update(newItem): + return newItem + } + } +} + /** * Struct designed to store options needed to describe how an observer should be handled by data provider. */ @@ -65,6 +100,12 @@ public struct DataProviderObserverOptions { /// mechanism. public var waitsInProgressSyncOnAdd: Bool + /// Asks data provider to notify observer if no difference from remote. + /// If this value is `false` (default value) then observer is only notified when + /// there are difference from remote source. + /// if this value is `true` and no diff observer will notify with local source values + public var notifyIfNoDiff: Bool + /// - parameters: /// - alwaysNotifyOnRefresh: Asks data provider to notify observer in any case after /// synchronization completes. @@ -80,10 +121,12 @@ public struct DataProviderObserverOptions { public init( alwaysNotifyOnRefresh: Bool = false, - waitsInProgressSyncOnAdd: Bool = true + waitsInProgressSyncOnAdd: Bool = true, + notifyIfNoDiff: Bool = false ) { self.alwaysNotifyOnRefresh = alwaysNotifyOnRefresh self.waitsInProgressSyncOnAdd = waitsInProgressSyncOnAdd + self.notifyIfNoDiff = notifyIfNoDiff } } @@ -117,6 +160,10 @@ public struct StreamableProviderObserverOptions { /// By default ```true```. public var refreshWhenEmpty: Bool + /// Will notify just when local repository was updated. + /// By default ```false```. + public var notifyJustWhenUpdated: Bool + /// - parameters: /// - alwaysNotifyOnRefresh: Asks data provider to notify observer in any case /// after synchronization completes. @@ -143,12 +190,14 @@ public struct StreamableProviderObserverOptions { alwaysNotifyOnRefresh: Bool = false, waitsInProgressSyncOnAdd: Bool = true, initialSize: Int = 0, - refreshWhenEmpty: Bool = true + refreshWhenEmpty: Bool = true, + notifyJustWhenUpdated: Bool = false ) { self.alwaysNotifyOnRefresh = alwaysNotifyOnRefresh self.waitsInProgressSyncOnAdd = waitsInProgressSyncOnAdd self.initialSize = initialSize self.refreshWhenEmpty = refreshWhenEmpty + self.notifyJustWhenUpdated = notifyJustWhenUpdated } } diff --git a/Sources/RobinHood/Classes/DataProvider/Difference/ListDifferenceCalculator.swift b/Sources/RobinHood/Classes/DataProvider/Difference/ListDifferenceCalculator.swift index 7f2e4720..12343276 100644 --- a/Sources/RobinHood/Classes/DataProvider/Difference/ListDifferenceCalculator.swift +++ b/Sources/RobinHood/Classes/DataProvider/Difference/ListDifferenceCalculator.swift @@ -140,6 +140,7 @@ public final class ListDifferenceCalculator: ListDifferenceCalc switch change { case let .insert(newItem): insertItems[newItem.identifier] = newItem + case let .update(newItem): if let oldItemIndex = allItems .firstIndex(where: { $0.identifier == newItem.identifier }) diff --git a/Sources/RobinHood/Classes/DataProvider/SingleValueProvider/AnySingleValueProvider.swift b/Sources/RobinHood/Classes/DataProvider/SingleValueProvider/AnySingleValueProvider.swift new file mode 100644 index 00000000..9afb68b5 --- /dev/null +++ b/Sources/RobinHood/Classes/DataProvider/SingleValueProvider/AnySingleValueProvider.swift @@ -0,0 +1,53 @@ +import Foundation + +public class AnySingleValueProvider: SingleValueProviderProtocol { + public typealias Model = T + + private let fetchClosure: (((Result?) -> Void)?) -> CompoundOperationWrapper + + private let addObserverClosure: ( + AnyObject, + DispatchQueue?, + @escaping ([DataProviderChange]) -> Void, + @escaping (Error) -> Void, + DataProviderObserverOptions + ) -> Void + + private let removeObserverClosure: (AnyObject) -> Void + + private let refreshClosure: () -> Void + + public var executionQueue: OperationQueue + + public init(_ dataProvider: U) where U.Model == Model { + fetchClosure = dataProvider.fetch(with:) + addObserverClosure = dataProvider.addObserver + removeObserverClosure = dataProvider.removeObserver + refreshClosure = dataProvider.refresh + executionQueue = dataProvider.executionQueue + } + + public func fetch(with completionBlock: ((Result?) -> Void)?) + -> CompoundOperationWrapper + { + fetchClosure(completionBlock) + } + + public func addObserver( + _ observer: AnyObject, + deliverOn queue: DispatchQueue?, + executing updateBlock: @escaping ([DataProviderChange]) -> Void, + failing failureBlock: @escaping (Error) -> Void, + options: DataProviderObserverOptions + ) { + addObserverClosure(observer, queue, updateBlock, failureBlock, options) + } + + public func removeObserver(_ observer: AnyObject) { + removeObserverClosure(observer) + } + + public func refresh() { + refreshClosure() + } +} diff --git a/Sources/SSFAccountManagment/Classes/Import/AccountImportService.swift b/Sources/SSFAccountManagment/Classes/Import/AccountImportService.swift index fd250218..f50c4689 100644 --- a/Sources/SSFAccountManagment/Classes/Import/AccountImportService.swift +++ b/Sources/SSFAccountManagment/Classes/Import/AccountImportService.swift @@ -84,6 +84,7 @@ public final class AccountImportService { case let .failure(error): continuation.resume(throwing: error) + case .none: let error = BaseOperationError.parentOperationCancelled continuation.resume(throwing: error) @@ -124,6 +125,7 @@ extension AccountImportService: AccountCreatable { case let .failure(error): continuation.resume(throwing: error) + case .none: let error = BaseOperationError.parentOperationCancelled continuation.resume(throwing: error) @@ -195,6 +197,7 @@ extension AccountImportService: AccountImportable { case let .failure(error): continuation.resume(throwing: error) + case .none: let error = BaseOperationError.parentOperationCancelled continuation.resume(throwing: error) diff --git a/Sources/SSFAssetManagment/AssetFetching/ChainAssetsFetchingService.swift b/Sources/SSFAssetManagment/AssetFetching/ChainAssetsFetchingService.swift index 66a9ac15..d1dad4a8 100644 --- a/Sources/SSFAssetManagment/AssetFetching/ChainAssetsFetchingService.swift +++ b/Sources/SSFAssetManagment/AssetFetching/ChainAssetsFetchingService.swift @@ -128,11 +128,19 @@ private extension ChainAssetsFetchingService { func sortByPrice(chainAssets: [ChainAsset], order: AssetSortOrder) -> [ChainAsset] { chainAssets.sorted { + let firstPriceDataSorted = $0.asset.priceData + .sorted { $0.currencyId < $1.currencyId } + let firstPriceString = firstPriceDataSorted.first?.price ?? "" + let firstPrice = Decimal(string: firstPriceString) + let secondPriceDataSorted = $1.asset.priceData + .sorted { $0.currencyId < $1.currencyId } + let secondPriceString = secondPriceDataSorted.first?.price ?? "" + let secondPrice = Decimal(string: secondPriceString) switch order { case .ascending: - return $0.asset.price ?? 0 < $1.asset.price ?? 0 + return firstPrice ?? 0 < secondPrice ?? 0 case .descending: - return $0.asset.price ?? 0 > $1.asset.price ?? 0 + return secondPrice ?? 0 > firstPrice ?? 0 } } } diff --git a/Sources/SSFAssetManagmentStorage/ChainModelMapper.swift b/Sources/SSFAssetManagmentStorage/ChainModelMapper.swift index f681a94b..6c28b3ec 100644 --- a/Sources/SSFAssetManagmentStorage/ChainModelMapper.swift +++ b/Sources/SSFAssetManagmentStorage/ChainModelMapper.swift @@ -8,16 +8,28 @@ public enum ChainModelMapperError: Error { case missingChainId } -public final class ChainModelMapper { +public final class ChainModelMapper: CoreDataMapperProtocol { public var entityIdentifierFieldName: String { #keyPath(CDChain.chainId) } public typealias DataProviderModel = ChainModel public typealias CoreDataEntity = CDChain - private let apiKeyInjector: ApiKeyInjector + public init() {} - public init(apiKeyInjector: ApiKeyInjector) { - self.apiKeyInjector = apiKeyInjector + private func createPriceData(from entity: CDPriceData) -> PriceData? { + guard let currencyId = entity.currencyId, + let priceId = entity.priceId, + let price = entity.price else + { + return nil + } + return PriceData( + currencyId: currencyId, + priceId: priceId, + price: price, + fiatDayChange: Decimal(string: entity.fiatDayByChange ?? ""), + coingeckoPriceId: entity.coingeckoPriceId + ) } private func createAsset(from entity: CDAsset) -> AssetModel? { @@ -61,30 +73,39 @@ public final class ChainModelMapper { priceProvider = PriceProvider(type: type, id: id, precision: Int16(precision)) } + let priceDatas: [PriceData] = entity.priceData.or([]).compactMap { data in + guard let priceData = data as? CDPriceData else { + return nil + } + return createPriceData(from: priceData) + } + let tokenProperties = TokenProperties( + type: createChainAssetModelType(from: entity.type), + isNative: entity.isNative, + staking: staking + ) return AssetModel( id: id, name: name, symbol: symbol, - isUtility: entity.isUtility, precision: UInt16(bitPattern: entity.precision), icon: entity.icon, - substrateType: createChainAssetModelType(from: entity.type), - ethereumType: nil, - tokenProperties: TokenProperties(), - price: entity.price as Decimal?, - priceId: nil, - coingeckoPriceId: nil, - priceProvider: nil + tokenProperties: tokenProperties, + existentialDeposit: entity.existentialDeposit, + isUtility: entity.isUtility, + purchaseProviders: purchaseProviders, + ethereumType: createEthereumAssetType(from: entity.ethereumType), + priceProvider: priceProvider, + coingeckoPriceId: entity.priceId, + priceData: priceDatas ) } - private func createChainNode(from entity: CDChainNode, chainId: String) -> ChainNodeModel { + private func createChainNode(from entity: CDChainNode, chainId _: String) -> ChainNodeModel { let apiKey: ChainNodeModel.ApiKey? - if let keyName = entity.apiKeyName, - let nodeApiKey = apiKeyInjector.getNodeApiKey(for: chainId, apiKeyName: keyName) - { - apiKey = ChainNodeModel.ApiKey(queryName: nodeApiKey, keyName: keyName) + if let queryName = entity.apiQueryName, let keyName = entity.apiKeyName { + apiKey = ChainNodeModel.ApiKey(queryName: queryName, keyName: keyName) } else { apiKey = nil } @@ -101,22 +122,69 @@ public final class ChainModelMapper { from model: ChainModel, context: NSManagedObjectContext ) { - let assets = (model.tokens.tokens ?? []).map { + let tokens: Set = model.tokens.tokens ?? [] + let assets = tokens.compactMap { assetModel in let assetEntity = CDAsset(context: context) - assetEntity.id = $0.id ?? "0" - assetEntity.icon = $0.icon - assetEntity.precision = Int16(bitPattern: $0.precision) - assetEntity.price = $0.price as NSDecimalNumber? - assetEntity.symbol = $0.symbol - assetEntity.name = $0.name - assetEntity.type = $0.substrateType?.rawValue + assetEntity.id = assetModel.id + assetEntity.icon = assetModel.icon + assetEntity.precision = Int16(bitPattern: assetModel.precision) + assetEntity.priceId = assetModel.coingeckoPriceId + assetEntity.symbol = assetModel.symbol + assetEntity.existentialDeposit = assetModel.existentialDeposit + assetEntity.color = assetModel.tokenProperties?.color + assetEntity.name = assetModel.name + assetEntity.currencyId = assetModel.tokenProperties?.currencyId + assetEntity.type = assetModel.tokenProperties?.type?.rawValue + assetEntity.isUtility = assetModel.isUtility + assetEntity.isNative = assetModel.tokenProperties?.isNative ?? false + assetEntity.staking = assetModel.tokenProperties?.staking?.rawValue + assetEntity.ethereumType = assetModel.ethereumType?.rawValue let priceProviderContext = CDPriceProvider(context: context) + priceProviderContext.type = assetModel.priceProvider?.type.rawValue + priceProviderContext.id = assetModel.priceProvider?.id + if let precision = assetModel.priceProvider?.precision { + priceProviderContext.precision = "\(precision)" + } assetEntity.priceProvider = priceProviderContext + let purchaseProviders: [String]? = assetModel.purchaseProviders?.map(\.rawValue) + assetEntity.purchaseProviders = purchaseProviders + + let priceData: [CDPriceData] = assetModel.priceData.map { priceData in + let entity = CDPriceData(context: context) + entity.currencyId = priceData.currencyId + entity.priceId = priceData.priceId + entity.price = priceData.price + entity.fiatDayByChange = String("\(priceData.fiatDayChange)") + entity.coingeckoPriceId = priceData.coingeckoPriceId + return entity + } + + if let oldAssets = entity.assets as? Set, + let updatedAsset = oldAssets.first(where: { cdAsset in + cdAsset.id == assetModel.id + }) + { + if let oldPrices = updatedAsset.priceData as? Set { + for cdPriceData in oldPrices { + if !priceData.contains(where: { $0.currencyId == cdPriceData.currencyId }) { + context.delete(cdPriceData) + } + } + } + } + assetEntity.priceData = Set(priceData) as NSSet + return assetEntity } + if let oldAssets = entity.assets as? Set { + for cdAsset in oldAssets { + context.delete(cdAsset) + } + } + entity.assets = Set(assets) as NSSet } @@ -139,6 +207,7 @@ public final class ChainModelMapper { nodeEntity.url = node.url nodeEntity.name = node.name + nodeEntity.apiQueryName = node.apikey?.queryName nodeEntity.apiKeyName = node.apikey?.keyName return nodeEntity @@ -180,6 +249,7 @@ public final class ChainModelMapper { nodeEntity.url = node.url nodeEntity.name = node.name + nodeEntity.apiQueryName = node.apikey?.queryName nodeEntity.apiKeyName = node.apikey?.keyName return nodeEntity @@ -230,6 +300,7 @@ public final class ChainModelMapper { nodeEntity.url = node.url nodeEntity.name = node.name + nodeEntity.apiQueryName = node.apikey?.queryName nodeEntity.apiKeyName = node.apikey?.keyName entity.selectedNode = nodeEntity @@ -238,14 +309,12 @@ public final class ChainModelMapper { private func createExternalApi(from entity: CDChain) -> ChainModel.ExternalApiSet? { var staking: ChainModel.BlockExplorer? if let type = entity.stakingApiType, let url = entity.stakingApiUrl { - let apiKey = getBlockExplorerApiKey(for: type, chainId: entity.chainId) - staking = ChainModel.BlockExplorer(type: type, url: url, apiKey: apiKey) + staking = ChainModel.BlockExplorer(type: type, url: url) } var history: ChainModel.BlockExplorer? if let type = entity.historyApiType, let url = entity.historyApiUrl { - let apiKey = getBlockExplorerApiKey(for: type, chainId: entity.chainId) - history = ChainModel.BlockExplorer(type: type, url: url, apiKey: apiKey) + history = ChainModel.BlockExplorer(type: type, url: url) } var crowdloans: ChainModel.ExternalResource? @@ -253,6 +322,11 @@ public final class ChainModelMapper { crowdloans = ChainModel.ExternalResource(type: type, url: url) } + var pricing: ChainModel.BlockExplorer? + if let type = entity.pricingApiType, let url = entity.pricingApiUrl { + pricing = ChainModel.BlockExplorer(type: type, url: url) + } + let explorers = createExplorers(from: entity) if staking != nil || history != nil || crowdloans != nil || explorers != nil { @@ -260,7 +334,8 @@ public final class ChainModelMapper { staking: staking, history: history, crowdloans: crowdloans, - explorers: explorers + explorers: explorers, + pricing: pricing ) } else { return nil @@ -268,35 +343,43 @@ public final class ChainModelMapper { } private func createXcmConfig(from entity: CDChain) -> XcmChain? { - guard let versionRaw = entity.xcmConfig?.xcmVersion else { + guard let versionRaw = entity.xcmConfig?.xcmVersion, + let availableAssets = entity.xcmConfig?.availableAssets, + let availableDestinations = entity.xcmConfig?.availableDestinations else + { return nil } let version = XcmCallFactoryVersion(rawValue: versionRaw) - let availableXcmAssets = entity.xcmConfig?.availableAssets? - .allObjects as? [CDXcmAvailableAsset] ?? [] - let assets: [XcmAvailableAsset] = availableXcmAssets.compactMap { entity in - guard let id = entity.id, let symbol = entity.symbol else { + let assets: [XcmAvailableAsset] = availableAssets.compactMap { entity in + guard let entity = entity as? CDXcmAvailableAsset, + let id = entity.id, + let symbol = entity.symbol else + { return nil } - return XcmAvailableAsset(id: id, symbol: symbol) + return XcmAvailableAsset(id: id, symbol: symbol, minAmount: nil) } - let availableXcmAssetDestinations = entity.xcmConfig?.availableDestinations? - .allObjects as? [CDXcmAvailableDestination] ?? [] - let destinations: [XcmAvailableDestination] = availableXcmAssetDestinations.compactMap { - guard let chainId = $0.chainId else { + let destinations: [XcmAvailableDestination] = availableDestinations.compactMap { entity in + guard let entity = entity as? CDXcmAvailableDestination, + let chainId = entity.chainId, + let assetsEntities = entity.assets else + { return nil } - let assetsEntities = $0.assets?.allObjects as? [CDXcmAvailableAsset] ?? [] + let assets: [XcmAvailableAsset] = assetsEntities.compactMap { entity in - guard let id = entity.id, let symbol = entity.symbol else { + guard let entity = entity as? CDXcmAvailableAsset, + let id = entity.id, + let symbol = entity.symbol else + { return nil } - return XcmAvailableAsset(id: id, symbol: symbol) + return XcmAvailableAsset(id: id, symbol: symbol, minAmount: entity.minAmount) } return XcmAvailableDestination( chainId: chainId, - bridgeParachainId: $0.bridgeParachainId, + bridgeParachainId: entity.bridgeParachainId, assets: assets ) } @@ -353,14 +436,17 @@ public final class ChainModelMapper { } private func updateExternalApis(in entity: CDChain, from apis: ChainModel.ExternalApiSet?) { - entity.stakingApiType = apis?.staking?.type.rawValue + entity.stakingApiType = apis?.staking?.type?.rawValue entity.stakingApiUrl = apis?.staking?.url - entity.historyApiType = apis?.history?.type.rawValue + entity.historyApiType = apis?.history?.type?.rawValue entity.historyApiUrl = apis?.history?.url entity.crowdloansApiType = apis?.crowdloans?.type entity.crowdloansApiUrl = apis?.crowdloans?.url + + entity.pricingApiType = apis?.pricing?.type?.rawValue + entity.pricingApiUrl = apis?.pricing?.url } private func createChainAssetModelType(from rawValue: String?) -> SubstrateAssetType? { @@ -391,7 +477,9 @@ public final class ChainModelMapper { let configEntity = CDChainXcmConfig(context: context) configEntity.xcmVersion = xcmConfig.xcmVersion?.rawValue - configEntity.destWeightIsPrimitive = xcmConfig.destWeightIsPrimitive ?? false + if let destWeightIsPrimitive = xcmConfig.destWeightIsPrimitive { + configEntity.destWeightIsPrimitive = destWeightIsPrimitive + } let availableAssets = xcmConfig.availableAssets.map { let entity = CDXcmAvailableAsset(context: context) @@ -401,7 +489,7 @@ public final class ChainModelMapper { } configEntity.availableAssets = Set(availableAssets) as NSSet - let destinationEntities = xcmConfig.availableDestinations.compactMap { + let destinationEntities = xcmConfig.availableDestinations.map { let destinationEntity = CDXcmAvailableDestination(context: context) destinationEntity.chainId = $0.chainId @@ -409,6 +497,7 @@ public final class ChainModelMapper { let entity = CDXcmAvailableAsset(context: context) entity.id = $0.id entity.symbol = $0.symbol + entity.minAmount = $0.minAmount return entity } destinationEntity.assets = Set(availableAssets) as NSSet @@ -421,17 +510,6 @@ public final class ChainModelMapper { entity.xcmConfig = configEntity } - private func getBlockExplorerApiKey(for type: String, chainId: ChainModel.Id?) -> String? { - guard let blockExplorerType = BlockExplorerType(rawValue: type), - let chainId else - { - return nil - } - return apiKeyInjector.getBlockExplorerKey(for: blockExplorerType, chainId: chainId) - } -} - -extension ChainModelMapper: CoreDataMapperProtocol { public func transform(entity: CDChain) throws -> ChainModel { guard let chainId = entity.chainId else { throw ChainModelMapperError.missingChainId @@ -478,19 +556,17 @@ extension ChainModelMapper: CoreDataMapperProtocol { let externalApiSet = createExternalApi(from: entity) let xcm = createXcmConfig(from: entity) - var rank: UInt16? - if let rankString = entity.rank { - rank = UInt16(rankString) - } - let chainModel = ChainModel( - rank: rank, disabled: entity.disabled, chainId: chainId, parentId: entity.parentId, - paraId: nil, name: entity.name!, - tokens: ChainRemoteTokens(type: .config, whitelist: nil, utilityId: nil, tokens: []), + tokens: ChainRemoteTokens( + type: .config, + whitelist: nil, + utilityId: nil, + tokens: [] + ), xcm: xcm, nodes: Set(nodes), types: types, @@ -500,7 +576,14 @@ extension ChainModelMapper: CoreDataMapperProtocol { selectedNode: selectedNode, customNodes: customNodesSet, iosMinAppVersion: entity.minimalAppVersion, - properties: ChainProperties(addressPrefix: String(entity.addressPrefix)) + properties: ChainProperties( + addressPrefix: String(entity.addressPrefix), + rank: entity.rank, + paraId: entity.paraId, + ethereumBased: entity.isEthereumBased, + crowdloans: entity.hasCrowdloans + ), + identityChain: entity.identityChain ) let assetsArray: [AssetModel] = entity.assets.or([]).compactMap { anyAsset in @@ -527,16 +610,18 @@ extension ChainModelMapper: CoreDataMapperProtocol { from model: ChainModel, using context: NSManagedObjectContext ) throws { - if let rank = model.rank { + if let rank = model.properties.rank { entity.rank = "\(rank)" } entity.disabled = model.disabled entity.chainId = model.chainId + entity.paraId = model.properties.paraId entity.parentId = model.parentId entity.name = model.name entity.types = model.types?.url entity.typesOverrideCommon = model.types.map { NSNumber(value: $0.overridesCommon) } + entity.addressPrefix = Int16(bitPattern: UInt16(model.properties.addressPrefix) ?? 69) entity.icon = model.icon entity.isEthereumBased = model.isEthereumBased entity.isTestnet = model.isTestnet @@ -544,7 +629,7 @@ extension ChainModelMapper: CoreDataMapperProtocol { entity.isTipRequired = model.isTipRequired entity.minimalAppVersion = model.iosMinAppVersion entity.options = model.options?.map(\.rawValue) as? NSArray - + entity.identityChain = model.identityChain updateEntityAsset(for: entity, from: model, context: context) updateEntityNodes(for: entity, from: model, context: context) updateExternalApis(in: entity, from: model.externalApi) diff --git a/Sources/SSFAssetManagmentStorage/ChainRepositoryFactory.swift b/Sources/SSFAssetManagmentStorage/ChainRepositoryFactory.swift new file mode 100644 index 00000000..2c6908c0 --- /dev/null +++ b/Sources/SSFAssetManagmentStorage/ChainRepositoryFactory.swift @@ -0,0 +1,36 @@ +import Foundation +import RobinHood +import SSFModels +import SSFUtils + +public final class ChainRepositoryFactory { + let storageFacade: StorageFacadeProtocol + + public init(storageFacade: StorageFacadeProtocol = SubstrateDataStorageFacade.shared!) { + self.storageFacade = storageFacade + } + + public func createRepository( + for filter: NSPredicate? = nil, + sortDescriptors: [NSSortDescriptor] = [] + ) -> CoreDataRepository { + let mapper = ChainModelMapper() + return storageFacade.createRepository( + filter: filter, + sortDescriptors: sortDescriptors, + mapper: AnyCoreDataMapper(mapper) + ) + } + + public func createAsyncRepository( + for filter: NSPredicate? = nil, + sortDescriptors: [NSSortDescriptor] = [] + ) -> AsyncCoreDataRepositoryDefault { + let mapper = ChainModelMapper() + return storageFacade.createAsyncRepository( + filter: filter, + sortDescriptors: sortDescriptors, + mapper: AnyCoreDataMapper(mapper) + ) + } +} diff --git a/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/.xccurrentversion b/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/.xccurrentversion index 09ac93ea..f929d5f6 100644 --- a/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/.xccurrentversion +++ b/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - SubstrateDataModel_v5.xcdatamodel + SubstrateDataModel_v8.xcdatamodel diff --git a/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel.xcdatamodel/contents b/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel.xcdatamodel/contents index 94a62ff8..6ab456d3 100644 --- a/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel.xcdatamodel/contents +++ b/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -75,6 +75,10 @@ + + + + @@ -92,4 +96,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v2.xcdatamodel/contents b/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v2.xcdatamodel/contents index 931add12..f18cd528 100644 --- a/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v2.xcdatamodel/contents +++ b/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v2.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -121,6 +121,10 @@ + + + + diff --git a/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v3.xcdatamodel/contents b/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v3.xcdatamodel/contents index 47df51a3..e6e3fdc5 100644 --- a/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v3.xcdatamodel/contents +++ b/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v3.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -129,6 +129,10 @@ + + + + diff --git a/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v4.xcdatamodel/contents b/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v4.xcdatamodel/contents index fd56e97c..08890152 100644 --- a/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v4.xcdatamodel/contents +++ b/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v4.xcdatamodel/contents @@ -123,6 +123,10 @@ + + + + diff --git a/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v5.xcdatamodel/contents b/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v5.xcdatamodel/contents index d73ca316..792771c0 100644 --- a/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v5.xcdatamodel/contents +++ b/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v5.xcdatamodel/contents @@ -37,7 +37,10 @@ + + + @@ -136,6 +139,19 @@ + + + + + + + + + + + + + diff --git a/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v6.xcdatamodel/contents b/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v6.xcdatamodel/contents new file mode 100644 index 00000000..74c328ae --- /dev/null +++ b/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v6.xcdatamodel/contents @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v7.xcdatamodel/contents b/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v7.xcdatamodel/contents new file mode 100644 index 00000000..251f73f7 --- /dev/null +++ b/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v7.xcdatamodel/contents @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v8.xcdatamodel/contents b/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v8.xcdatamodel/contents new file mode 100644 index 00000000..a82226c7 --- /dev/null +++ b/Sources/SSFAssetManagmentStorage/Resources/SubstrateDataModel.xcdatamodeld/SubstrateDataModel_v8.xcdatamodel/contents @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Sources/SSFChainlinkProvider/ChainlinkOperationFactory.swift b/Sources/SSFChainlinkProvider/ChainlinkOperationFactory.swift new file mode 100644 index 00000000..79e47803 --- /dev/null +++ b/Sources/SSFChainlinkProvider/ChainlinkOperationFactory.swift @@ -0,0 +1,98 @@ +import Foundation +import RobinHood +import SSFChainRegistry +import SSFModels +import SSFNetwork +import SSFUtils +import Web3 +import Web3ContractABI + +public protocol ChainlinkOperationFactoryProtocol { + func priceCall(for chainAsset: ChainAsset, connection: Web3.Eth?) -> BaseOperation? +} + +public final class ChainlinkOperationFactory: ChainlinkOperationFactoryProtocol { + private let chainRegistry: ChainRegistryProtocol + + public init(chainRegistry: ChainRegistryProtocol) { + self.chainRegistry = chainRegistry + } + + public func priceCall(for chainAsset: ChainAsset, connection: Web3.Eth?) -> BaseOperation? { + let operation = ManualOperation() + + do { + guard let contract = chainAsset.asset.priceProvider?.id else { + throw ConvenienceError(error: "Missing price contract address") + } + guard let ws = connection else { + throw ConvenienceError( + error: "Can't get ethereum connection for chain: \(chainAsset.chain.name)" + ) + } + + let receiverAddress = try EthereumAddress(rawAddress: contract.hexToBytes()) + + let outputs: [ABI.Element.InOut] = [ + ABI.Element.InOut(name: "roundId", type: .uint(bits: 80)), + ABI.Element.InOut(name: "answer", type: .int(bits: 256)), + ABI.Element.InOut(name: "startedAt", type: .int(bits: 256)), + ABI.Element.InOut(name: "updatedAt", type: .int(bits: 256)), + ABI.Element.InOut(name: "answeredInRound", type: .uint(bits: 80)), + ] + let method = ABI.Element.Function( + name: "latestRoundData", + inputs: [], + outputs: outputs, + constant: false, + payable: false + ) + let priceCall = try EthereumCall( + to: receiverAddress, + value: EthereumQuantity(quantity: .zero), + data: EthereumData(ethereumValue: .string(method.methodString)) + ) + ws.call(call: priceCall, block: .latest) { resp in + switch resp.status { + case let .success(result): + let decoded = ABIDecoder.decode( + types: outputs, + data: Data(hex: result.hex()) + ) + guard let price: BigInt = decoded?[safe: 1] as? BigInt, + let precision = chainAsset.asset.priceProvider?.precision, + let priceDecimal = Decimal.fromSubstrateAmount( + BigUInt(price), + precision: precision + ) else + { + let error = ConvenienceError(error: "Decoding price error") + operation.result = .failure(error) + operation.finish() + return + } + + let priceData = PriceData( + currencyId: "usd", + priceId: contract, + price: "\(priceDecimal)", + fiatDayChange: nil, + coingeckoPriceId: chainAsset.asset.coingeckoPriceId + ) + operation.result = .success(priceData) + operation.finish() + + case let .failure(error): + operation.result = .failure(error) + operation.finish() + } + } + } catch { + operation.result = .failure(error) + operation.finish() + return nil + } + + return operation + } +} diff --git a/Sources/SSFChainlinkProvider/ChainlinkService.swift b/Sources/SSFChainlinkProvider/ChainlinkService.swift new file mode 100644 index 00000000..a971ff2f --- /dev/null +++ b/Sources/SSFChainlinkProvider/ChainlinkService.swift @@ -0,0 +1,51 @@ +import RobinHood +import SSFChainRegistry +import SSFModels +import SSFUtils +import SSFPrices + +public final class ChainlinkService: PriceProviderServiceProtocol { + private let chainlinkOperationFactory: ChainlinkOperationFactoryProtocol + private let chainRegistry: ChainRegistryProtocol + + public init( + chainlinkOperationFactory: ChainlinkOperationFactory, + chainRegistry: ChainRegistryProtocol + ) { + self.chainlinkOperationFactory = chainlinkOperationFactory + self.chainRegistry = chainRegistry + } + + public func getPrices(for chainAssets: [ChainAsset], currencies: [Currency]) async -> [PriceData] { + let operations = await createChainlinkOperations(for: chainAssets, currencies: currencies) + return operations.compactMap { + try? $0.extractNoCancellableResultData() + } + } + + private func createChainlinkOperations( + for chainAssets: [ChainAsset], + currencies: [Currency] + ) async -> [BaseOperation] { + guard currencies.count == 1, currencies.first?.id == Currency.defaultCurrency().id else { + return [] + } + let chainlinkProvider = chainAssets.map { $0.chain } + .first(where: { $0.options?.contains(.chainlinkProvider) == true }) + guard let provider = chainlinkProvider else { + return [] + } + do { + let connection = try await chainRegistry.getEthereumConnection(for: provider) + let chainlinkPriceChainAsset = chainAssets + .filter { $0.asset.priceProvider?.type == .chainlink } + + let operations = chainlinkPriceChainAsset + .map { chainlinkOperationFactory.priceCall(for: $0, connection: connection) } + return operations.compactMap { $0 } + } catch { + print("can't create ethereum connection for \(provider.name)") + return [] + } + } +} diff --git a/Sources/SSFChainlinkProvider/eip_mew/ABI/ABI.swift b/Sources/SSFChainlinkProvider/eip_mew/ABI/ABI.swift new file mode 100644 index 00000000..643759a1 --- /dev/null +++ b/Sources/SSFChainlinkProvider/eip_mew/ABI/ABI.swift @@ -0,0 +1,26 @@ +// +// Created by Alex Vlasov on 25/10/2018. +// Copyright © 2018 Alex Vlasov. All rights reserved. +// + +import Foundation + +public struct ABI {} + +protocol ABIElementPropertiesProtocol { + var isStatic: Bool { get } + var isArray: Bool { get } + var isTuple: Bool { get } + var arraySize: ABI.Element.ArraySize { get } + var subtype: ABI.Element.ParameterType? { get } + var memoryUsage: UInt64 { get } + var emptyValue: Any { get } +} + +protocol ABIEncoding { + var abiRepresentation: String { get } +} + +protocol ABIValidation { + var isValid: Bool { get } +} diff --git a/Sources/SSFChainlinkProvider/eip_mew/ABI/ABIDecoding.swift b/Sources/SSFChainlinkProvider/eip_mew/ABI/ABIDecoding.swift new file mode 100644 index 00000000..7a9129f4 --- /dev/null +++ b/Sources/SSFChainlinkProvider/eip_mew/ABI/ABIDecoding.swift @@ -0,0 +1,323 @@ +// +// Created by Alex Vlasov on 25/10/2018. +// Copyright © 2018 Alex Vlasov. All rights reserved. +// + +import BigInt +import CryptoSwift +import Foundation + +// swiftlint:disable identifier_name + +public struct ABIDecoder {} + +public extension ABIDecoder { + static func decode(types: [ABI.Element.InOut], data: Data) -> [AnyObject]? { + let params = types.compactMap { el -> ABI.Element.ParameterType in + el.type + } + return decode(types: params, data: data) + } + + static func decode(types: [ABI.Element.ParameterType], data: Data) -> [AnyObject]? { + // print("Full data: \n" + data.toHexString()) + var toReturn = [AnyObject]() + var consumed: UInt64 = 0 + for i in 0 ..< types.count { + let (v, c) = decodeSignleType(type: types[i], data: data, pointer: consumed) + guard let valueUnwrapped = v, let consumedUnwrapped = c else { return nil } + toReturn.append(valueUnwrapped) + consumed += consumedUnwrapped + } + guard toReturn.count == types.count else { return nil } + return toReturn + } + + // swiftlint:disable:next function_body_length cyclomatic_complexity + static func decodeSignleType( + type: ABI.Element.ParameterType, + data: Data, + pointer: UInt64 = 0 + ) -> (value: AnyObject?, bytesConsumed: UInt64?) { + let (elData, nextPtr) = followTheData(type: type, data: data, pointer: pointer) + guard let elementItself = elData, let nextElementPointer = nextPtr else { + return (nil, nil) + } + switch type { + case let .uint(bits): + // print("Uint256 element itself: \n" + elementItself.toHexString()) + guard elementItself.count >= 32 else { break } + let mod = BigUInt(1) << bits + let dataSlice = elementItself[0 ..< 32] + let v = BigUInt(dataSlice) % mod + // print("Uint256 element is: \n" + String(v)) + return (v as AnyObject, type.memoryUsage) + case let .int(bits): + // print("Int256 element itself: \n" + elementItself.toHexString()) + guard elementItself.count >= 32 else { break } + let mod = BigInt(1) << bits + let dataSlice = elementItself[0 ..< 32] + let v = BigInt.fromTwosComplement(data: dataSlice) % mod + // print("Int256 element is: \n" + String(v)) + return (v as AnyObject, type.memoryUsage) + case .address: + // print("Address element itself: \n" + elementItself.toHexString()) + guard elementItself.count >= 32 else { break } + let dataSlice = elementItself[12 ..< 32] + let address = Address(data: dataSlice) + // print("Address element is: \n" + String(address.address)) + return (address as AnyObject, type.memoryUsage) + case .bool: + // print("Bool element itself: \n" + elementItself.toHexString()) + guard elementItself.count >= 32 else { break } + let dataSlice = elementItself[0 ..< 32] + let v = BigUInt(dataSlice) + // print("Address element is: \n" + String(v)) + if v == BigUInt(36) || + v == BigUInt(32) || + v == BigUInt(28) || + v == BigUInt(1) + { + return (true as AnyObject, type.memoryUsage) + } else if v == BigUInt(35) || + v == BigUInt(31) || + v == BigUInt(27) || + v == BigUInt(0) + { + return (false as AnyObject, type.memoryUsage) + } + case let .bytes(length): + // print("Bytes32 element itself: \n" + elementItself.toHexString()) + guard elementItself.count >= 32 else { break } + let dataSlice = elementItself[0 ..< length] + // print("Bytes32 element is: \n" + String(dataSlice.toHexString())) + return (dataSlice as AnyObject, type.memoryUsage) + case .string: + // print("String element itself: \n" + elementItself.toHexString()) + guard elementItself.count >= 32 else { break } + var dataSlice = elementItself[0 ..< 32] + let length = UInt64(BigUInt(dataSlice)) + guard elementItself.count >= 32 + length else { break } + dataSlice = elementItself[32 ..< 32 + length] + guard let string = String(data: dataSlice, encoding: .utf8) else { break } + // print("String element is: \n" + String(string)) + return (string as AnyObject, type.memoryUsage) + case .dynamicBytes: + // print("Bytes element itself: \n" + elementItself.toHexString()) + guard elementItself.count >= 32 else { break } + var dataSlice = elementItself[0 ..< 32] + let length = UInt64(BigUInt(dataSlice)) + guard elementItself.count >= 32 + length else { break } + dataSlice = elementItself[32 ..< 32 + length] + // print("Bytes element is: \n" + String(dataSlice.toHexString())) + return (dataSlice as AnyObject, type.memoryUsage) + case let .array(type: subType, length: length): + switch type.arraySize { + case .dynamicSize: + // print("Dynamic array element itself: \n" + + // elementItself.toHexString()) + if subType.isStatic { + // uint[] like, expect length and elements + guard elementItself.count >= 32 else { break } + var dataSlice = elementItself[0 ..< 32] + let length = UInt64(BigUInt(dataSlice)) + guard elementItself.count >= 32 + subType.memoryUsage * length else { break } + dataSlice = elementItself[32 ..< 32 + subType.memoryUsage * length] + var subpointer: UInt64 = 32 + var toReturn = [AnyObject]() + for _ in 0 ..< length { + let (v, c) = decodeSignleType( + type: subType, + data: elementItself, + pointer: subpointer + ) + guard let valueUnwrapped = v, let consumedUnwrapped = c else { break } + toReturn.append(valueUnwrapped) + subpointer += consumedUnwrapped + } + return (toReturn as AnyObject, type.memoryUsage) + } else { + // in principle is true for tuple[], so will work for string[] too + guard elementItself.count >= 32 else { break } + var dataSlice = elementItself[0 ..< 32] + let length = UInt64(BigUInt(dataSlice)) + guard elementItself.count >= 32 else { break } + dataSlice = Data(elementItself[32 ..< elementItself.count]) + var subpointer: UInt64 = 0 + var toReturn = [AnyObject]() + // print("Dynamic array sub element itself: \n" + + // dataSlice.toHexString()) + for _ in 0 ..< length { + let (v, c) = decodeSignleType( + type: subType, + data: dataSlice, + pointer: subpointer + ) + guard let valueUnwrapped = v, let consumedUnwrapped = c else { break } + toReturn.append(valueUnwrapped) + subpointer += consumedUnwrapped + } + return (toReturn as AnyObject, nextElementPointer) + } + case let .staticSize(staticLength): + // print("Static array element itself: \n" + + // elementItself.toHexString()) + guard length == staticLength else { break } + var toReturn = [AnyObject]() + var consumed: UInt64 = 0 + for _ in 0 ..< length { + let (v, c) = decodeSignleType( + type: subType, + data: elementItself, + pointer: consumed + ) + guard let valueUnwrapped = v, + let consumedUnwrapped = c else { return (nil, nil) } + toReturn.append(valueUnwrapped) + consumed += consumedUnwrapped + } + if subType.isStatic { + return (toReturn as AnyObject, consumed) + } else { + return (toReturn as AnyObject, nextElementPointer) + } + case .notArray: + break + } + case let .tuple(types: subTypes): + // print("Tuple element itself: \n" + elementItself.toHexString()) + var toReturn = [AnyObject]() + var consumed: UInt64 = 0 + for i in 0 ..< subTypes.count { + let (v, c) = decodeSignleType( + type: subTypes[i], + data: elementItself, + pointer: consumed + ) + guard let valueUnwrapped = v, let consumedUnwrapped = c else { return (nil, nil) } + toReturn.append(valueUnwrapped) + consumed += consumedUnwrapped + } + // print("Tuple element is: \n" + String(describing: toReturn)) + if type.isStatic { + return (toReturn as AnyObject, consumed) + } else { + return (toReturn as AnyObject, nextElementPointer) + } + case .function: + // print("Function element itself: \n" + elementItself.toHexString()) + guard elementItself.count >= 32 else { break } + let dataSlice = elementItself[8 ..< 32] + // print("Function element is: \n" + String(dataSlice.toHexString())) + return (dataSlice as AnyObject, type.memoryUsage) + } + return (nil, nil) + } + + fileprivate static func followTheData( + type: ABI.Element.ParameterType, + data: Data, + pointer: UInt64 = 0 + ) -> (elementEncoding: Data?, nextElementPointer: UInt64?) { + // print("Follow the data: \n" + data.toHexString()) + // print("At pointer: \n" + String(pointer)) + if type.isStatic { + guard data.count >= pointer + type.memoryUsage else { return (nil, nil) } + let elementItself = data[pointer ..< pointer + type.memoryUsage] + let nextElement = pointer + type.memoryUsage + // print("Got element itself: \n" + elementItself.toHexString()) + // print("Next element pointer: \n" + String(nextElement)) + return (Data(elementItself), nextElement) + } else { + guard data.count >= pointer + type.memoryUsage else { return (nil, nil) } + let dataSlice = data[pointer ..< pointer + type.memoryUsage] + let bn = BigUInt(dataSlice) + if bn > UINT64_MAX || bn >= data.count { + // there are ERC20 contracts that use bytes32 intead of string. Let's be optimistic + // and return some data + if case .string = type { + let nextElement = pointer + type.memoryUsage + let preambula = BigUInt(32).abiEncode(bits: 256)! + return (preambula + Data(dataSlice), nextElement) + } else if case .dynamicBytes = type { + let nextElement = pointer + type.memoryUsage + let preambula = BigUInt(32).abiEncode(bits: 256)! + return (preambula + Data(dataSlice), nextElement) + } + return (nil, nil) + } + let elementPointer = UInt64(bn) + let elementItself = data[elementPointer ..< UInt64(data.count)] + let nextElement = pointer + type.memoryUsage + // print("Got element itself: \n" + elementItself.toHexString()) + // print("Next element pointer: \n" + String(nextElement)) + return (Data(elementItself), nextElement) + } + } + + static func decodeLog( + event: ABI.Element.Event, + eventLogTopics: [Data], + eventLogData: Data + ) -> [String: Any]? { + if event.topic != eventLogTopics[0], !event.anonymous { + return nil + } + var eventContent = [String: Any]() + eventContent["name"] = event.name + let logs = eventLogTopics + let dataForProcessing = eventLogData + let indexedInputs = event.inputs.filter { inp -> Bool in + inp.indexed + } + if logs.count == 1, !indexedInputs.isEmpty { + return nil + } + let nonIndexedInputs = event.inputs.filter { inp -> Bool in + !inp.indexed + } + let nonIndexedTypes = nonIndexedInputs.compactMap { inp -> ABI.Element.ParameterType in + inp.type + } + guard logs.count == indexedInputs.count + 1 else { return nil } + var indexedValues = [AnyObject]() + for i in 0 ..< indexedInputs.count { + let data = logs[i + 1] + let input = indexedInputs[i] + if !input.type.isStatic || input.type.isArray || input.type.memoryUsage != 32 { + let (v, _) = ABIDecoder.decodeSignleType(type: .bytes(length: 32), data: data) + guard let valueUnwrapped = v else { return nil } + indexedValues.append(valueUnwrapped) + } else { + let (v, _) = ABIDecoder.decodeSignleType(type: input.type, data: data) + guard let valueUnwrapped = v else { return nil } + indexedValues.append(valueUnwrapped) + } + } + let v = ABIDecoder.decode(types: nonIndexedTypes, data: dataForProcessing) + guard let nonIndexedValues = v else { return nil } + var indexedInputCounter = 0 + var nonIndexedInputCounter = 0 + for i in 0 ..< event.inputs.count { + let el = event.inputs[i] + if el.indexed { + let name = "\(i)" + let value = indexedValues[indexedInputCounter] + eventContent[name] = value + if el.name != "" { + eventContent[el.name] = value + } + indexedInputCounter += 1 + } else { + let name = "\(i)" + let value = nonIndexedValues[nonIndexedInputCounter] + eventContent[name] = value + if el.name != "" { + eventContent[el.name] = value + } + nonIndexedInputCounter += 1 + } + } + return eventContent + } +} diff --git a/Sources/SSFChainlinkProvider/eip_mew/ABI/ABIElements.swift b/Sources/SSFChainlinkProvider/eip_mew/ABI/ABIElements.swift new file mode 100644 index 00000000..07ba2e1f --- /dev/null +++ b/Sources/SSFChainlinkProvider/eip_mew/ABI/ABIElements.swift @@ -0,0 +1,296 @@ +// +// Created by Alex Vlasov on 25/10/2018. +// Copyright © 2018 Alex Vlasov. All rights reserved. +// + +import Foundation +import Web3ContractABI + +public extension ABI { + // JSON Decoding + struct Input: Decodable { + public var name: String? + public var type: String + public var indexed: Bool? + public var components: [Input]? + } + + struct Output: Decodable { + public var name: String? + public var type: String + public var components: [Output]? + } + + struct Record: Decodable { + public var name: String? + public var type: String? + public var payable: Bool? + public var constant: Bool? + public var stateMutability: String? + public var inputs: [ABI.Input]? + public var outputs: [ABI.Output]? + public var anonymous: Bool? + } + + enum Element { + public enum ArraySize { // bytes for convenience + case staticSize(UInt64) + case dynamicSize + case notArray + } + + case function(Function) + case constructor(Constructor) + case fallback(Fallback) + case event(Event) + + public enum StateMutability { + case payable + case mutating + case view + case pure + + var isConstant: Bool { + switch self { + case .payable: + return false + case .mutating: + return false + default: + return true + } + } + + var isPayable: Bool { + switch self { + case .payable: + return true + default: + return false + } + } + } + + public typealias InOutName = String + + public struct InOut: Equatable { + public let name: InOutName + public let type: ParameterType + + public init(name: InOutName, type: ParameterType) { + self.name = name + self.type = type + } + } + + public struct Function: Equatable { + public let name: String? + public let inputs: [InOut] + public let outputs: [InOut] + public let stateMutability: StateMutability? = nil + public let constant: Bool + public let payable: Bool + + public init( + name: String?, + inputs: [InOut], + outputs: [InOut], + constant: Bool, + payable: Bool + ) { + self.name = name + self.inputs = inputs + self.outputs = outputs + self.constant = constant + self.payable = payable + } + } + + public struct Constructor { + public let inputs: [InOut] + public let constant: Bool + public let payable: Bool + public init(inputs: [InOut], constant: Bool, payable: Bool) { + self.inputs = inputs + self.constant = constant + self.payable = payable + } + } + + public struct Fallback { + public let constant: Bool + public let payable: Bool + + public init(constant: Bool, payable: Bool) { + self.constant = constant + self.payable = payable + } + } + + public struct Event { + public let name: String + public let inputs: [Input] + public let anonymous: Bool + + public init(name: String, inputs: [Input], anonymous: Bool) { + self.name = name + self.inputs = inputs + self.anonymous = anonymous + } + + public struct Input { + public let name: String + public let type: ParameterType + public let indexed: Bool + + public init(name: String, type: ParameterType, indexed: Bool) { + self.name = name + self.type = type + self.indexed = indexed + } + } + } + } +} + +public extension ABI.Element { + func encodeParameters(_ parameters: [AnyObject]) -> Data? { + switch self { + case let .constructor(constructor): + guard parameters.count == constructor.inputs.count else { return nil } + guard let data = ABIEncoder.encode(types: constructor.inputs, values: parameters) else { return nil } + return data + case .event: + return nil + case .fallback: + return nil + case let .function(function): + guard parameters.count == function.inputs.count else { return nil } + let signature = function.methodEncoding + guard let data = ABIEncoder.encode(types: function.inputs, values: parameters) else { return nil } + return signature + data + } + } +} + +public extension ABI.Element { + func decodeReturnData(_ data: Data) -> [String: Any]? { + switch self { + case .constructor: + return nil + case .event: + return nil + case .fallback: + return nil + case let .function(function): + if data.isEmpty, function.outputs.count == 1 { + let name = "0" + let value = function.outputs[0].type.emptyValue + var returnArray = [String: Any]() + returnArray[name] = value + if function.outputs[0].name != "" { + returnArray[function.outputs[0].name] = value + } + return returnArray + } + + guard function.outputs.count * 32 <= data.count else { return nil } + var returnArray = [String: Any]() + var i = 0 + guard let values = ABIDecoder.decode(types: function.outputs, data: data) else { return nil } + for output in function.outputs { + let name = "\(i)" + returnArray[name] = values[i] + if output.name != "" { + returnArray[output.name] = values[i] + } + i += 1 + } + return returnArray + } + } + + func decodeInputData(_ rawData: Data) -> [String: Any]? { + var data = rawData + var sig: Data? + switch rawData.count % 32 { + case 0: + break + case 4: + sig = rawData[0 ..< 4] + data = Data(rawData[4 ..< rawData.count]) + default: + return nil + } + switch self { + case let .constructor(function): + if data.isEmpty, function.inputs.count == 1 { + let name = "0" + let value = function.inputs[0].type.emptyValue + var returnArray = [String: Any]() + returnArray[name] = value + if function.inputs[0].name != "" { + returnArray[function.inputs[0].name] = value + } + return returnArray + } + + guard function.inputs.count * 32 <= data.count else { return nil } + var returnArray = [String: Any]() + var i = 0 + guard let values = ABIDecoder.decode(types: function.inputs, data: data) else { return nil } + for input in function.inputs { + let name = "\(i)" + returnArray[name] = values[i] + if input.name != "" { + returnArray[input.name] = values[i] + } + i += 1 + } + return returnArray + case .event: + return nil + case .fallback: + return nil + case let .function(function): + if sig != nil, sig != function.methodEncoding { + return nil + } + if data.isEmpty, function.inputs.count == 1 { + let name = "0" + let value = function.inputs[0].type.emptyValue + var returnArray = [String: Any]() + returnArray[name] = value + if function.inputs[0].name != "" { + returnArray[function.inputs[0].name] = value + } + return returnArray + } + + guard function.inputs.count * 32 <= data.count else { return nil } + var returnArray = [String: Any]() + var i = 0 + guard let values = ABIDecoder.decode(types: function.inputs, data: data) else { return nil } + for input in function.inputs { + let name = "\(i)" + returnArray[name] = values[i] + if input.name != "" { + returnArray[input.name] = values[i] + } + i += 1 + } + return returnArray + } + } +} + +public extension ABI.Element.Event { + func decodeReturnedLogs(eventLogTopics: [Data], eventLogData: Data) -> [String: Any]? { + guard let eventContent = ABIDecoder.decodeLog( + event: self, + eventLogTopics: eventLogTopics, + eventLogData: eventLogData + ) else { return nil } + return eventContent + } +} diff --git a/Sources/SSFChainlinkProvider/eip_mew/ABI/ABIEncoding.swift b/Sources/SSFChainlinkProvider/eip_mew/ABI/ABIEncoding.swift new file mode 100644 index 00000000..ba3279be --- /dev/null +++ b/Sources/SSFChainlinkProvider/eip_mew/ABI/ABIEncoding.swift @@ -0,0 +1,398 @@ +// +// Created by Alex Vlasov on 25/10/2018. +// Copyright © 2018 Alex Vlasov. All rights reserved. +// + +import BigInt +import CryptoSwift +import Foundation +import SSFUtils + +// swiftlint:disable identifier_name + +public struct ABIEncoder {} + +public extension ABIEncoder { + static func convertToBigUInt(_ value: AnyObject) -> BigUInt? { + switch value { + case let v as BigUInt: + return v + case let v as BigInt: + switch v.sign { + case .minus: + return nil + case .plus: + return v.magnitude + } + case let v as String: + let base10 = BigUInt(v, radix: 10) + if base10 != nil { + return base10! + } + let base16 = BigUInt(v.stringRemoveHexPrefix(), radix: 16) + if base16 != nil { + return base16! + } + case let v as UInt: + return BigUInt(v) + case let v as UInt8: + return BigUInt(v) + case let v as UInt16: + return BigUInt(v) + case let v as UInt32: + return BigUInt(v) + case let v as UInt64: + return BigUInt(v) + case let v as Int: + if v < 0 { return nil } + return BigUInt(v) + case let v as Int8: + if v < 0 { return nil } + return BigUInt(v) + case let v as Int16: + if v < 0 { return nil } + return BigUInt(v) + case let v as Int32: + if v < 0 { return nil } + return BigUInt(v) + case let v as Int64: + if v < 0 { return nil } + return BigUInt(v) + default: + return nil + } + return nil + } + + static func convertToBigInt(_ value: AnyObject) -> BigInt? { + switch value { + case let v as BigUInt: + return BigInt(v) + case let v as BigInt: + return v + case let v as String: + let base10 = BigInt(v, radix: 10) + if base10 != nil { + return base10 + } + let base16 = BigInt(v.stringRemoveHexPrefix(), radix: 16) + if base16 != nil { + return base16 + } + case let v as UInt: + return BigInt(v) + case let v as UInt8: + return BigInt(v) + case let v as UInt16: + return BigInt(v) + case let v as UInt32: + return BigInt(v) + case let v as UInt64: + return BigInt(v) + case let v as Int: + return BigInt(v) + case let v as Int8: + return BigInt(v) + case let v as Int16: + return BigInt(v) + case let v as Int32: + return BigInt(v) + case let v as Int64: + return BigInt(v) + default: + return nil + } + return nil + } + + static func convertToData(_ value: AnyObject, type: ABI.Element.ParameterType) -> Data? { + switch value { + case let d as Data: + return d + case let d as String: + if type.isNumber { + guard let value = IntegerLiteralType(d) else { return nil } + return convertToData(value as AnyObject, type: type) + } + if d.hasHexPrefix() { + return Data(hex: d) + } + let str = d.data(using: .utf8) + if str != nil { + return str + } + case let d as [UInt8]: + return Data(d) + case let d as Address: + return d.data + case let d as [IntegerLiteralType]: + var bytesArray = [UInt8]() + for el in d { + guard el >= 0, el <= 255 else { return nil } + bytesArray.append(UInt8(el)) + } + return Data(bytesArray) + case is IntegerLiteralType: + return convertToBigInt(value)?.toTwosComplement() + default: + return nil + } + return nil + } + + static func encode(types: [ABI.Element.InOut], values: [AnyObject]) -> Data? { + guard types.count == values.count else { return nil } + let params = types.compactMap { el -> ABI.Element.ParameterType in + el.type + } + return encode(types: params, values: values) + } + + static func encode(types: [ABI.Element.ParameterType], values: [AnyObject]) -> Data? { + guard types.count == values.count else { return nil } + var tails = [Data]() + var heads = [Data]() + for i in 0 ..< types.count { + let enc = encodeSingleType(type: types[i], value: values[i]) + guard let encoding = enc else { return nil } + if types[i].isStatic { + heads.append(encoding) + tails.append(Data()) + } else { + heads.append(Data(repeating: 0x0, count: 32)) + tails.append(encoding) + } + } + var headsConcatenated = Data() + for head in heads { + headsConcatenated.append(head) + } + var tailsPointer = BigUInt(headsConcatenated.count) + headsConcatenated = Data() + var tailsConcatenated = Data() + for i in 0 ..< types.count { + let head = heads[i] + let tail = tails[i] + if !types[i].isStatic { + guard let newHead = tailsPointer.abiEncode(bits: 256) else { return nil } + headsConcatenated.append(newHead) + tailsConcatenated.append(tail) + tailsPointer += BigUInt(tail.count) + } else { + headsConcatenated.append(head) + tailsConcatenated.append(tail) + } + } + return headsConcatenated + tailsConcatenated + } + + // swiftlint:disable:next function_body_length cyclomatic_complexity + static func encodeSingleType(type: ABI.Element.ParameterType, value: AnyObject) -> Data? { + switch type { + case .uint: + if let biguint = convertToBigUInt(value) { + return biguint.abiEncode(bits: 256) + } + if let bigint = convertToBigInt(value) { + return bigint.abiEncode(bits: 256) + } + case .int: + if let biguint = convertToBigUInt(value) { + return biguint.abiEncode(bits: 256) + } + if let bigint = convertToBigInt(value) { + return bigint.abiEncode(bits: 256) + } + case .address: + if let string = value as? String { + guard let address = Address(address: string) else { return nil } + let data = address.data + return data.setLengthLeft(32) + } else if let address = value as? Address { + let data = address.data + return data.setLengthLeft(32) + } else if let data = value as? Data { + return data.setLengthLeft(32) + } + case .bool: + if let bool = value as? Bool { + if bool { + return BigUInt(1).abiEncode(bits: 256) + } else { + return BigUInt(0).abiEncode(bits: 256) + } + } + case let .bytes(length): + guard let data = convertToData(value, type: type) else { break } + if data.count > length { break } + return data.setLengthRight(32) + case .string: + if let string = value as? String { + var dataGuess: Data? + if string.hasHexPrefix() { + dataGuess = Data(hex: string.lowercased()) + } else { + dataGuess = string.data(using: .utf8) + } + guard let data = dataGuess else { break } + let minLength = ((data.count + 31) / 32) * 32 + let paddedData = data.setLengthRight(minLength) + let length = BigUInt(data.count) + guard let head = length.abiEncode(bits: 256) else { break } + let total = head + paddedData + return total + } + case .dynamicBytes: + guard let data = convertToData(value, type: type) else { break } + let minLength = ((data.count + 31) / 32) * 32 + let paddedData = data.setLengthRight(minLength) + let length = BigUInt(data.count) + guard let head = length.abiEncode(bits: 256) else { break } + let total = head + paddedData + return total + case let .array(type: subType, length: length): + switch type.arraySize { + case .dynamicSize: + guard length == 0 else { break } + guard let val = value as? [AnyObject] else { break } + guard let lengthEncoding = BigUInt(val.count).abiEncode(bits: 256) else { break } + if subType.isStatic { + // work in a previous context + var toReturn = Data() + for i in 0 ..< val.count { + let enc = encodeSingleType(type: subType, value: val[i]) + guard let encoding = enc else { break } + toReturn.append(encoding) + } + let total = lengthEncoding + toReturn + // print("Dynamic array of static types encoding :\n" + + // String(total.toHexString())) + return total + } else { + // create new context + var tails = [Data]() + var heads = [Data]() + for i in 0 ..< val.count { + let enc = encodeSingleType(type: subType, value: val[i]) + guard let encoding = enc else { return nil } + heads.append(Data(repeating: 0x0, count: 32)) + tails.append(encoding) + } + var headsConcatenated = Data() + for h in heads { + headsConcatenated.append(h) + } + var tailsPointer = BigUInt(headsConcatenated.count) + headsConcatenated = Data() + var tailsConcatenated = Data() + for i in 0 ..< val.count { + let head = heads[i] + let tail = tails[i] + if tail != Data() { + guard let newHead = tailsPointer.abiEncode(bits: 256) else { return nil } + headsConcatenated.append(newHead) + tailsConcatenated.append(tail) + tailsPointer += BigUInt(tail.count) + } else { + headsConcatenated.append(head) + tailsConcatenated.append(tail) + } + } + let total = lengthEncoding + headsConcatenated + tailsConcatenated + // print("Dynamic array of dynamic types encoding :\n" + + // String(total.toHexString())) + return total + } + case let .staticSize(staticLength): + guard staticLength != 0 else { break } + guard let val = value as? [AnyObject] else { break } + guard staticLength == val.count else { break } + if subType.isStatic { + // work in a previous context + var toReturn = Data() + for i in 0 ..< val.count { + let enc = encodeSingleType(type: subType, value: val[i]) + guard let encoding = enc else { break } + toReturn.append(encoding) + } + // print("Static array of static types encoding :\n" + + // String(toReturn.toHexString())) + let total = toReturn + return total + } else { + // create new context + var tails = [Data]() + var heads = [Data]() + for i in 0 ..< val.count { + let enc = encodeSingleType(type: subType, value: val[i]) + guard let encoding = enc else { return nil } + heads.append(Data(repeating: 0x0, count: 32)) + tails.append(encoding) + } + var headsConcatenated = Data() + for h in heads { + headsConcatenated.append(h) + } + var tailsPointer = BigUInt(headsConcatenated.count) + headsConcatenated = Data() + var tailsConcatenated = Data() + for i in 0 ..< val.count { + let tail = tails[i] + guard let newHead = tailsPointer.abiEncode(bits: 256) else { return nil } + headsConcatenated.append(newHead) + tailsConcatenated.append(tail) + tailsPointer += BigUInt(tail.count) + } + let total = headsConcatenated + tailsConcatenated + // print("Static array of dynamic types encoding :\n" + + // String(total.toHexString())) + return total + } + case .notArray: + break + } + case let .tuple(types: subTypes): + var tails = [Data]() + var heads = [Data]() + guard let val = value as? [AnyObject] else { break } + for i in 0 ..< subTypes.count { + let enc = encodeSingleType(type: subTypes[i], value: val[i]) + guard let encoding = enc else { return nil } + if subTypes[i].isStatic { + heads.append(encoding) + tails.append(Data()) + } else { + heads.append(Data(repeating: 0x0, count: 32)) + tails.append(encoding) + } + } + var headsConcatenated = Data() + for h in heads { + headsConcatenated.append(h) + } + var tailsPointer = BigUInt(headsConcatenated.count) + headsConcatenated = Data() + var tailsConcatenated = Data() + for i in 0 ..< subTypes.count { + let head = heads[i] + let tail = tails[i] + if !subTypes[i].isStatic { + guard let newHead = tailsPointer.abiEncode(bits: 256) else { return nil } + headsConcatenated.append(newHead) + tailsConcatenated.append(tail) + tailsPointer += BigUInt(tail.count) + } else { + headsConcatenated.append(head) + tailsConcatenated.append(tail) + } + } + let total = headsConcatenated + tailsConcatenated + return total + case .function: + if let data = value as? Data { + return data.setLengthLeft(32) + } + } + return nil + } +} diff --git a/Sources/SSFChainlinkProvider/eip_mew/ABI/ABIParameterTypes.swift b/Sources/SSFChainlinkProvider/eip_mew/ABI/ABIParameterTypes.swift new file mode 100644 index 00000000..342520e0 --- /dev/null +++ b/Sources/SSFChainlinkProvider/eip_mew/ABI/ABIParameterTypes.swift @@ -0,0 +1,253 @@ +// +// Created by Alex Vlasov on 25/10/2018. +// Copyright © 2018 Alex Vlasov. All rights reserved. +// + +import BigInt +import Foundation +import Web3ContractABI + +public extension ABI.Element { + /// Specifies the type that parameters in a contract have. + enum ParameterType: ABIElementPropertiesProtocol { + case uint(bits: UInt64) + case int(bits: UInt64) + case address + case function + case bool + case bytes(length: UInt64) + indirect case array(type: ParameterType, length: UInt64) + case dynamicBytes + case string + indirect case tuple(types: [ParameterType]) + + var isNumber: Bool { + switch self { + case .int, .uint: + return true + default: + return false + } + } + + var isStatic: Bool { + switch self { + case .string: + return false + case .dynamicBytes: + return false + case let .array(type: type, length: length): + if length == 0 { + return false + } + if !type.isStatic { + return false + } + return true + case let .tuple(types: types): + for type in types where !type.isStatic { + return false + } + return true + case .bytes(length: _): + return true + default: + return true + } + } + + var isArray: Bool { + switch self { + case .array(type: _, length: _): + return true + default: + return false + } + } + + var isTuple: Bool { + switch self { + case .tuple: + return true + default: + return false + } + } + + var subtype: ABI.Element.ParameterType? { + switch self { + case .array(type: let type, length: _): + return type + default: + return nil + } + } + + var memoryUsage: UInt64 { + switch self { + case let .array(_, length: length): + if length == 0 { + return 32 + } + if isStatic { + return 32 * length + } + return 32 + case let .tuple(types: types): + if !isStatic { + return 32 + } + var sum: UInt64 = 0 + for type in types { + sum += type.memoryUsage + } + return sum + default: + return 32 + } + } + + var emptyValue: Any { + switch self { + case .uint(bits: _): + return BigUInt(0) + case .int(bits: _): + return BigUInt(0) + case .address: + return Address(address: "0x0000000000000000000000000000000000000000")! + case .function: + return Data(repeating: 0x00, count: 24) + case .bool: + return false + case let .bytes(length: length): + return Data(repeating: 0x00, count: Int(length)) + case let .array(type: type, length: length): + let emptyValueOfType = type.emptyValue + return Array(repeating: emptyValueOfType, count: Int(length)) + case .dynamicBytes: + return Data() + case .string: + return "" + case .tuple(types: _): + return [Any]() + } + } + + var arraySize: ABI.Element.ArraySize { + switch self { + case .array(type: _, length: let length): + if length == 0 { + return ArraySize.dynamicSize + } + return ArraySize.staticSize(length) + default: + return ArraySize.notArray + } + } + } +} + +extension ABI.Element.ParameterType: Equatable { + public static func == (lhs: ABI.Element.ParameterType, rhs: ABI.Element.ParameterType) -> Bool { + switch (lhs, rhs) { + case let (.uint(length1), .uint(length2)): + return length1 == length2 + case let (.int(length1), .int(length2)): + return length1 == length2 + case (.address, .address): + return true + case (.bool, .bool): + return true + case let (.bytes(length1), .bytes(length2)): + return length1 == length2 + case (.function, .function): + return true + case let (.array(type1, length1), .array(type2, length2)): + return type1 == type2 && length1 == length2 + case (.dynamicBytes, .dynamicBytes): + return true + case (.string, .string): + return true + default: + return false + } + } +} + +public extension ABI.Element.Function { + var signature: String { + "\(name ?? "")(\(inputs.map { $0.type.abiRepresentation }.joined(separator: ",")))" + } + + var methodString: String { + String(signature.sha3(.keccak256).prefix(8)) + } + + var methodEncoding: Data { + signature.data(using: .ascii)!.sha3(.keccak256)[0 ... 3] + } +} + +// MARK: - Event topic + +public extension ABI.Element.Event { + var signature: String { + "\(name)(\(inputs.map { $0.type.abiRepresentation }.joined(separator: ",")))" + } + + var topic: Data { + signature.data(using: .ascii)!.sha3(.keccak256) + } +} + +extension ABI.Element.ParameterType: ABIEncoding { + public var abiRepresentation: String { + switch self { + case let .uint(bits): + return "uint\(bits)" + case let .int(bits): + return "int\(bits)" + case .address: + return "address" + case .bool: + return "bool" + case let .bytes(length): + return "bytes\(length)" + case .dynamicBytes: + return "bytes" + case .function: + return "function" + case let .array(type: type, length: length): + if length == 0 { + return "\(type.abiRepresentation)[]" + } + return "\(type.abiRepresentation)[\(length)]" + case let .tuple(types: types): + let typesRepresentation = types.map { $0.abiRepresentation } + let typesJoined = typesRepresentation.joined(separator: ",") + return "tuple(\(typesJoined))" + case .string: + return "string" + } + } +} + +extension ABI.Element.ParameterType: ABIValidation { + public var isValid: Bool { + switch self { + case let .uint(bits), let .int(bits): + return bits > 0 && bits <= 256 && bits % 8 == 0 + case let .bytes(length): + return length > 0 && length <= 32 + case let .array(type: type, _): + return type.isValid + case let .tuple(types: types): + for type in types where !type.isValid { + return false + } + return true + default: + return true + } + } +} diff --git a/Sources/SSFChainlinkProvider/eip_mew/ABI/BigInt+ABI.swift b/Sources/SSFChainlinkProvider/eip_mew/ABI/BigInt+ABI.swift new file mode 100644 index 00000000..be37a3a8 --- /dev/null +++ b/Sources/SSFChainlinkProvider/eip_mew/ABI/BigInt+ABI.swift @@ -0,0 +1,58 @@ +// +// BigInt+ABI.swift +// MEWwalletKit +// +// Created by Nail Galiaskarov on 3/18/21. +// Copyright © 2021 MyEtherWallet Inc. All rights reserved. +// + +import BigInt +import Foundation +import SSFUtils + +extension BigInt { + func toTwosComplement() -> Data { + if sign == BigInt.Sign.plus { + return magnitude.serialize() + } else { + let serializedLength = magnitude.serialize().count + let MAX = BigUInt(1) << (serializedLength * 8) + let twoComplement = MAX - magnitude + return twoComplement.serialize() + } + } +} + +extension BigUInt { + func abiEncode(bits: UInt64) -> Data? { + let data = serialize() + let paddedLength = Int(ceil(Double(bits) / 8.0)) + let padded = data.setLengthLeft(paddedLength) + return padded + } +} + +extension BigInt { + func abiEncode(bits: UInt64) -> Data? { + let isNegative = self < BigInt(0) + let data = toTwosComplement() + let paddedLength = Int(ceil(Double(bits) / 8.0)) + let padded = data.setLengthLeft(paddedLength, isNegative: isNegative) + return padded + } +} + +extension BigInt { + static func fromTwosComplement(data: Data) -> BigInt { + let isPositive = ((data[0] & 128) >> 7) == 0 + if isPositive { + let magnitude = BigUInt(data) + return BigInt(magnitude) + } else { + let MAX = (BigUInt(1) << (data.count * 8)) + let magnitude = MAX - BigUInt(data) + let bigint = BigInt(0) - BigInt(magnitude) + return bigint + } + } +} diff --git a/Sources/SSFChainlinkProvider/eip_mew/Address.swift b/Sources/SSFChainlinkProvider/eip_mew/Address.swift new file mode 100644 index 00000000..2cf45847 --- /dev/null +++ b/Sources/SSFChainlinkProvider/eip_mew/Address.swift @@ -0,0 +1,64 @@ +// +// Address.swift +// MEWwalletKit +// +// Created by Mikhail Nikanorov on 4/25/19. +// Copyright © 2019 MyEtherWallet Inc. All rights reserved. +// + +import CryptoSwift +import Foundation + +public struct Address: CustomDebugStringConvertible { + public enum Ethereum { + static let length = 42 + } + + private var _address: String + public var address: String { + _address + } + + public var data: Data { + Data(hex: address) + } + + public init?(data: Data, prefix: String? = nil) { + self.init(address: data.toHexString(), prefix: prefix) + } + + public init(raw: String) { + _address = raw + } + + public init?(address: String, prefix: String? = nil) { + var address = address + if let prefix = prefix, !address.hasPrefix(prefix) { + address.insert(contentsOf: prefix, at: address.startIndex) + } + if address.stringAddHexPrefix().count == Address.Ethereum.length, prefix == nil, + address.isHex(), let eip55address = address.stringAddHexPrefix().eip55() + { + _address = eip55address + } else { + _address = address + } + } + + public init?(ethereumAddress: String) { + let value = ethereumAddress.stringAddHexPrefix() + guard value.count == Address.Ethereum.length, value.isHex(), + let address = value.eip55() else { return nil } // 42 = 0x + 20bytes + _address = address + } + + public var debugDescription: String { + _address + } +} + +extension Address: Equatable { + public static func == (lhs: Address, rhs: Address) -> Bool { + lhs._address.lowercased() == rhs._address.lowercased() + } +} diff --git a/Sources/SSFChainlinkProvider/eip_mew/Extensions/String/String+EIP55.swift b/Sources/SSFChainlinkProvider/eip_mew/Extensions/String/String+EIP55.swift new file mode 100644 index 00000000..03943ca8 --- /dev/null +++ b/Sources/SSFChainlinkProvider/eip_mew/Extensions/String/String+EIP55.swift @@ -0,0 +1,42 @@ +// +// String+EIP55.swift +// fearless +// +// Created by Soramitsu on 30.08.2023. +// Copyright © 2023 Soramitsu. All rights reserved. +// + +import Foundation + +extension String { + func eip55() -> String? { + guard isHex() else { + return nil + } + var address = self + let hasHexPrefix = address.hasHexPrefix() + if hasHexPrefix { + address.removeHexPrefix() + } + address = address.lowercased() + guard let hash = address.data(using: .ascii)?.sha3(.keccak256).toHexString() else { + return nil + } + + var eip55 = zip(address, hash).map { addr, hash -> String in + switch (addr, hash) { + case ("0", _), ("1", _), ("2", _), ("3", _), ("4", _), ("5", _), ("6", _), ("7", _), + ("8", _), ("9", _): + return String(addr) + case (_, "8"), (_, "9"), (_, "a"), (_, "b"), (_, "c"), (_, "d"), (_, "e"), (_, "f"): + return String(addr).uppercased() + default: + return String(addr).lowercased() + } + }.joined() + if hasHexPrefix { + eip55.addHexPrefix() + } + return eip55 + } +} diff --git a/Sources/SSFChainlinkProvider/eip_mew/Extensions/String/String+Hex.swift b/Sources/SSFChainlinkProvider/eip_mew/Extensions/String/String+Hex.swift new file mode 100644 index 00000000..3aa53b3d --- /dev/null +++ b/Sources/SSFChainlinkProvider/eip_mew/Extensions/String/String+Hex.swift @@ -0,0 +1,74 @@ +// +// String+Hex.swift +// MEWwalletKit +// +// Created by Mikhail Nikanorov on 4/25/19. +// Copyright © 2019 MyEtherWallet Inc. All rights reserved. +// + +import Foundation + +private let _nonHexCharacterSet = CharacterSet(charactersIn: "0123456789ABCDEF").inverted + +extension String { + func isHex() -> Bool { + var rawHex = self + rawHex.removeHexPrefix() + rawHex = rawHex.uppercased() + return rawHex.rangeOfCharacter(from: _nonHexCharacterSet) == nil + } + + func isHexWithPrefix() -> Bool { + guard hasHexPrefix() else { return false } + return isHex() + } + + mutating func removeHexPrefix() { + if hasPrefix("0x") { + let indexStart = index(startIndex, offsetBy: 2) + self = String(self[indexStart...]) + } + } + + mutating func addHexPrefix() { + if !hasPrefix("0x") { + self = "0x" + self + } + } + + mutating func alignHexBytes() { + guard isHex(), count % 2 != 0 else { + return + } + let hasPrefix = self.hasPrefix("0x") + if hasPrefix { + removeHexPrefix() + } + self = "0" + self + if hasPrefix { + addHexPrefix() + } + } + + func hasHexPrefix() -> Bool { + hasPrefix("0x") + } + + func stringRemoveHexPrefix() -> String { + var string = self + string.removeHexPrefix() + return string + } + + func stringAddHexPrefix() -> String { + var string = self + string.addHexPrefix() + return string + } + + func stringWithAlignedHexBytes() -> String { + var string = self + string.alignHexBytes() + return string + } +} diff --git a/Sources/SSFCoingeckoProvider/CoingeckoOperationFactory.swift b/Sources/SSFCoingeckoProvider/CoingeckoOperationFactory.swift new file mode 100644 index 00000000..b61b17ba --- /dev/null +++ b/Sources/SSFCoingeckoProvider/CoingeckoOperationFactory.swift @@ -0,0 +1,101 @@ +import Foundation +import RobinHood +import SSFModels + +enum CoingeckoAPI { + static let baseURL = URL(string: "https://api.coingecko.com/api/v3")! + static let price = "simple/price" +} + +public protocol CoingeckoOperationFactoryProtocol { + func fetchPriceOperation( + for tokenIds: [String], + currencies: [Currency] + ) -> BaseOperation<[PriceData]> +} + +public final class CoingeckoOperationFactory { + private func buildURLForAssets( + _ tokenIds: [String], + method: String, + currencies: [Currency] + ) -> URL? { + guard var components = URLComponents( + url: CoingeckoAPI.baseURL.appendingPathComponent(method), + resolvingAgainstBaseURL: false + ) else { return nil } + + let tokenIDParam = tokenIds.joined(separator: ",") + let currencyParam = currencies.map { $0.id }.joined(separator: ",") + + components.queryItems = [ + URLQueryItem(name: "ids", value: tokenIDParam), + URLQueryItem(name: "vs_currencies", value: currencyParam), + URLQueryItem(name: "include_24hr_change", value: "true"), + ] + + return components.url + } +} + +extension CoingeckoOperationFactory: CoingeckoOperationFactoryProtocol { + public func fetchPriceOperation( + for tokenIds: [String], + currencies: [Currency] + ) -> BaseOperation<[PriceData]> { + guard let url = buildURLForAssets( + tokenIds, + method: CoingeckoAPI.price, + currencies: currencies + ) else { + return BaseOperation.createWithError(NetworkBaseError.invalidUrl) + } + + let requestFactory = BlockNetworkRequestFactory { + var request = URLRequest(url: url) + + request.setValue( + HttpContentType.json.rawValue, + forHTTPHeaderField: HttpHeaderKey.contentType.rawValue + ) + + request.httpMethod = HttpMethod.get.rawValue + + return request + } + + let resultFactory = AnyNetworkResultFactory<[PriceData]> { data in + + let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] + return tokenIds.compactMap { assetId in + guard let priceDataJson = json?[assetId] as? [String: Any] else { + return nil + } + + return currencies.compactMap { currency in + let price = priceDataJson[currency.id] as? CGFloat + let dayChange = priceDataJson["\(currency.id)_24h_change"] as? CGFloat + + guard let price = price else { + return nil + } + + return PriceData( + currencyId: currency.id, + priceId: assetId, + price: String(describing: price), + fiatDayChange: Decimal(dayChange ?? 0.0), + coingeckoPriceId: assetId + ) + } + }.reduce([], +) + } + + let operation = NetworkOperation( + requestFactory: requestFactory, + resultFactory: resultFactory + ) + + return operation + } +} diff --git a/Sources/SSFCoingeckoProvider/CoingeckoService.swift b/Sources/SSFCoingeckoProvider/CoingeckoService.swift new file mode 100644 index 00000000..9265474a --- /dev/null +++ b/Sources/SSFCoingeckoProvider/CoingeckoService.swift @@ -0,0 +1,39 @@ +import RobinHood +import SSFModels +import SSFUtils +import SSFPrices + +public final class CoingeckoService: PriceProviderServiceProtocol { + let coingeckoOperationFactory: CoingeckoOperationFactoryProtocol + + public init(coingeckoOperationFactory: CoingeckoOperationFactoryProtocol) { + self.coingeckoOperationFactory = coingeckoOperationFactory + } + + public func getPrices(for chainAssets: [ChainAsset], currencies: [Currency]) async -> [PriceData] { + let operation = createCoingeckoOperation(for: chainAssets, currencies: currencies) + do { + return try operation.extractNoCancellableResultData() + } catch { + return [] + } + } + + private func createCoingeckoOperation( + for chainAssets: [ChainAsset], + currencies: [Currency] + ) -> BaseOperation<[PriceData]> { + let priceIds = chainAssets + .map { $0.asset.coingeckoPriceId } + .compactMap { $0 } + .uniq(predicate: { $0 }) + guard priceIds.isNotEmpty else { + return BaseOperation.createWithResult([]) + } + let operation = coingeckoOperationFactory.fetchPriceOperation( + for: priceIds, + currencies: currencies + ) + return operation + } +} diff --git a/Sources/SSFEtherscanIndexer/EtherscanHistoryRequest.swift b/Sources/SSFEtherscanIndexer/EtherscanHistoryRequest.swift index 21d85895..6a0239eb 100644 --- a/Sources/SSFEtherscanIndexer/EtherscanHistoryRequest.swift +++ b/Sources/SSFEtherscanIndexer/EtherscanHistoryRequest.swift @@ -9,15 +9,15 @@ final class EtherscanHistoryRequest: RequestConfig { address: String ) { let action: String = chainAsset.asset.ethereumType == .normal ? "txlist" : "tokentx" - var queryItems = [ + let queryItems = [ URLQueryItem(name: "module", value: "account"), URLQueryItem(name: "action", value: action), URLQueryItem(name: "address", value: address), ] - if let apiKey = chainAsset.chain.externalApi?.history?.apiKey { - queryItems.append(URLQueryItem(name: "apikey", value: apiKey)) - } +// if let apiKey = chainAsset.chain.externalApi?.history?.apiKey { +// queryItems.append(URLQueryItem(name: "apikey", value: apiKey)) +// } super.init( baseURL: baseURL, diff --git a/Sources/SSFHelpers/SSFHelpers/SSFHelpers/Classes/Tests/ChainModelGenerator.swift b/Sources/SSFHelpers/SSFHelpers/SSFHelpers/Classes/Tests/ChainModelGenerator.swift index 3304df52..cac5c52f 100644 --- a/Sources/SSFHelpers/SSFHelpers/SSFHelpers/Classes/Tests/ChainModelGenerator.swift +++ b/Sources/SSFHelpers/SSFHelpers/SSFHelpers/Classes/Tests/ChainModelGenerator.swift @@ -6,7 +6,7 @@ public enum ChainModelGenerator { name: String? = nil, chainId: String? = nil, parentId: String? = nil, - paraId: String? = nil, + paraId _: String? = nil, count: Int, withTypes: Bool = true, staking: StakingType? = nil, @@ -56,11 +56,9 @@ public enum ChainModelGenerator { ) let chain = ChainModel( - rank: nil, disabled: false, chainId: chainId, parentId: parentId, - paraId: paraId, name: name ?? String(chainId.reversed()), tokens: ChainRemoteTokens( type: .config, @@ -76,7 +74,8 @@ public enum ChainModelGenerator { externalApi: externalApi, customNodes: nil, iosMinAppVersion: nil, - properties: ChainProperties(addressPrefix: String(index)) + properties: ChainProperties(addressPrefix: String(index)), + identityChain: nil ) let asset = generateAssetWithId("", symbol: "", assetPresicion: 12, chainId: chainId) @@ -96,11 +95,9 @@ public enum ChainModelGenerator { availableAssets: [XcmAvailableAsset] ) -> ChainModel { ChainModel( - rank: nil, disabled: chain.disabled, chainId: chain.chainId, parentId: chain.parentId, - paraId: chain.paraId, name: chain.name, tokens: chain.tokens, xcm: XcmChain( @@ -114,10 +111,10 @@ public enum ChainModelGenerator { icon: chain.icon, options: chain.options, externalApi: chain.externalApi, - selectedNode: chain.selectedNode, customNodes: chain.customNodes, iosMinAppVersion: chain.iosMinAppVersion, - properties: chain.properties + properties: chain.properties, + identityChain: chain.identityChain ) } @@ -164,11 +161,9 @@ public enum ChainModelGenerator { ) let chain = ChainModel( - rank: nil, disabled: false, chainId: chainId, parentId: nil, - paraId: nil, name: UUID().uuidString, tokens: ChainRemoteTokens(type: .config, whitelist: nil, utilityId: nil, tokens: []), xcm: xcm, @@ -179,7 +174,8 @@ public enum ChainModelGenerator { externalApi: externalApi, customNodes: nil, iosMinAppVersion: nil, - properties: ChainProperties(addressPrefix: String(addressPrefix)) + properties: ChainProperties(addressPrefix: String(addressPrefix)), + identityChain: nil ) let chainAssetsArray: [AssetModel] = (0 ..< count).map { index in generateAssetWithId( @@ -210,23 +206,15 @@ public enum ChainModelGenerator { id: identifier, name: "", symbol: symbol, - isUtility: true, precision: assetPresicion, - icon: nil, - substrateType: substrateAssetType, - ethereumType: nil, - tokenProperties: - TokenProperties( + tokenProperties: TokenProperties( priceId: nil, currencyId: currencyId, color: nil, type: substrateAssetType, isNative: true ), - price: nil, - priceId: nil, - coingeckoPriceId: nil, - priceProvider: nil + isUtility: true ) } @@ -251,8 +239,7 @@ public enum ChainModelGenerator { if staking != nil { stakingApi = ChainModel.BlockExplorer( type: "test", - url: URL(string: "https://staking.io/\(chainId)-\(UUID().uuidString).json")!, - apiKey: nil + url: URL(string: "https://staking.io/\(chainId)-\(UUID().uuidString).json")! ) } else { stakingApi = nil diff --git a/Sources/SSFModels/SSFModels/AssetModel.swift b/Sources/SSFModels/SSFModels/AssetModel.swift index aa0be09a..0f24d164 100644 --- a/Sources/SSFModels/SSFModels/AssetModel.swift +++ b/Sources/SSFModels/SSFModels/AssetModel.swift @@ -6,7 +6,7 @@ public struct TokenProperties: Codable, Hashable { public let color: String? public let type: SubstrateAssetType? public let isNative: Bool? - public let stacking: String? + public let staking: RawStakingType? public init( priceId: String? = nil, @@ -14,67 +14,102 @@ public struct TokenProperties: Codable, Hashable { color: String? = nil, type: SubstrateAssetType? = nil, isNative: Bool = false, - stacking: String? = "" + staking: RawStakingType? = nil ) { self.priceId = priceId self.currencyId = currencyId self.color = color self.type = type self.isNative = isNative - self.stacking = stacking + self.staking = staking } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + priceId = try? container.decode(String.self, forKey: .priceId) + currencyId = nil + color = try? container.decode(String.self, forKey: .color) + type = try? container.decode(SubstrateAssetType.self, forKey: .type) + isNative = try? container.decode(Bool.self, forKey: .isNative) + staking = try? container.decode(RawStakingType.self, forKey: .staking) + } + + public func encode(to _: Encoder) throws {} } -public struct AssetModel: Equatable, Codable, Hashable { +extension TokenProperties { + private enum CodingKeys: String, CodingKey { + case priceId + case currencyId + case color + case type + case isNative + case staking + } +} + +public struct AssetModel: Equatable, Codable, Hashable, Identifiable { public typealias Id = String public typealias PriceId = String + public var identifier: String { id } + public let id: String public let name: String public let symbol: String - public let isUtility: Bool public let precision: UInt16 public let icon: URL? - public let substrateType: SubstrateAssetType? + public let existentialDeposit: String? + public let isUtility: Bool + public let purchaseProviders: [PurchaseProvider]? public let ethereumType: EthereumAssetType? public let tokenProperties: TokenProperties? - public let price: Decimal? - public let priceId: String? - public let coingeckoPriceId: String? public let priceProvider: PriceProvider? + public let coingeckoPriceId: PriceId? + public var priceId: PriceId? { + if let priceProvider = priceProvider { + return priceProvider.id + } + + return coingeckoPriceId + } + public var symbolUppercased: String { symbol.uppercased() } + public var priceData: [PriceData] + public init( id: String, name: String, symbol: String, - isUtility: Bool, precision: UInt16, icon: URL? = nil, - substrateType: SubstrateAssetType?, - ethereumType: EthereumAssetType?, tokenProperties: TokenProperties?, - price: Decimal?, - priceId: String?, - coingeckoPriceId: String?, - priceProvider: PriceProvider? + existentialDeposit: String? = nil, + isUtility: Bool, + purchaseProviders: [PurchaseProvider]? = nil, + ethereumType: EthereumAssetType? = nil, + priceProvider: PriceProvider? = nil, + coingeckoPriceId: PriceId? = nil, + priceData: [PriceData] = [] ) { self.id = id self.symbol = symbol self.name = name - self.isUtility = isUtility self.precision = precision self.icon = icon - self.substrateType = substrateType + self.existentialDeposit = existentialDeposit + self.isUtility = isUtility + self.purchaseProviders = purchaseProviders self.ethereumType = ethereumType self.tokenProperties = tokenProperties - self.price = price - self.priceId = priceId - self.coingeckoPriceId = coingeckoPriceId self.priceProvider = priceProvider + self.coingeckoPriceId = coingeckoPriceId + self.priceData = priceData } public init(from decoder: Decoder) throws { @@ -83,51 +118,58 @@ public struct AssetModel: Equatable, Codable, Hashable { id = try container.decode(String.self, forKey: .id) name = try container.decode(String.self, forKey: .name) symbol = try container.decode(String.self, forKey: .symbol) - isUtility = try container.decode(Bool.self, forKey: .isUtility) precision = try container.decode(UInt16.self, forKey: .precision) icon = try? container.decode(URL?.self, forKey: .icon) - substrateType = try? container.decode(SubstrateAssetType?.self, forKey: .substrateType) + existentialDeposit = try? container.decode(String?.self, forKey: .existentialDeposit) + isUtility = (try? container.decode(Bool?.self, forKey: .isUtility)) ?? false + purchaseProviders = try? container.decode( + [PurchaseProvider]?.self, + forKey: .purchaseProviders + ) ethereumType = try? container.decode(EthereumAssetType?.self, forKey: .ethereumType) tokenProperties = try? container.decode(TokenProperties?.self, forKey: .tokenProperties) - price = nil - priceId = nil - coingeckoPriceId = nil - priceProvider = nil + + coingeckoPriceId = try? container.decode(String?.self, forKey: .priceId) + priceProvider = try container.decodeIfPresent(PriceProvider.self, forKey: .priceProvider) + + priceData = [] } public func encode(to _: Encoder) throws {} - public func replacingPrice(_: PriceData) -> AssetModel { + public func replacingPrice(_ priceData: [PriceData]) -> AssetModel { AssetModel( id: id, name: name, symbol: symbol, - isUtility: isUtility, precision: precision, icon: icon, - substrateType: substrateType, + tokenProperties: tokenProperties, existentialDeposit: existentialDeposit, + isUtility: isUtility, + purchaseProviders: purchaseProviders, ethereumType: ethereumType, - tokenProperties: tokenProperties, - price: price, - priceId: priceId, + priceProvider: priceProvider, coingeckoPriceId: coingeckoPriceId, - priceProvider: priceProvider + priceData: priceData ) } + public func getPrice(for currency: Currency) -> PriceData? { + priceData.first { $0.currencyId == currency.id } + } + public static func == (lhs: AssetModel, rhs: AssetModel) -> Bool { lhs.id == rhs.id && lhs.name == rhs.name && - lhs.isUtility == rhs.isUtility && lhs.precision == rhs.precision && lhs.icon == rhs.icon && lhs.symbol == rhs.symbol && lhs.tokenProperties == rhs.tokenProperties && - lhs.substrateType == rhs.substrateType && + lhs.existentialDeposit == rhs.existentialDeposit && + lhs.isUtility == rhs.isUtility && + lhs.purchaseProviders == rhs.purchaseProviders && lhs.ethereumType == rhs.ethereumType && - lhs.tokenProperties == rhs.tokenProperties && - lhs.priceId == rhs.priceId && - lhs.coingeckoPriceId == rhs.coingeckoPriceId + lhs.priceProvider == rhs.priceProvider } public func hash(into hasher: inout Hasher) { @@ -140,12 +182,14 @@ extension AssetModel { case id case name case symbol - case isUtility - case precision case icon case tokenProperties - case substrateType + case priceId + case existentialDeposit + case isUtility + case purchaseProviders case ethereumType - case currencyId + case priceProvider + case precision } } diff --git a/Sources/SSFModels/SSFModels/ChainAsset.swift b/Sources/SSFModels/SSFModels/ChainAsset.swift index f4e0262f..547f0bbd 100644 --- a/Sources/SSFModels/SSFModels/ChainAsset.swift +++ b/Sources/SSFModels/SSFModels/ChainAsset.swift @@ -151,7 +151,7 @@ public extension ChainAsset { var hasStaking: Bool { let model: AssetModel? = chain.tokens.tokens?.first { $0.id == asset.id } - return model?.tokenProperties?.stacking != nil + return model?.tokenProperties?.staking != nil } var storagePath: StorageCodingPath { diff --git a/Sources/SSFModels/SSFModels/ChainModel.swift b/Sources/SSFModels/SSFModels/ChainModel.swift index b0147fd9..c6c16f4e 100644 --- a/Sources/SSFModels/SSFModels/ChainModel.swift +++ b/Sources/SSFModels/SSFModels/ChainModel.swift @@ -51,7 +51,7 @@ public struct ChainRemoteTokens: Codable, Hashable { } } -public struct ChainProperties: Codable { +public struct ChainProperties: Codable, Equatable { public let addressPrefix: String public let rank: String? public let paraId: String? @@ -76,35 +76,28 @@ public struct ChainProperties: Codable { public final class ChainModel: Codable, Identifiable { public typealias Id = String - public let disabled: Bool - public let chainId: Id - public let externalApi: ExternalApiSet? public var tokens: ChainRemoteTokens - public var identifier: String { chainId } - public let rank: UInt16? - + public let disabled: Bool + public let chainId: Id public let parentId: Id? - public let paraId: String? public let name: String - public let xcm: XcmChain? public let nodes: Set public let types: TypesSettings? public let icon: URL? public let options: [ChainOptions]? - + public let externalApi: ExternalApiSet? public var selectedNode: ChainNodeModel? public let customNodes: Set? public let iosMinAppVersion: String? public let properties: ChainProperties + public let identityChain: String? public init( - rank: UInt16?, disabled: Bool, chainId: Id, parentId: Id? = nil, - paraId: String?, name: String, tokens: ChainRemoteTokens, xcm: XcmChain?, @@ -116,13 +109,12 @@ public final class ChainModel: Codable, Identifiable { selectedNode: ChainNodeModel? = nil, customNodes: Set? = nil, iosMinAppVersion: String?, - properties: ChainProperties + properties: ChainProperties, + identityChain: String? ) { - self.rank = rank self.disabled = disabled self.chainId = chainId self.parentId = parentId - self.paraId = paraId self.name = name self.tokens = tokens self.xcm = xcm @@ -135,6 +127,7 @@ public final class ChainModel: Codable, Identifiable { self.customNodes = customNodes self.iosMinAppVersion = iosMinAppVersion self.properties = properties + self.identityChain = identityChain } public var isRelaychain: Bool { @@ -271,7 +264,9 @@ public final class ChainModel: Codable, Identifiable { } public func utilityChainAssets() -> [ChainAsset] { - [] + tokens.tokens?.filter { $0.isUtility }.map { + ChainAsset(chain: self, asset: $0) + } ?? [] } public func seedTag(metaId: MetaAccountId, accountId: AccountId? = nil) -> String { @@ -294,11 +289,9 @@ public final class ChainModel: Codable, Identifiable { public func replacingSelectedNode(_ node: ChainNodeModel?) -> ChainModel { ChainModel( - rank: rank, disabled: disabled, chainId: chainId, parentId: parentId, - paraId: paraId, name: name, tokens: tokens, xcm: xcm, @@ -310,17 +303,16 @@ public final class ChainModel: Codable, Identifiable { selectedNode: node, customNodes: customNodes, iosMinAppVersion: iosMinAppVersion, - properties: properties + properties: properties, + identityChain: identityChain ) } public func replacingCustomNodes(_ newCustomNodes: [ChainNodeModel]) -> ChainModel { ChainModel( - rank: rank, disabled: disabled, chainId: chainId, parentId: parentId, - paraId: paraId, name: name, tokens: tokens, xcm: xcm, @@ -332,7 +324,29 @@ public final class ChainModel: Codable, Identifiable { selectedNode: selectedNode, customNodes: Set(newCustomNodes), iosMinAppVersion: iosMinAppVersion, - properties: properties + properties: properties, + identityChain: identityChain + ) + } + + public func replacing(_ tokens: ChainRemoteTokens) -> ChainModel { + ChainModel( + disabled: disabled, + chainId: chainId, + parentId: parentId, + name: name, + tokens: tokens, + xcm: xcm, + nodes: nodes, + types: types, + icon: icon, + options: options, + externalApi: externalApi, + selectedNode: selectedNode, + customNodes: customNodes, + iosMinAppVersion: iosMinAppVersion, + properties: properties, + identityChain: identityChain ) } } @@ -350,19 +364,22 @@ public extension ChainModel { extension ChainModel: Hashable { public static func == (lhs: ChainModel, rhs: ChainModel) -> Bool { - lhs.rank == rhs.rank + lhs.disabled == rhs.disabled && lhs.chainId == rhs.chainId - && lhs.externalApi == rhs.externalApi + && lhs.parentId == rhs.parentId + && lhs.name == rhs.name && lhs.tokens == rhs.tokens - && lhs.options == rhs.options + && lhs.xcm == rhs.xcm + && lhs.nodes == rhs.nodes && lhs.types == rhs.types && lhs.icon == rhs.icon - && lhs.name == rhs.name - && lhs.nodes == rhs.nodes - && lhs.iosMinAppVersion == rhs.iosMinAppVersion + && lhs.options == rhs.options + && lhs.externalApi == rhs.externalApi && lhs.selectedNode == rhs.selectedNode - && lhs.xcm == rhs.xcm - && lhs.disabled == rhs.disabled + && lhs.customNodes == rhs.customNodes + && lhs.iosMinAppVersion == rhs.iosMinAppVersion + && lhs.properties == rhs.properties + && lhs.identityChain == rhs.identityChain } public func hash(into hasher: inout Hasher) { @@ -382,6 +399,7 @@ public enum ChainOptions: String, Codable { case nft case utilityFeePayment case chainlinkProvider + case checkAppId case unsupported @@ -418,22 +436,27 @@ public extension ChainModel { } struct BlockExplorer: Codable, Hashable { - public let type: BlockExplorerType + enum CodingKeys: String, CodingKey { + case type + case url + } + + public let type: BlockExplorerType? public let url: URL - public let apiKey: String? - public init?( - type: String, - url: URL, - apiKey: String? - ) { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + type = try? container.decode(BlockExplorerType.self, forKey: .type) + url = try container.decode(URL.self, forKey: .url) + } + + public init?(type: String, url: URL) { guard let externalApiType = BlockExplorerType(rawValue: type) else { return nil } self.type = externalApiType self.url = url - self.apiKey = apiKey } } @@ -458,6 +481,7 @@ public extension ChainModel { case polkascan case etherscan case reef + case oklink case unknown public init(from decoder: Decoder) throws { @@ -488,24 +512,28 @@ public extension ChainModel { public let history: BlockExplorer? public let crowdloans: ExternalResource? public let explorers: [ExternalApiExplorer]? + public let pricing: BlockExplorer? public init( staking: ChainModel.BlockExplorer? = nil, history: ChainModel.BlockExplorer? = nil, crowdloans: ChainModel.ExternalResource? = nil, - explorers: [ChainModel.ExternalApiExplorer]? = nil + explorers: [ChainModel.ExternalApiExplorer]? = nil, + pricing: ChainModel.BlockExplorer? = nil ) { self.staking = staking self.history = history self.crowdloans = crowdloans self.explorers = explorers + self.pricing = pricing } public static func == (lhs: ExternalApiSet, rhs: ExternalApiSet) -> Bool { lhs.staking == rhs.staking && lhs.history == rhs.history && lhs.crowdloans == rhs.crowdloans && - Set(lhs.explorers ?? []) == Set(rhs.explorers ?? []) + Set(lhs.explorers ?? []) == Set(rhs.explorers ?? []) && + lhs.pricing == rhs.pricing } } @@ -575,7 +603,7 @@ public extension ChainModel.ExternalApiExplorer { var transactionType: ChainModel.SubscanType { switch type { - case .etherscan: + case .etherscan, .oklink: return .tx default: return .extrinsic diff --git a/Sources/SSFModels/SSFModels/RemoteChain.swift b/Sources/SSFModels/SSFModels/RemoteChain.swift index f80e8fcf..7b64b8e8 100644 --- a/Sources/SSFModels/SSFModels/RemoteChain.swift +++ b/Sources/SSFModels/SSFModels/RemoteChain.swift @@ -45,9 +45,11 @@ public struct XcmAvailableDestination: Codable, Hashable { public struct XcmAvailableAsset: Codable, Hashable { public let id: String public let symbol: String + public let minAmount: String? - public init(id: String, symbol: String) { + public init(id: String, symbol: String, minAmount: String?) { self.id = id self.symbol = symbol + self.minAmount = minAmount } } diff --git a/Sources/SSFNetwork/Operations/ManualOperation.swift b/Sources/SSFNetwork/Operations/ManualOperation.swift new file mode 100644 index 00000000..00bacd0c --- /dev/null +++ b/Sources/SSFNetwork/Operations/ManualOperation.swift @@ -0,0 +1,72 @@ +import Foundation +import RobinHood + +public final class ManualOperation: BaseOperation { + private let lockQueue = DispatchQueue( + label: "jp.co.soramitsu.asyncoperation", + attributes: .concurrent + ) + + override public var isAsynchronous: Bool { + true + } + + private var _isExecuting: Bool = false + override public private(set) var isExecuting: Bool { + get { + lockQueue.sync { () -> Bool in + _isExecuting + } + } + set { + willChangeValue(forKey: "isExecuting") + lockQueue.sync(flags: [.barrier]) { + _isExecuting = newValue + } + didChangeValue(forKey: "isExecuting") + } + } + + private var _isFinished: Bool = false + override public private(set) var isFinished: Bool { + get { + lockQueue.sync { () -> Bool in + _isFinished + } + } + set { + willChangeValue(forKey: "isFinished") + lockQueue.sync(flags: [.barrier]) { + _isFinished = newValue + } + didChangeValue(forKey: "isFinished") + } + } + + override public func start() { + isFinished = false + isExecuting = true + main() + } + + override public func main() { + super.main() + + if isCancelled { + finish() + return + } + + if result != nil { + finish() + return + } + } + + public func finish() { + if isExecuting { + isExecuting = false + isFinished = true + } + } +} diff --git a/Sources/SSFOklinkIndexer/OklinkHistoryRequest.swift b/Sources/SSFOklinkIndexer/OklinkHistoryRequest.swift index 0b4d3677..54510c93 100644 --- a/Sources/SSFOklinkIndexer/OklinkHistoryRequest.swift +++ b/Sources/SSFOklinkIndexer/OklinkHistoryRequest.swift @@ -14,9 +14,9 @@ final class OklinkHistoryRequest: RequestConfig { ] var headers: [HTTPHeader]? - if let apiKey = chainAsset.chain.externalApi?.history?.apiKey { - headers?.append(HTTPHeader(field: "Ok-Access-Key", value: apiKey)) - } +// if let apiKey = chainAsset.chain.externalApi?.history?.apiKey { +// headers?.append(HTTPHeader(field: "Ok-Access-Key", value: apiKey)) +// } super.init( baseURL: baseUrl, diff --git a/Sources/SSFPrices/PriceProviderServiceModel.swift b/Sources/SSFPrices/PriceProviderServiceModel.swift new file mode 100644 index 00000000..63b8a3e0 --- /dev/null +++ b/Sources/SSFPrices/PriceProviderServiceModel.swift @@ -0,0 +1,6 @@ +import SSFModels + +public struct PriceProviderServiceModel { + let type: PriceProviderType + let service: PriceProviderServiceProtocol +} diff --git a/Sources/SSFPrices/PriceProviderServiceProtocol.swift b/Sources/SSFPrices/PriceProviderServiceProtocol.swift new file mode 100644 index 00000000..4dcdc7b7 --- /dev/null +++ b/Sources/SSFPrices/PriceProviderServiceProtocol.swift @@ -0,0 +1,6 @@ +import SSFModels + +// sourcery: AutoMockable +public protocol PriceProviderServiceProtocol { + func getPrices(for chainAssets: [ChainAsset], currencies: [Currency]) async -> [PriceData] +} diff --git a/Sources/SSFPrices/PricesService.swift b/Sources/SSFPrices/PricesService.swift new file mode 100644 index 00000000..ebd01215 --- /dev/null +++ b/Sources/SSFPrices/PricesService.swift @@ -0,0 +1,193 @@ +import SSFModels +import RobinHood +import Foundation +import SSFAssetManagment + +// sourcery: AutoMockable +public protocol PricesServiceProtocol { + func getPriceDataFromCache(for chainAssets: [ChainAsset], currencies: [Currency]) async -> [ChainAsset: [PriceData]] + func getPriceDataFromAPI(for chainAssets: [ChainAsset], currencies: [Currency]) async -> [ChainAsset: [PriceData]] +} + +public final class PricesService { + private let chainRepository: AnyDataProviderRepository + private let chainAssetFetcher: ChainAssetFetchingServiceProtocol + private let operationQueue: OperationQueue + private let priceProviders: [PriceProviderServiceModel] + private let chainlinkService: PriceProviderServiceProtocol? + private let coingeckoService: PriceProviderServiceProtocol? + private let subqueryService: PriceProviderServiceProtocol? + private var chainAssets: [ChainAsset] = [] + private var currencies: [Currency] = [] + + public init( + chainRepository: AnyDataProviderRepository, + chainAssetFetcher: ChainAssetFetchingServiceProtocol, + operationQueue: OperationQueue, + priceProviders: [PriceProviderServiceModel], + chainlinkService: PriceProviderServiceProtocol?, + coingeckoService: PriceProviderServiceProtocol?, + subqueryService: PriceProviderServiceProtocol? + ) { + self.chainRepository = chainRepository + self.chainAssetFetcher = chainAssetFetcher + self.operationQueue = operationQueue + self.priceProviders = priceProviders + self.chainlinkService = chainlinkService + self.coingeckoService = coingeckoService + self.subqueryService = subqueryService + } + + public func getPriceDataFromCache( + for chainAssets: [ChainAsset], + currencies: [Currency] + ) async -> [ChainAsset: [PriceData]] { + let allChainAssets = await chainAssetFetcher.fetch(filters: [], sorts: [], forceUpdate: false) + let requestedChainAssets = allChainAssets.filter { dbChainAsset in + chainAssets.contains { chainAsset in + return chainAsset.chain.chainId == dbChainAsset.chain.chainId && chainAsset.asset.id == dbChainAsset.asset.id + } + } + var chainAssetsPriceData: [ChainAsset: [PriceData]] = [:] + requestedChainAssets.forEach { chainAsset in + chainAssetsPriceData[chainAsset] = chainAsset.asset.priceData.filter({ priceData in + currencies.map { $0.id }.contains(priceData.currencyId) + }) + } + return chainAssetsPriceData + } + + public func getPriceDataFromAPI( + for chainAssets: [ChainAsset], + currencies: [Currency] + ) async -> [ChainAsset: [PriceData]] { + async let chainlinkPrices = await self.priceProviders.first { $0.type == .chainlink }?.service + .getPrices(for: chainAssets, currencies: currencies) ?? [] + async let coingeckoPrices = await self.priceProviders.first { $0.type == .coingecko }?.service + .getPrices(for: chainAssets, currencies: currencies) ?? [] + async let subqueryPrices = await self.priceProviders.first { $0.type == .coingecko }?.service + .getPrices(for: chainAssets, currencies: currencies) ?? [] + let mergedPrices = await merge(chainlinkPrices: chainlinkPrices, coingeckoPrices: coingeckoPrices, soraSubqueryPrices: subqueryPrices) + handle(prices: mergedPrices, for: chainAssets) + var chainAssetsPriceData: [ChainAsset: [PriceData]] = [:] + chainAssets.forEach { chainAsset in + chainAssetsPriceData[chainAsset] = mergedPrices.filter { $0.priceId == chainAsset.asset.priceId } + } + return chainAssetsPriceData + } +} + +// MARK: - Private methods + +private extension PricesService { + func merge(chainlinkPrices: [PriceData], coingeckoPrices: [PriceData], soraSubqueryPrices: [PriceData]) -> [PriceData] { + var prices: [PriceData] = [] + prices = self.merge(coingeckoPrices: coingeckoPrices, chainlinkPrices: chainlinkPrices) + prices = self.merge(coingeckoPrices: prices, soraSubqueryPrices: soraSubqueryPrices) + return prices + } + + func merge(coingeckoPrices: [PriceData], chainlinkPrices: [PriceData]) -> [PriceData] { + if chainlinkPrices.isEmpty { + let prices = makePrices(from: coingeckoPrices, for: .chainlink) + return coingeckoPrices + prices + } + let caPriceIds = Set(chainAssets.compactMap { $0.asset.coingeckoPriceId }) + let sqPriceIds = Set(chainlinkPrices.compactMap { $0.coingeckoPriceId }) + + let replacedFiatDayChange: [PriceData] = chainlinkPrices.compactMap { chainlinkPrice in + let coingeckoPrice = coingeckoPrices + .first(where: { $0.coingeckoPriceId == chainlinkPrice.coingeckoPriceId }) + return chainlinkPrice.replaceFiatDayChange(fiatDayChange: coingeckoPrice?.fiatDayChange) + } + + let filtered = coingeckoPrices.filter { coingeckoPrice in + guard let coingeckoPriceId = coingeckoPrice.coingeckoPriceId else { + return true + } + return !caPriceIds.intersection(sqPriceIds).contains(coingeckoPriceId) + } + + return filtered + replacedFiatDayChange + } + + func merge( + coingeckoPrices: [PriceData], + soraSubqueryPrices: [PriceData] + ) -> [PriceData] { + if soraSubqueryPrices.isEmpty { + let prices = makePrices(from: coingeckoPrices, for: .sorasubquery) + return coingeckoPrices + prices + } + let caPriceIds = Set(chainAssets.compactMap { $0.asset.priceId }) + let sqPriceIds = Set(soraSubqueryPrices.compactMap { $0.priceId }) + + let filtered = coingeckoPrices.filter { coingeckoPrice in + let chainAsset = chainAssets + .first { $0.asset.coingeckoPriceId == coingeckoPrice.priceId } + guard let priceId = chainAsset?.asset.priceId else { + return true + } + return !caPriceIds.intersection(sqPriceIds).contains(priceId) + } + + return filtered + soraSubqueryPrices + } + + func makePrices( + from coingeckoPrices: [PriceData], + for type: PriceProviderType + ) -> [PriceData] { + let typePriceChainAssets = chainAssets + .filter { $0.asset.priceProvider?.type == type } + + let prices = coingeckoPrices.filter { coingeckoPrice in + typePriceChainAssets.contains { chainAsset in + chainAsset.asset.coingeckoPriceId == coingeckoPrice.coingeckoPriceId + } + } + + let newPrices: [PriceData] = prices.compactMap { price in + guard let chainAsset = typePriceChainAssets + .first(where: { $0.asset.coingeckoPriceId == price.coingeckoPriceId }) else + { + return nil + } + return PriceData( + currencyId: price.currencyId, + priceId: chainAsset.asset.priceId ?? price.priceId, + price: price.price, + fiatDayChange: price.fiatDayChange, + coingeckoPriceId: price.coingeckoPriceId + ) + } + return newPrices + } + + func handle(prices: [PriceData], for chainAssets: [ChainAsset]) { + var updatedChains: [ChainModel] = [] + let uniqChains: [ChainModel] = chainAssets.compactMap { $0.chain }.uniq { $0.chainId } + for chain in uniqChains { + var updatedAssets: [AssetModel] = [] + for chainAsset in chain.chainAssets { + let assetPrices = prices.filter { $0.priceId == chainAsset.asset.priceId } + let updatedAsset = chainAsset.asset.replacingPrice(assetPrices) + updatedAssets.append(updatedAsset) + } + let chainRemoteTokens = ChainRemoteTokens( + type: chain.tokens.type, + whitelist: chain.tokens.whitelist, + utilityId: chain.tokens.utilityId, + tokens: Set(updatedAssets) + ) + let updatedChain = chain.replacing(chainRemoteTokens) + updatedChains.append(updatedChain) + } + let saveOperation = chainRepository.saveOperation({ + updatedChains + }, { + [] + }) + operationQueue.addOperation(saveOperation) + } +} diff --git a/Sources/SSFSoraSubqueryProvider/PriceFetcher/SoraSubqueryPrice.swift b/Sources/SSFSoraSubqueryProvider/PriceFetcher/SoraSubqueryPrice.swift new file mode 100644 index 00000000..a9952240 --- /dev/null +++ b/Sources/SSFSoraSubqueryProvider/PriceFetcher/SoraSubqueryPrice.swift @@ -0,0 +1,23 @@ +import Foundation +import SSFUtils + +struct SoraSubqueryPriceResponse: Decodable { + let entities: SoraSubqueryPricePage +} + +struct SoraSubqueryPricePage: Decodable { + let nodes: [SoraSubqueryPrice] + let pageInfo: SubqueryPageInfo +} + +struct SoraSubqueryPrice: Decodable { + enum CodingKeys: String, CodingKey { + case id + case priceUsd = "priceUSD" + case priceChangeDay + } + + let id: String + let priceUsd: String? + let priceChangeDay: Decimal? +} diff --git a/Sources/SSFSoraSubqueryProvider/PriceFetcher/SoraSubqueryPriceFetcher.swift b/Sources/SSFSoraSubqueryProvider/PriceFetcher/SoraSubqueryPriceFetcher.swift new file mode 100644 index 00000000..1fca53cc --- /dev/null +++ b/Sources/SSFSoraSubqueryProvider/PriceFetcher/SoraSubqueryPriceFetcher.swift @@ -0,0 +1,114 @@ +import Foundation +import RobinHood +import SSFIndexers +import SSFModels +import SSFNetwork +import SSFUtils + +enum SubqueryPriceFetcherError: Error { + case missingBlockExplorer +} + +public protocol SoraSubqueryPriceFetcherProtocol { + func fetchPriceOperation( + for chainAssets: [ChainAsset] + ) -> BaseOperation<[PriceData]> +} + +public final class SoraSubqueryPriceFetcher: SoraSubqueryPriceFetcherProtocol { + public func fetchPriceOperation( + for chainAssets: [ChainAsset] + ) -> BaseOperation<[PriceData]> { + AwaitOperation { [weak self] in + guard let self else { return [] } + + guard let blockExplorer = chainAssets.first(where: { chainAsset in + chainAsset.chain.knownChainEquivalent == .soraMain + })?.chain.externalApi?.pricing else { + throw SubqueryPriceFetcherError.missingBlockExplorer + } + let priceIds = chainAssets.map { $0.asset.priceProvider?.id }.compactMap { $0 } + let prices = try await self.fetch(priceIds: priceIds, url: blockExplorer.url) + + return prices.compactMap { price in + let chainAsset = chainAssets + .first(where: { $0.asset.tokenProperties?.currencyId == price.id }) + + guard let chainAsset = chainAsset, + chainAsset.asset.priceProvider?.type == .sorasubquery, + let priceId = chainAsset.asset.priceId else + { + return nil + } + + return PriceData( + currencyId: "usd", + priceId: priceId, + price: "\(price.priceUsd.or("0"))", + fiatDayChange: price.priceChangeDay, + coingeckoPriceId: chainAsset.asset.coingeckoPriceId + ) + } + } + } + + private func fetch( + priceIds: [String], + url: URL + ) async throws -> [SoraSubqueryPrice] { + var prices: [SoraSubqueryPrice] = [] + var cursor = "" + var allPricesFetched = false + + while !allPricesFetched { + let response = try await loadNewPrices(url: url, priceIds: priceIds, cursor: cursor) + prices = prices + response.nodes + allPricesFetched = response.pageInfo.hasNextPage.or(false) == false + cursor = response.pageInfo.endCursor.or("") + } + + return prices + } + + private func loadNewPrices( + url: URL, + priceIds: [String], + cursor: String + ) async throws -> SoraSubqueryPricePage { + let request = try SoraSubqueryPricesRequest( + baseURL: url, + query: queryString(priceIds: priceIds, cursor: cursor) + ) + let worker = NetworkWorkerDefault() + let response: GraphQLResponse = try await worker + .performRequest(with: request) + + switch response { + case let .data(data): + return data.entities + case let .errors(error): + throw error + } + } + + private func queryString(priceIds: [String], cursor: String) -> String { + """ + query FiatPriceQuery { + entities: assets( + first: 100 + after: "\(cursor)", + filter: {id: {in: \(priceIds)}}) { + nodes { + id + priceUSD + priceChangeDay + } + pageInfo { + hasNextPage + endCursor + } + } + } + """ + } +} diff --git a/Sources/SSFSoraSubqueryProvider/PriceFetcher/SoraSubqueryPricesRequest.swift b/Sources/SSFSoraSubqueryProvider/PriceFetcher/SoraSubqueryPricesRequest.swift new file mode 100644 index 00000000..c3251467 --- /dev/null +++ b/Sources/SSFSoraSubqueryProvider/PriceFetcher/SoraSubqueryPricesRequest.swift @@ -0,0 +1,30 @@ +import Foundation +import RobinHood +import SSFModels +import SSFNetwork +import SSFUtils + +class SoraSubqueryPricesRequest: RequestConfig { + init( + baseURL: URL, + query: String + ) throws { + let defaultHeaders = [ + HTTPHeader( + field: HttpHeaderKey.contentType.rawValue, + value: HttpContentType.json.rawValue + ), + ] + + let info = JSON.dictionaryValue(["query": JSON.stringValue(query)]) + let data = try JSONEncoder().encode(info) + + super.init( + baseURL: baseURL, + method: .post, + endpoint: nil, + headers: defaultHeaders, + body: data + ) + } +} diff --git a/Sources/SSFSoraSubqueryProvider/PriceFetcher/SubqueryPageInfo.swift b/Sources/SSFSoraSubqueryProvider/PriceFetcher/SubqueryPageInfo.swift new file mode 100644 index 00000000..310c297e --- /dev/null +++ b/Sources/SSFSoraSubqueryProvider/PriceFetcher/SubqueryPageInfo.swift @@ -0,0 +1,21 @@ +struct SubqueryPageInfo: Decodable { + let startCursor: String? + let endCursor: String? + let hasNextPage: Bool? + + func toContext() -> [String: String]? { + if startCursor == nil, endCursor == nil { + return nil + } + var context: [String: String] = [:] + if let startCursor = startCursor { + context["startCursor"] = startCursor + } + + if let endCursor = endCursor { + context["endCursor"] = endCursor + } + + return context + } +} diff --git a/Sources/SSFSoraSubqueryProvider/SoraSubqueryService.swift b/Sources/SSFSoraSubqueryProvider/SoraSubqueryService.swift new file mode 100644 index 00000000..d0119e03 --- /dev/null +++ b/Sources/SSFSoraSubqueryProvider/SoraSubqueryService.swift @@ -0,0 +1,37 @@ +import RobinHood +import SSFModels +import SSFPrices + +public final class SoraSubqueryService: PriceProviderServiceProtocol { + let soraSubqueryFetcher: SoraSubqueryPriceFetcherProtocol + + public init(soraSubqueryFetcher: SoraSubqueryPriceFetcherProtocol) { + self.soraSubqueryFetcher = soraSubqueryFetcher + } + + public func getPrices(for chainAssets: [ChainAsset], currencies: [Currency]) async -> [PriceData] { + let operation = createSoraSubqueryOperation(for: chainAssets, currencies: currencies) + do { + return try operation.extractNoCancellableResultData() + } catch { + return [] + } + } + + private func createSoraSubqueryOperation( + for chainAssets: [ChainAsset], + currencies: [Currency] + ) -> BaseOperation<[PriceData]> { + guard currencies.count == 1, currencies.first?.id == Currency.defaultCurrency().id else { + return BaseOperation.createWithResult([]) + } + + let chainAssets = chainAssets.filter { $0.asset.priceProvider?.type == .sorasubquery } + guard chainAssets.isNotEmpty else { + return BaseOperation.createWithResult([]) + } + + let operation = soraSubqueryFetcher.fetchPriceOperation(for: chainAssets) + return operation + } +} diff --git a/Sources/SSFTransactionHistory/Model/TransactionContext.swift b/Sources/SSFTransactionHistory/Model/TransactionContext.swift index 27e853d4..5c71ad74 100644 --- a/Sources/SSFTransactionHistory/Model/TransactionContext.swift +++ b/Sources/SSFTransactionHistory/Model/TransactionContext.swift @@ -189,7 +189,6 @@ struct TransactionContext { ) return } - case .migration: return nil } diff --git a/Sources/SSFUtils/SSFUtils/Classes/Data/Data+Length.swift b/Sources/SSFUtils/SSFUtils/Classes/Data/Data+Length.swift new file mode 100644 index 00000000..0560e52b --- /dev/null +++ b/Sources/SSFUtils/SSFUtils/Classes/Data/Data+Length.swift @@ -0,0 +1,38 @@ +// +// Data+Length.swift +// MEWwalletKit +// +// Created by Mikhail Nikanorov on 4/29/19. +// Copyright © 2019 MyEtherWallet Inc. All rights reserved. +// + +import Foundation + +public extension Data { + mutating func setLength(_ length: Int, appendFromLeft: Bool = true, negative: Bool = false) { + guard count < length else { + return + } + + let leftLength = length - count + + if appendFromLeft { + self = Data(repeating: negative ? 0xFF : 0x00, count: leftLength) + self + } else { + self += Data(repeating: negative ? 0xFF : 0x00, count: leftLength) + } + } + + func setLengthLeft(_ toBytes: Int, isNegative: Bool = false) -> Data { + var data = self + data.setLength(toBytes, negative: isNegative) + return data + } + + func setLengthRight(_ toBytes: Int, isNegative: Bool = false) -> Data { + var data = self + data.setLength(toBytes, appendFromLeft: false, negative: isNegative) + + return data + } +} diff --git a/Sources/SSFUtils/SSFUtils/Classes/Network/WebSocketEngine.swift b/Sources/SSFUtils/SSFUtils/Classes/Network/WebSocketEngine.swift index f25361bc..ecb5074a 100644 --- a/Sources/SSFUtils/SSFUtils/Classes/Network/WebSocketEngine.swift +++ b/Sources/SSFUtils/SSFUtils/Classes/Network/WebSocketEngine.swift @@ -170,7 +170,6 @@ public final class WebSocketEngine { connection.disconnect() logger?.debug("Cancel socket connection") - case .waitingReconnection: logger?.debug("Cancel reconnection scheduler due to disconnection") reconnectionScheduler.cancel() diff --git a/Sources/SSFXCM/Classes/CallPathDeterminer.swift b/Sources/SSFXCM/Classes/CallPathDeterminer.swift index fd0d88a9..60b7a928 100644 --- a/Sources/SSFXCM/Classes/CallPathDeterminer.swift +++ b/Sources/SSFXCM/Classes/CallPathDeterminer.swift @@ -72,7 +72,6 @@ final class CallPathDeterminerImpl: CallPathDeterminer { return .xTokensTransferMultiasset case (.xTokens, .parachain): return .xTokensTransferMultiasset - case (.polkadotXcm, .relaychain): return .polkadotXcmLimitedReserveWithdrawAssets case (.polkadotXcm, .nativeParachain): diff --git a/Sources/SSFXCM/Classes/Models/XcmCallPath.swift b/Sources/SSFXCM/Classes/Models/XcmCallPath.swift index b6b1aa21..9b57e902 100644 --- a/Sources/SSFXCM/Classes/Models/XcmCallPath.swift +++ b/Sources/SSFXCM/Classes/Models/XcmCallPath.swift @@ -14,12 +14,10 @@ enum XcmCallPath: StorageCodingPathProtocol { switch self { case .parachainId: return (moduleName: "parachainInfo", itemName: "parachainId") - case .xcmPalletLimitedTeleportAssets: return (moduleName: "xcmPallet", itemName: "limited_teleport_assets") case .xcmPalletLimitedReserveTransferAssets: return (moduleName: "xcmPallet", itemName: "limited_reserve_transfer_assets") - case .polkadotXcmTeleportAssets: return (moduleName: "polkadotXcm", itemName: "teleport_assets") case .polkadotXcmLimitedTeleportAssets: @@ -28,12 +26,10 @@ enum XcmCallPath: StorageCodingPathProtocol { return (moduleName: "polkadotXcm", itemName: "limited_reserve_transfer_assets") case .polkadotXcmLimitedReserveWithdrawAssets: return (moduleName: "polkadotXcm", itemName: "limited_reserve_withdraw_assets") - case .xTokensTransfer: return (moduleName: "xTokens", itemName: "transfer") case .xTokensTransferMultiasset: return (moduleName: "xTokens", itemName: "transfer_multiasset") - case .bridgeProxyBurn: return (moduleName: "bridgeProxy", itemName: "burn") case .bridgeProxyTransactions: diff --git a/Sources/SSFXCM/Classes/Models/XcmDestination.swift b/Sources/SSFXCM/Classes/Models/XcmDestination.swift index f7901c13..0be40bca 100644 --- a/Sources/SSFXCM/Classes/Models/XcmDestination.swift +++ b/Sources/SSFXCM/Classes/Models/XcmDestination.swift @@ -8,7 +8,7 @@ enum XcmChainType { case soraMainnet static func determineChainType(for chain: ChainModel) throws -> XcmChainType { - guard let paraId = UInt32(chain.paraId ?? "") else { + guard let paraId = UInt32(chain.properties.paraId ?? "") else { if chain.knownChainEquivalent == .soraMain || chain.knownChainEquivalent == .soraTest { return .soraMainnet } diff --git a/Sources/SSFXCM/Classes/XcmCallFactory.swift b/Sources/SSFXCM/Classes/XcmCallFactory.swift index 7fe85645..1c5e5fb7 100644 --- a/Sources/SSFXCM/Classes/XcmCallFactory.swift +++ b/Sources/SSFXCM/Classes/XcmCallFactory.swift @@ -83,7 +83,7 @@ final class XcmCallFactory: XcmCallFactoryProtocol { weightLimit: BigUInt?, path: XcmCallPath ) -> RuntimeCall { - let destParachainId = UInt32(destChainModel.paraId ?? "") + let destParachainId = UInt32(destChainModel.properties.paraId ?? "") let destParents: UInt8 = destChainModel.isRelaychain ? 1 : 0 let destination = createVersionedMultiLocation( version: version, @@ -138,7 +138,7 @@ final class XcmCallFactory: XcmCallFactoryProtocol { weightLimit: BigUInt, path: XcmCallPath ) -> RuntimeCall { - let destParachainId = UInt32(destChainModel.paraId ?? "") + let destParachainId = UInt32(destChainModel.properties.paraId ?? "") let destParents: UInt8 = destChainModel.isRelaychain ? 1 : 0 let destination = createVersionedMultiLocation( version: version, @@ -191,7 +191,7 @@ final class XcmCallFactory: XcmCallFactoryProtocol { amount: amount ) - let destParachainId = UInt32(destChainModel.paraId ?? "") + let destParachainId = UInt32(destChainModel.properties.paraId ?? "") let destination = createVersionedMultiLocation( version: version, chainModel: destChainModel, @@ -247,7 +247,7 @@ final class XcmCallFactory: XcmCallFactoryProtocol { amount: amount ) - let destParachainId = UInt32(destChainModel.paraId ?? "") + let destParachainId = UInt32(destChainModel.properties.paraId ?? "") let destination = createVersionedMultiLocation( version: version, chainModel: destChainModel, @@ -295,7 +295,7 @@ final class XcmCallFactory: XcmCallFactoryProtocol { let networkId = BridgeTypesGenericNetworkId(from: destChainModel) let assetId = SoraAssetId(wrappedValue: currencyId) - let destParachainId = UInt32(destChainModel.paraId ?? "") + let destParachainId = UInt32(destChainModel.properties.paraId ?? "") let destParents: UInt8 = destChainModel.isRelaychain ? 1 : 0 let destination = createVersionedMultiLocation( version: .V3, diff --git a/Tests/SSFAccountManagmentTests/AccountManagementServiceTests.swift b/Tests/SSFAccountManagmentTests/AccountManagementServiceTests.swift index d1e18aef..a0b02658 100644 --- a/Tests/SSFAccountManagmentTests/AccountManagementServiceTests.swift +++ b/Tests/SSFAccountManagmentTests/AccountManagementServiceTests.swift @@ -68,14 +68,15 @@ final class AccountManagementServiceTests: XCTestCase { Task { [weak self] in do { - let updatedAccount = try await self?.service?.updateEnabilibilty(for: chainAsset.chainAssetId.id) - + let updatedAccount = try await self?.service? + .updateEnabilibilty(for: chainAsset.chainAssetId.id) + DispatchQueue.main.async { [weak self] in XCTAssertTrue( self?.accountManagementWorker? .saveAccountCompletionCalled ?? false ) - + XCTAssertTrue(updatedAccount != nil) } } catch { @@ -102,10 +103,9 @@ final class AccountManagementServiceTests: XCTestCase { private extension AccountManagementServiceTests { enum TestData { static let chain = ChainModel( - rank: 1, disabled: true, chainId: "Kusama", - paraId: "test", + parentId: "2", name: "test", tokens: ChainRemoteTokens( type: .config, @@ -117,22 +117,37 @@ private extension AccountManagementServiceTests { nodes: [], icon: nil, iosMinAppVersion: nil, - properties: .init(addressPrefix: "1", rank: "2", paraId: "test", ethereumBased: true) + properties: .init( + addressPrefix: "1", + rank: "2", + paraId: "test", + ethereumBased: true, + crowdloans: nil + ), + identityChain: nil ) static let asset = AssetModel( id: "2", name: "test", symbol: "XOR", - isUtility: true, precision: 1, - substrateType: .soraAsset, + icon: nil, + tokenProperties: TokenProperties( + priceId: nil, + currencyId: nil, + color: nil, + type: .soraAsset, + isNative: false, + staking: nil + ), + existentialDeposit: nil, + isUtility: true, + purchaseProviders: nil, ethereumType: nil, - tokenProperties: nil, - price: nil, - priceId: nil, + priceProvider: nil, coingeckoPriceId: nil, - priceProvider: nil + priceData: [] ) static let chainAccounts = ChainAccountModel( diff --git a/Tests/SSFAccountManagmentTests/JSONExportServiceTests.swift b/Tests/SSFAccountManagmentTests/JSONExportServiceTests.swift index 790d596d..3de46274 100644 --- a/Tests/SSFAccountManagmentTests/JSONExportServiceTests.swift +++ b/Tests/SSFAccountManagmentTests/JSONExportServiceTests.swift @@ -97,11 +97,9 @@ extension JSONExportServiceTests { ) static let chain = ChainModel( - rank: 1, disabled: true, chainId: "Kusama", parentId: "2", - paraId: "test", name: "test", tokens: ChainRemoteTokens( type: .config, @@ -113,7 +111,14 @@ extension JSONExportServiceTests { nodes: [], icon: nil, iosMinAppVersion: nil, - properties: .init(addressPrefix: "1", rank: "2", paraId: "test", ethereumBased: true) + properties: .init( + addressPrefix: "1", + rank: "2", + paraId: "test", + ethereumBased: true, + crowdloans: nil + ), + identityChain: nil ) static let response = ChainAccountResponse( diff --git a/Tests/SSFAccountManagmentTests/MnemonicExportServiceTests.swift b/Tests/SSFAccountManagmentTests/MnemonicExportServiceTests.swift index 45d1a27f..cbbc3c74 100644 --- a/Tests/SSFAccountManagmentTests/MnemonicExportServiceTests.swift +++ b/Tests/SSFAccountManagmentTests/MnemonicExportServiceTests.swift @@ -97,11 +97,9 @@ extension MnemonicExportServiceTests { ) static let chain = ChainModel( - rank: 1, disabled: true, chainId: "Kusama", parentId: "2", - paraId: "test", name: "test", tokens: ChainRemoteTokens( type: .config, @@ -113,7 +111,14 @@ extension MnemonicExportServiceTests { nodes: [], icon: nil, iosMinAppVersion: nil, - properties: .init(addressPrefix: "1", rank: "2", paraId: "test", ethereumBased: true) + properties: .init( + addressPrefix: "1", + rank: "2", + paraId: "test", + ethereumBased: true, + crowdloans: nil + ), + identityChain: nil ) static let response = ChainAccountResponse( diff --git a/Tests/SSFAccountManagmentTests/SeedExportServiceTests.swift b/Tests/SSFAccountManagmentTests/SeedExportServiceTests.swift index 07ea471b..a23c03d0 100644 --- a/Tests/SSFAccountManagmentTests/SeedExportServiceTests.swift +++ b/Tests/SSFAccountManagmentTests/SeedExportServiceTests.swift @@ -92,11 +92,9 @@ extension SeedExportServiceTests { ) static let chain = ChainModel( - rank: 1, disabled: true, chainId: "Kusama", parentId: "2", - paraId: "test", name: "test", tokens: ChainRemoteTokens( type: .config, @@ -108,7 +106,14 @@ extension SeedExportServiceTests { nodes: [], icon: nil, iosMinAppVersion: nil, - properties: .init(addressPrefix: "1", rank: "2", paraId: "test", ethereumBased: true) + properties: .init( + addressPrefix: "1", + rank: "2", + paraId: "test", + ethereumBased: true, + crowdloans: nil + ), + identityChain: nil ) static let response = ChainAccountResponse( diff --git a/Tests/SSFAssetManagmentTests/ChainAssetsFetchWorkerTests.swift b/Tests/SSFAssetManagmentTests/ChainAssetsFetchWorkerTests.swift index 933d9b22..1a7b01b8 100644 --- a/Tests/SSFAssetManagmentTests/ChainAssetsFetchWorkerTests.swift +++ b/Tests/SSFAssetManagmentTests/ChainAssetsFetchWorkerTests.swift @@ -41,7 +41,7 @@ private extension ChainAssetsFetchWorkerTests { func prepareRepostory() -> CoreDataRepository { let facade = SubstrateStorageTestFacade() let apiKeyInjector = ApiKeyInjectorMock() - let mapper = ChainModelMapper(apiKeyInjector: apiKeyInjector) + let mapper = ChainModelMapper() let chains: [ChainModel] = (0 ..< 10).map { index in ChainModelGenerator.generateChain( diff --git a/Tests/SSFAssetManagmentTests/ChainAssetsFetchingServiceTests.swift b/Tests/SSFAssetManagmentTests/ChainAssetsFetchingServiceTests.swift index 6413035e..f751bda7 100644 --- a/Tests/SSFAssetManagmentTests/ChainAssetsFetchingServiceTests.swift +++ b/Tests/SSFAssetManagmentTests/ChainAssetsFetchingServiceTests.swift @@ -420,11 +420,8 @@ final class ChainAssetsFetchingServiceTests: XCTestCase { func testFetchSortPrice() async { // arrange let chain = ChainModel( - rank: 1, disabled: true, chainId: "Kusama", - parentId: "2", - paraId: "test", name: "test", tokens: ChainRemoteTokens( type: .config, @@ -436,10 +433,20 @@ final class ChainAssetsFetchingServiceTests: XCTestCase { nodes: [], icon: nil, iosMinAppVersion: nil, - properties: .init(addressPrefix: "1", rank: "2", paraId: "test", ethereumBased: true) + properties: .init( + addressPrefix: "1", + rank: "1", + paraId: "test", + ethereumBased: true, + crowdloans: nil + ), + identityChain: nil ) - let extectedAssetArray = chain.tokens.tokens?.compactMap { ChainAsset(chain: chain, asset: $0) } + let extectedAssetArray = chain.tokens.tokens?.compactMap { ChainAsset( + chain: chain, + asset: $0 + ) } let chainAssetsFetcher = ChainAssetsFetchWorkerProtocolMock() chainAssetsFetcher.getChainAssetsModelsReturnValue = extectedAssetArray @@ -551,7 +558,12 @@ final class ChainAssetsFetchingServiceTests: XCTestCase { let chain = TestData.chain let asset = TestData.asset let chainAsset = ChainAsset(chain: chain, asset: asset) - chain.tokens = ChainRemoteTokens(type: .config, whitelist: nil, utilityId: nil, tokens: [asset]) + chain.tokens = ChainRemoteTokens( + type: .config, + whitelist: nil, + utilityId: nil, + tokens: [asset] + ) let chainWithStacking = TestData.chainWithStacking let assetWithStacking = TestData.assetWithStacking @@ -618,11 +630,9 @@ final class ChainAssetsFetchingServiceTests: XCTestCase { func testFetchSortAssetId() async { // arrange let chain = ChainModel( - rank: 3, disabled: true, chainId: "Kusama", parentId: "2", - paraId: "test", name: "test", tokens: ChainRemoteTokens( type: .config, @@ -634,10 +644,20 @@ final class ChainAssetsFetchingServiceTests: XCTestCase { nodes: [], icon: nil, iosMinAppVersion: nil, - properties: .init(addressPrefix: "1", rank: "2", paraId: "test", ethereumBased: true) + properties: .init( + addressPrefix: "1", + rank: "3", + paraId: "test", + ethereumBased: true, + crowdloans: nil + ), + identityChain: nil ) - let extectedAssetArray = chain.tokens.tokens?.compactMap { ChainAsset(chain: chain, asset: $0) } + let extectedAssetArray = chain.tokens.tokens?.compactMap { ChainAsset( + chain: chain, + asset: $0 + ) } let chainAssetsFetcher = ChainAssetsFetchWorkerProtocolMock() chainAssetsFetcher.getChainAssetsModelsReturnValue = extectedAssetArray @@ -662,10 +682,8 @@ final class ChainAssetsFetchingServiceTests: XCTestCase { private extension ChainAssetsFetchingServiceTests { enum TestData { static let chain = ChainModel( - rank: 1, disabled: true, chainId: "Kusama", - paraId: "test", name: "test", tokens: ChainRemoteTokens( type: .config, @@ -677,29 +695,40 @@ private extension ChainAssetsFetchingServiceTests { nodes: [], icon: nil, iosMinAppVersion: nil, - properties: .init(addressPrefix: "test", ethereumBased: true, crowdloans: true) + properties: .init( + addressPrefix: "test", + ethereumBased: true, + crowdloans: true + ), + identityChain: nil ) static let asset = AssetModel( id: "2", name: "test", symbol: "XOR", - isUtility: true, precision: 1, - substrateType: .soraAsset, + icon: nil, + tokenProperties: TokenProperties( + priceId: nil, + currencyId: nil, + color: nil, + type: .soraAsset, + isNative: false, + staking: nil + ), + existentialDeposit: nil, + isUtility: true, + purchaseProviders: nil, ethereumType: nil, - tokenProperties: nil, - price: nil, - priceId: nil, + priceProvider: nil, coingeckoPriceId: nil, - priceProvider: nil + priceData: [] ) - + static let chainWithStacking = ChainModel( - rank: 2, disabled: true, chainId: "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3", - paraId: "test", name: "test1", tokens: ChainRemoteTokens( type: .config, @@ -711,22 +740,35 @@ private extension ChainAssetsFetchingServiceTests { nodes: [], icon: nil, iosMinAppVersion: nil, - properties: .init(addressPrefix: "1", crowdloans: false) + properties: ChainProperties( + addressPrefix: "1", + rank: "2", + paraId: "test" + ), + identityChain: nil ) static let assetWithStacking = AssetModel( id: "3", name: "test", symbol: "XOR2", - isUtility: true, precision: 1, - substrateType: .soraAsset, + icon: nil, + tokenProperties: TokenProperties( + priceId: nil, + currencyId: nil, + color: nil, + type: .soraAsset, + isNative: false, + staking: .relayChain + ), + existentialDeposit: nil, + isUtility: true, + purchaseProviders: nil, ethereumType: nil, - tokenProperties: TokenProperties(stacking: "relaychain"), - price: nil, - priceId: nil, + priceProvider: nil, coingeckoPriceId: nil, - priceProvider: nil + priceData: [] ) } } diff --git a/Tests/SSFIndexersTests/History/BaseHistoryServiceTestCase.swift b/Tests/SSFIndexersTests/History/BaseHistoryServiceTestCase.swift index d2398d19..6150edf0 100644 --- a/Tests/SSFIndexersTests/History/BaseHistoryServiceTestCase.swift +++ b/Tests/SSFIndexersTests/History/BaseHistoryServiceTestCase.swift @@ -36,13 +36,13 @@ class BaseHistoryServiceTestCase: XCTestCase { substrateType: nil, ethereumType: ethereumType, tokenProperties: - TokenProperties( - priceId: nil, - currencyId: nil, - color: nil, - type: .normal, - isNative: true - ), + TokenProperties( + priceId: nil, + currencyId: nil, + color: nil, + type: .normal, + isNative: true + ), price: nil, priceId: nil, coingeckoPriceId: nil, @@ -56,7 +56,12 @@ class BaseHistoryServiceTestCase: XCTestCase { parentId: "2", paraId: "test", name: "test", - tokens: ChainRemoteTokens(type: .config, whitelist: nil, utilityId: nil, tokens: [asset]), + tokens: ChainRemoteTokens( + type: .config, + whitelist: nil, + utilityId: nil, + tokens: [asset] + ), xcm: nil, nodes: [], types: nil, diff --git a/Tests/SSFTransferServiceTests/SubstrateCallFactoryTests.swift b/Tests/SSFTransferServiceTests/SubstrateCallFactoryTests.swift index df986c82..fcfc869b 100644 --- a/Tests/SSFTransferServiceTests/SubstrateCallFactoryTests.swift +++ b/Tests/SSFTransferServiceTests/SubstrateCallFactoryTests.swift @@ -375,7 +375,8 @@ final class SubstrateCallFactoryTests: XCTestCase { private func generateAccountId(for chain: ChainModel) -> AccountId { let chainFormate: SFChainFormat = chain - .isEthereumBased ? .sfEthereum : .sfSubstrate(UInt16(chain.properties.addressPrefix) ?? 69) + .isEthereumBased ? .sfEthereum : + .sfSubstrate(UInt16(chain.properties.addressPrefix) ?? 69) let accountId = AddressFactory.randomAccountId(for: chainFormate) return accountId } diff --git a/Tests/SSFXCMTests/BridgeProxyBurnCallTests.swift b/Tests/SSFXCMTests/BridgeProxyBurnCallTests.swift index bb7fc6e5..51e60a8d 100644 --- a/Tests/SSFXCMTests/BridgeProxyBurnCallTests.swift +++ b/Tests/SSFXCMTests/BridgeProxyBurnCallTests.swift @@ -158,17 +158,27 @@ extension BridgeProxyBurnCallTests { .appendingPathExtension("json") static let chain = ChainModel( - rank: 1, disabled: false, chainId: "b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe", - paraId: "1", name: "test", - tokens: ChainRemoteTokens(type: .config, whitelist: nil, utilityId: nil, tokens: []), + tokens: ChainRemoteTokens( + type: .config, + whitelist: nil, + utilityId: nil, + tokens: [] + ), xcm: nil, nodes: Set([ChainNodeModel(url: TestData.url, name: "test", apikey: nil)]), icon: nil, iosMinAppVersion: nil, - properties: ChainProperties(addressPrefix: "1") + properties: .init( + addressPrefix: "1", + rank: "1", + paraId: "1", + ethereumBased: true, + crowdloans: nil + ), + identityChain: nil ) static let subNetworkId = BridgeTypesSubNetworkId(from: chain) diff --git a/Tests/SSFXCMTests/XcmCallFactoryTests.swift b/Tests/SSFXCMTests/XcmCallFactoryTests.swift index e34027fd..15667570 100644 --- a/Tests/SSFXCMTests/XcmCallFactoryTests.swift +++ b/Tests/SSFXCMTests/XcmCallFactoryTests.swift @@ -250,13 +250,14 @@ extension XcmCallFactoryTests { static let chain = XcmChain( xcmVersion: .V1, destWeightIsPrimitive: true, - availableAssets: [.init(id: "0", symbol: "0")], + availableAssets: [.init(id: "0", symbol: "0", minAmount: nil)], availableDestinations: [.init( chainId: "1", bridgeParachainId: "2", assets: [.init( id: "0", - symbol: "0" + symbol: "0", + minAmount: nil )] )] ) @@ -268,17 +269,25 @@ extension XcmCallFactoryTests { ) static let model = ChainModel( - rank: 0, disabled: false, chainId: "0", - paraId: "0", name: "model", - tokens: ChainRemoteTokens(type: .config, whitelist: nil, utilityId: nil, tokens: []), + tokens: ChainRemoteTokens( + type: .config, + whitelist: nil, + utilityId: nil, + tokens: [] + ), xcm: chain, nodes: Set([node]), icon: nil, iosMinAppVersion: nil, - properties: ChainProperties(addressPrefix: "0") + properties: .init( + addressPrefix: "0", + rank: "0", + paraId: "0" + ), + identityChain: nil ) } } diff --git a/Tests/SSFXCMTests/XcmChainsConfigFetcherTests.swift b/Tests/SSFXCMTests/XcmChainsConfigFetcherTests.swift index 2f5756cd..79dc86c6 100644 --- a/Tests/SSFXCMTests/XcmChainsConfigFetcherTests.swift +++ b/Tests/SSFXCMTests/XcmChainsConfigFetcherTests.swift @@ -173,10 +173,8 @@ final class XcmChainsConfigFetcherTests: XCTestCase { extension XcmChainsConfigFetcherTests { enum TestData { static let firstChain = ChainModel( - rank: 0, disabled: false, chainId: "0", - paraId: "1001", name: "test1", tokens: ChainRemoteTokens( type: .config, @@ -189,14 +187,16 @@ extension XcmChainsConfigFetcherTests { destWeightIsPrimitive: true, availableAssets: [.init( id: "0", - symbol: "0" + symbol: "0", + minAmount: nil )], availableDestinations: [.init( chainId: "0", bridgeParachainId: "2", assets: [.init( id: "1", - symbol: "1" + symbol: "1", + minAmount: nil )] )] ), @@ -207,14 +207,13 @@ extension XcmChainsConfigFetcherTests { )]), icon: nil, iosMinAppVersion: nil, - properties: ChainProperties(addressPrefix: "0") + properties: ChainProperties(addressPrefix: "0"), + identityChain: nil ) static let secondChain = ChainModel( - rank: 1, disabled: false, chainId: "1", - paraId: "1002", name: "test2", tokens: ChainRemoteTokens( type: .config, @@ -227,12 +226,17 @@ extension XcmChainsConfigFetcherTests { destWeightIsPrimitive: true, availableAssets: [.init( id: "1", - symbol: "1" + symbol: "1", + minAmount: nil )], availableDestinations: [.init( chainId: "1", bridgeParachainId: "2", - assets: [.init(id: "0", symbol: "0")] + assets: [.init( + id: "0", + symbol: "0", + minAmount: nil + )] )] ), nodes: Set([ChainNodeModel( @@ -242,14 +246,16 @@ extension XcmChainsConfigFetcherTests { )]), icon: nil, iosMinAppVersion: nil, - properties: ChainProperties(addressPrefix: "1") + properties: ChainProperties( + addressPrefix: "1", + paraId: "1002" + ), + identityChain: nil ) static let errorChain = ChainModel( - rank: 2, disabled: false, chainId: "2", - paraId: "1", name: "test3", tokens: ChainRemoteTokens( type: .config, @@ -265,7 +271,11 @@ extension XcmChainsConfigFetcherTests { )]), icon: nil, iosMinAppVersion: nil, - properties: ChainProperties(addressPrefix: "2") + properties: ChainProperties( + addressPrefix: "2", + paraId: "1" + ), + identityChain: nil ) } } diff --git a/Tests/SSFXCMTests/XcmDependencyContainerTests.swift b/Tests/SSFXCMTests/XcmDependencyContainerTests.swift index 6a82c17f..a1d92839 100644 --- a/Tests/SSFXCMTests/XcmDependencyContainerTests.swift +++ b/Tests/SSFXCMTests/XcmDependencyContainerTests.swift @@ -62,25 +62,30 @@ extension XcmDependencyContainerTests { ) static let chainModel = ChainModel( - rank: 0, disabled: false, chainId: "0", - paraId: "1001", name: "test1", - tokens: ChainRemoteTokens(type: .config, whitelist: nil, utilityId: nil, tokens: []), + tokens: ChainRemoteTokens( + type: .config, + whitelist: nil, + utilityId: nil, + tokens: [] + ), xcm: XcmChain( xcmVersion: .V3, destWeightIsPrimitive: true, availableAssets: [.init( id: "0", - symbol: "0" + symbol: "0", + minAmount: nil )], availableDestinations: [.init( chainId: "0", bridgeParachainId: "2", assets: [.init( id: "1", - symbol: "1" + symbol: "1", + minAmount: nil )] )] ), @@ -91,7 +96,10 @@ extension XcmDependencyContainerTests { )]), icon: nil, iosMinAppVersion: nil, - properties: ChainProperties(addressPrefix: "0") + properties: ChainProperties( + addressPrefix: "0" + ), + identityChain: nil ) static let runtimeProvider = RuntimeProvider( diff --git a/Tests/SSFXCMTests/XcmDestinationTests.swift b/Tests/SSFXCMTests/XcmDestinationTests.swift index 4c56d2fa..99a63e81 100644 --- a/Tests/SSFXCMTests/XcmDestinationTests.swift +++ b/Tests/SSFXCMTests/XcmDestinationTests.swift @@ -34,26 +34,40 @@ extension XcmDestinationTests { .appendingPathExtension("json") static let chain = ChainModel( - rank: 1, disabled: false, chainId: "1", - paraId: "1001", name: "test", - tokens: ChainRemoteTokens(type: .config, whitelist: nil, utilityId: nil, tokens: []), + tokens: ChainRemoteTokens( + type: .config, + whitelist: nil, + utilityId: nil, + tokens: [] + ), xcm: nil, - nodes: Set([ChainNodeModel(url: TestData.url, name: "test", apikey: nil)]), + nodes: Set([ChainNodeModel( + url: TestData.url, + name: "test", + apikey: nil + )]), icon: nil, iosMinAppVersion: nil, - properties: ChainProperties(addressPrefix: "1") + properties: ChainProperties( + addressPrefix: "0", + rank: "1" + ), + identityChain: nil ) static let errorChain = ChainModel( - rank: 1, disabled: false, chainId: "1", - paraId: "1", name: "test", - tokens: ChainRemoteTokens(type: .config, whitelist: nil, utilityId: nil, tokens: []), + tokens: ChainRemoteTokens( + type: .config, + whitelist: nil, + utilityId: nil, + tokens: [] + ), xcm: nil, nodes: Set([ChainNodeModel( url: TestData.url, @@ -62,7 +76,11 @@ extension XcmDestinationTests { )]), icon: nil, iosMinAppVersion: nil, - properties: ChainProperties(addressPrefix: "1") + properties: ChainProperties( + addressPrefix: "1", + rank: "1" + ), + identityChain: nil ) } } diff --git a/Tests/SSFXCMTests/XcmExtrinsicBuilderTests.swift b/Tests/SSFXCMTests/XcmExtrinsicBuilderTests.swift index f1baabd6..b10402e0 100644 --- a/Tests/SSFXCMTests/XcmExtrinsicBuilderTests.swift +++ b/Tests/SSFXCMTests/XcmExtrinsicBuilderTests.swift @@ -197,25 +197,30 @@ final class XcmExtrinsicBuilderTests: XCTestCase { private extension XcmExtrinsicBuilderTests { private enum TestData { static let chainModel = ChainModel( - rank: 0, disabled: false, chainId: "0", - paraId: "0", name: "model", - tokens: ChainRemoteTokens(type: .config, whitelist: nil, utilityId: nil, tokens: []), + tokens: ChainRemoteTokens( + type: .config, + whitelist: nil, + utilityId: nil, + tokens: [] + ), xcm: XcmChain( xcmVersion: .V1, destWeightIsPrimitive: true, availableAssets: [.init( id: "0", - symbol: "0" + symbol: "0", + minAmount: nil )], availableDestinations: [.init( chainId: "1", bridgeParachainId: "2", assets: [.init( id: "0", - symbol: "0" + symbol: "0", + minAmount: nil )] )] ), @@ -224,10 +229,14 @@ private extension XcmExtrinsicBuilderTests { name: "node", apikey: nil )]), - icon: nil, iosMinAppVersion: nil, - properties: ChainProperties(addressPrefix: "0") + properties: ChainProperties( + addressPrefix: "0", + rank: "0", + paraId: "0" + ), + identityChain: nil ) static let reserveTransferCall = ReserveTransferAssetsCall( diff --git a/Tests/SSFXCMTests/XcmExtrinsicServiceTests.swift b/Tests/SSFXCMTests/XcmExtrinsicServiceTests.swift index b1fbb017..6c6d6858 100644 --- a/Tests/SSFXCMTests/XcmExtrinsicServiceTests.swift +++ b/Tests/SSFXCMTests/XcmExtrinsicServiceTests.swift @@ -153,10 +153,8 @@ private extension XcmExtrinsicServiceTests { ) static let fromChain = ChainModel( - rank: 0, disabled: false, chainId: "0", - paraId: "1001", name: "test1", tokens: ChainRemoteTokens( type: .config, @@ -168,38 +166,34 @@ private extension XcmExtrinsicServiceTests { id: "0", name: "0", symbol: "0", - isUtility: true, precision: 0, - substrateType: .soraAsset, - ethereumType: nil, - tokenProperties: - TokenProperties( - priceId: "0", - currencyId: "0", - color: "0", - type: .soraAsset, - isNative: true - ), - price: nil, - priceId: nil, - coingeckoPriceId: nil, - priceProvider: nil + tokenProperties: TokenProperties( + priceId: "0", + currencyId: "0", + color: "0", + type: .soraAsset, + isNative: true + ), + isUtility: true ), ] - )), + ) + ), xcm: XcmChain( xcmVersion: .V3, destWeightIsPrimitive: true, availableAssets: [.init( id: "0", - symbol: "0" + symbol: "0", + minAmount: nil )], availableDestinations: [.init( chainId: "0", bridgeParachainId: "2", assets: [.init( id: "1", - symbol: "1" + symbol: "1", + minAmount: nil )] )] ), @@ -210,7 +204,12 @@ private extension XcmExtrinsicServiceTests { )]), icon: nil, iosMinAppVersion: nil, - properties: ChainProperties(addressPrefix: "0") + properties: ChainProperties( + addressPrefix: "0", + rank: "0", + paraId: "1001" + ), + identityChain: nil ) static let paths: [XcmCallPath] = [