From 522fd4be5b3f57a7bdfbcf5d07ff89f34be2d997 Mon Sep 17 00:00:00 2001 From: Hwee-Boon Yar Date: Mon, 7 Aug 2023 10:04:41 +0800 Subject: [PATCH] [TokenScript] WIP for attestation support --- AlphaWallet.xcodeproj/project.pbxproj | 12 ++ AlphaWallet/Common/Services/Application.swift | 11 +- .../TokenScript/FetchTokenScriptFiles.swift | 67 ++++++ .../Coordinators/TokensCoordinator.swift | 5 +- .../Coordinators/ActiveWalletViewTests.swift | 6 + .../PaymentCoordinatorTests.swift | 1 + .../AlphaWalletAttestation/Attestation.swift | 15 ++ .../AttestationsStore.swift | 10 +- .../FetchTokenScriptFiles.swift | 49 +---- .../Tokens/AlphaWalletTokensService.swift | 13 +- .../Tokens/TokensProcessingPipeline.swift | 11 +- .../Models/AssetDefinitionStore.swift | 71 ++++++- .../TokenScriptForAttestationStore.swift | 13 ++ .../Models/TokenScriptSignatureVerifier.swift | 5 + .../Types/AssetDefinitionStoreProtocol.swift | 3 + .../Types/TokenScriptStatusResolver.swift | 1 + .../Models/XMLHandler.swift | 190 ++++++++++++++++-- 17 files changed, 392 insertions(+), 91 deletions(-) create mode 100644 AlphaWallet/TokenScript/FetchTokenScriptFiles.swift create mode 100644 modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/TokenScriptForAttestationStore.swift diff --git a/AlphaWallet.xcodeproj/project.pbxproj b/AlphaWallet.xcodeproj/project.pbxproj index 29738a287..7c82739aa 100644 --- a/AlphaWallet.xcodeproj/project.pbxproj +++ b/AlphaWallet.xcodeproj/project.pbxproj @@ -359,6 +359,7 @@ 5E7C7C53FC6F46DF501E8AD1 /* CallSmartContractFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C78E17FA9C8C892EEA355 /* CallSmartContractFunctionTests.swift */; }; 5E7C7C60BAF11B0BD135FC1E /* GroupActivityViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7E58DD0CF4E2B35B6ED2 /* GroupActivityViewCell.swift */; }; 5E7C7C6CCD56211F6F4F6362 /* AttestationViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C707BB91428781B60832A /* AttestationViewCell.swift */; }; + 5E7C7C7C7B62A23FA7C3669B /* FetchTokenScriptFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7FD2D3F1340BD277AF86 /* FetchTokenScriptFiles.swift */; }; 5E7C7C869A09FD09DCE77EE6 /* ActivityCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C760CC5A9E144BDB5451D /* ActivityCellViewModel.swift */; }; 5E7C7C8C689E972389F4A564 /* CrashReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C76A087FB8690364F8552 /* CrashReporterViewController.swift */; }; 5E7C7C98EAF40E8110241DBD /* NonFungibleTokenViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C783E3ADA4CF9554A0E7D /* NonFungibleTokenViewCell.swift */; }; @@ -1353,6 +1354,7 @@ 5E7C7FB7C3FB2A9CC0CC51D7 /* TokensViewControllerTableViewSectionHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokensViewControllerTableViewSectionHeader.swift; sourceTree = ""; }; 5E7C7FB99843529061368DA1 /* LocalesViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalesViewModel.swift; sourceTree = ""; }; 5E7C7FCE2427A30ACD860DF8 /* ServerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerViewModel.swift; sourceTree = ""; }; + 5E7C7FD2D3F1340BD277AF86 /* FetchTokenScriptFiles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchTokenScriptFiles.swift; sourceTree = ""; }; 5E7C7FE30D58E4022AF04E48 /* AssetDefinitionStoreTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetDefinitionStoreTests.swift; sourceTree = ""; }; 5E7C7FE5EEC96A7CDF62213F /* BrowserHomeHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserHomeHeaderView.swift; sourceTree = ""; }; 5E7C7FF2EF77D5004A600DDB /* SuccessOverlayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SuccessOverlayView.swift; sourceTree = ""; }; @@ -2046,6 +2048,7 @@ 5E7C704885516A808B3B8866 /* ErrorLocalizations.swift */, 5E7C7D784300C463FF3C7514 /* HardwareWallet */, 5E7C7864DB8E82BACE2F5F1F /* ImportedTypes.swift */, + 5E7C7CA7694367E5D9D8DE99 /* Attestation */, ); path = AlphaWallet; sourceTree = ""; @@ -3115,6 +3118,7 @@ 5E7C71077F12A2CF6733081D /* Views */, 5E7C7878E9E27F0CA05DFF03 /* Coordinators */, 5E7C78D36531AF80D3BAC20E /* ViewModels */, + 5E7C7FD2D3F1340BD277AF86 /* FetchTokenScriptFiles.swift */, ); path = TokenScript; sourceTree = ""; @@ -3373,6 +3377,13 @@ path = Types; sourceTree = ""; }; + 5E7C7CA7694367E5D9D8DE99 /* Attestation */ = { + isa = PBXGroup; + children = ( + ); + path = Attestation; + sourceTree = ""; + }; 5E7C7CC82D5F600655CF9FA7 /* Logic */ = { isa = PBXGroup; children = ( @@ -5884,6 +5895,7 @@ 5E7C74D586547B32EF5CD91C /* AcceptProposalViewController.swift in Sources */, 5E7C7270C6A83CC1ADC106DC /* AcceptAuthRequestViewController.swift in Sources */, 5E7C7F776D272AC23213B3BA /* ImportedTypes.swift in Sources */, + 5E7C7C7C7B62A23FA7C3669B /* FetchTokenScriptFiles.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/AlphaWallet/Common/Services/Application.swift b/AlphaWallet/Common/Services/Application.swift index d92b98d93..6c33b1717 100644 --- a/AlphaWallet/Common/Services/Application.swift +++ b/AlphaWallet/Common/Services/Application.swift @@ -432,13 +432,12 @@ class Application: WalletDependenciesProvidable { sessionsProvider.start() - let tokensService = AlphaWalletTokensService( - sessionsProvider: sessionsProvider, - tokensDataStore: tokensDataStore, - analytics: analytics, - transactionsStorage: transactionsDataStore, + let fetchTokenScriptFiles = FetchTokenScriptFilesImpl( assetDefinitionStore: assetDefinitionStore, - transporter: BaseApiTransporter()) + tokensDataStore: tokensDataStore, + sessionsProvider: sessionsProvider) + + let tokensService = AlphaWalletTokensService(sessionsProvider: sessionsProvider, tokensDataStore: tokensDataStore, analytics: analytics, transactionsStorage: transactionsDataStore, assetDefinitionStore: assetDefinitionStore, fetchTokenScriptFiles: fetchTokenScriptFiles, transporter: BaseApiTransporter()) let tokensPipeline: TokensProcessingPipeline = WalletDataProcessingPipeline( wallet: wallet, diff --git a/AlphaWallet/TokenScript/FetchTokenScriptFiles.swift b/AlphaWallet/TokenScript/FetchTokenScriptFiles.swift new file mode 100644 index 000000000..929f07d01 --- /dev/null +++ b/AlphaWallet/TokenScript/FetchTokenScriptFiles.swift @@ -0,0 +1,67 @@ +// Copyright © 2018 Stormbird PTE. LTD. + +import Foundation +import Combine +import AlphaWalletAttestation +import AlphaWalletFoundation +import AlphaWalletTokenScript + +public class FetchTokenScriptFilesImpl: FetchTokenScriptFiles { + private let assetDefinitionStore: AssetDefinitionStore + private let tokensDataStore: TokensDataStore + private let sessionsProvider: SessionsProvider + private let queue = DispatchQueue(label: "com.FetchAssetDefinitions.UpdateQueue") + private var cancellable = Set() + + public init(assetDefinitionStore: AssetDefinitionStore, + tokensDataStore: TokensDataStore, + sessionsProvider: SessionsProvider) { + + self.assetDefinitionStore = assetDefinitionStore + self.tokensDataStore = tokensDataStore + self.sessionsProvider = sessionsProvider + } + + public func start() { + fetchForTokens() + fetchForAttestations() + } + + private func fetchForTokens() { + sessionsProvider.sessions + .map { $0.keys } + .receive(on: queue) + .flatMap { [tokensDataStore] servers in + asFuture { + await tokensDataStore.tokens(for: Array(servers)) + } + } + .map { tokens in + return tokens.filter { + switch $0.type { + case .erc20, .erc721, .erc875, .erc721ForTickets, .erc1155: + return true + case .nativeCryptocurrency: + return false + } + }.map { AddressAndOptionalRPCServer(address: $0.contractAddress, server: $0.server) } + }.sink { [assetDefinitionStore] contractsInDatabase in + let contractsWithTokenScriptFileFromOfficialRepo = assetDefinitionStore.contractsWithTokenScriptFileFromOfficialRepo.map { AddressAndOptionalRPCServer(address: $0, server: nil) } + + let contractsAndServers = Array(Set(contractsInDatabase + contractsWithTokenScriptFileFromOfficialRepo)) + assetDefinitionStore.fetchXMLs(forContractsAndServers: contractsAndServers) + }.store(in: &cancellable) + } + + private func fetchForAttestations() { + let attestations = AttestationsStore.allAttestations() + for each in attestations { + if let url = each.scriptUri { + Task { @MainActor in + await assetDefinitionStore.fetchXMLForAttestation(withScriptURL: url) + //TODO: attestations+TokenScript to implement + } + } + } + } +} diff --git a/AlphaWallet/Tokens/Coordinators/TokensCoordinator.swift b/AlphaWallet/Tokens/Coordinators/TokensCoordinator.swift index 5b13303b9..11f0d7d7e 100644 --- a/AlphaWallet/Tokens/Coordinators/TokensCoordinator.swift +++ b/AlphaWallet/Tokens/Coordinators/TokensCoordinator.swift @@ -248,7 +248,10 @@ class TokensCoordinator: Coordinator { } private func importAttestation(_ attestation: Attestation, intoWallet address: AlphaWallet.Address) { - attestationsStore.addAttestation(attestation, forWallet: address) + let isSuccessful = attestationsStore.addAttestation(attestation, forWallet: address) + if isSuccessful { + //TODO: attestations+TokenScript to implement reload like when we download TokenScript files at launch + } } } diff --git a/AlphaWalletTests/Coordinators/ActiveWalletViewTests.swift b/AlphaWalletTests/Coordinators/ActiveWalletViewTests.swift index d1b59d83e..5c6bca643 100644 --- a/AlphaWalletTests/Coordinators/ActiveWalletViewTests.swift +++ b/AlphaWalletTests/Coordinators/ActiveWalletViewTests.swift @@ -549,6 +549,12 @@ class ActiveWalletViewTests: XCTestCase { } // swiftlint:enable type_body_length +class FakeFetchTokenScriptFiles: FetchTokenScriptFiles { + func start() { + //no-op + } +} + import AlphaWalletNotifications extension BasePushNotificationsService { diff --git a/AlphaWalletTests/ViewControllers/PaymentCoordinatorTests.swift b/AlphaWalletTests/ViewControllers/PaymentCoordinatorTests.swift index df51f319d..3ecc985b2 100644 --- a/AlphaWalletTests/ViewControllers/PaymentCoordinatorTests.swift +++ b/AlphaWalletTests/ViewControllers/PaymentCoordinatorTests.swift @@ -55,6 +55,7 @@ extension WalletDataProcessingPipeline { analytics: fas, transactionsStorage: transactionsDataStore, assetDefinitionStore: .make(), + fetchTokenScriptFiles: FakeFetchTokenScriptFiles(), transporter: FakeApiTransporter()) let pipeline: TokensProcessingPipeline = WalletDataProcessingPipeline( diff --git a/modules/AlphaWalletAttestation/AlphaWalletAttestation/Attestation.swift b/modules/AlphaWalletAttestation/AlphaWalletAttestation/Attestation.swift index 08d1b3ec0..1f43a1dc0 100644 --- a/modules/AlphaWalletAttestation/AlphaWalletAttestation/Attestation.swift +++ b/modules/AlphaWalletAttestation/AlphaWalletAttestation/Attestation.swift @@ -191,6 +191,21 @@ public struct Attestation: Codable, Hashable { public var server: RPCServer { easAttestation.server } //TODO not hardcode public var name: String { "EAS Attestation" } + public var scriptUri: URL? { + let url: URL? = data.compactMap { each in + if each.type.name == "scriptURI" { + switch each.value { + case .string(let value): + return URL(string: value) + case .address, .bool, .bytes, .int, .uint: + return nil + } + } else { + return nil + } + }.first + return url + } private init(data: [TypeValuePair], easAttestation: EasAttestation, isValidAttestationIssuer: Bool, source: String) { self.data = data diff --git a/modules/AlphaWalletAttestation/AlphaWalletAttestation/AttestationsStore.swift b/modules/AlphaWalletAttestation/AlphaWalletAttestation/AttestationsStore.swift index d73c23f0e..1ea454e00 100644 --- a/modules/AlphaWalletAttestation/AlphaWalletAttestation/AttestationsStore.swift +++ b/modules/AlphaWalletAttestation/AlphaWalletAttestation/AttestationsStore.swift @@ -21,21 +21,27 @@ public class AttestationsStore { self.attestations = functional.readAttestations(forWallet: wallet, from: Self.fileUrl) } - public func addAttestation(_ attestation: Attestation, forWallet address: AlphaWallet.Address) { + public static func allAttestations() -> [Attestation] { + return functional.readAttestations(from: fileUrl).flatMap { $0.value } + } + + public func addAttestation(_ attestation: Attestation, forWallet address: AlphaWallet.Address) -> Bool { var allAttestations = functional.readAttestations(from: Self.fileUrl) do { var attestationsForWallet: [Attestation] = allAttestations[address] ?? [] guard !attestations.contains(attestation) else { infoLog("[Attestation] Attestation already exist. Skipping") - return + return false } attestationsForWallet.append(attestation) allAttestations[address] = attestationsForWallet try saveAttestations(attestations: allAttestations) attestations = attestationsForWallet infoLog("[Attestation] Imported attestation") + return true } catch { errorLog("[Attestation] failed to encode attestations while adding attestation to: \(Self.fileUrl.absoluteString) error: \(error)") + return false } } diff --git a/modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/FetchTokenScriptFiles.swift b/modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/FetchTokenScriptFiles.swift index ce59751e3..612dfffe7 100644 --- a/modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/FetchTokenScriptFiles.swift +++ b/modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/FetchTokenScriptFiles.swift @@ -1,48 +1,5 @@ -// Copyright © 2018 Stormbird PTE. LTD. +// Copyright © 2023 Stormbird PTE. LTD. -import Foundation -import Combine -import AlphaWalletTokenScript - -public class FetchTokenScriptFiles { - private let assetDefinitionStore: AssetDefinitionStore - private let tokensDataStore: TokensDataStore - private let sessionsProvider: SessionsProvider - private let queue = DispatchQueue(label: "com.FetchAssetDefinitions.UpdateQueue") - private var cancellable = Set() - - public init(assetDefinitionStore: AssetDefinitionStore, - tokensDataStore: TokensDataStore, - sessionsProvider: SessionsProvider) { - - self.assetDefinitionStore = assetDefinitionStore - self.tokensDataStore = tokensDataStore - self.sessionsProvider = sessionsProvider - } - - public func start() { - sessionsProvider.sessions - .map { $0.keys } - .receive(on: queue) - .flatMap { [tokensDataStore] servers in - asFuture { - await tokensDataStore.tokens(for: Array(servers)) - } - } - .map { tokens in - return tokens.filter { - switch $0.type { - case .erc20, .erc721, .erc875, .erc721ForTickets, .erc1155: - return true - case .nativeCryptocurrency: - return false - } - }.map { AddressAndOptionalRPCServer(address: $0.contractAddress, server: $0.server) } - }.sink { [assetDefinitionStore] contractsInDatabase in - let contractsWithTokenScriptFileFromOfficialRepo = assetDefinitionStore.contractsWithTokenScriptFileFromOfficialRepo.map { AddressAndOptionalRPCServer(address: $0, server: nil) } - - let contractsAndServers = Array(Set(contractsInDatabase + contractsWithTokenScriptFileFromOfficialRepo)) - assetDefinitionStore.fetchXMLs(forContractsAndServers: contractsAndServers) - }.store(in: &cancellable) - } +public protocol FetchTokenScriptFiles { + func start() } diff --git a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/AlphaWalletTokensService.swift b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/AlphaWalletTokensService.swift index 5acf5dda9..5d4facf68 100644 --- a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/AlphaWalletTokensService.swift +++ b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/AlphaWalletTokensService.swift @@ -58,23 +58,14 @@ public class AlphaWalletTokensService: TokensService { .eraseToAnyPublisher() }() - public init(sessionsProvider: SessionsProvider, - tokensDataStore: TokensDataStore, - analytics: AnalyticsLogger, - transactionsStorage: TransactionDataStore, - assetDefinitionStore: AssetDefinitionStore, - transporter: ApiTransporter) { - + public init(sessionsProvider: SessionsProvider, tokensDataStore: TokensDataStore, analytics: AnalyticsLogger, transactionsStorage: TransactionDataStore, assetDefinitionStore: AssetDefinitionStore, fetchTokenScriptFiles: FetchTokenScriptFiles, transporter: ApiTransporter) { self.transporter = transporter self.sessionsProvider = sessionsProvider self.tokensDataStore = tokensDataStore self.analytics = analytics self.transactionsStorage = transactionsStorage self.assetDefinitionStore = assetDefinitionStore - self.fetchTokenScriptFiles = FetchTokenScriptFiles( - assetDefinitionStore: assetDefinitionStore, - tokensDataStore: tokensDataStore, - sessionsProvider: sessionsProvider) + self.fetchTokenScriptFiles = fetchTokenScriptFiles } public func tokens(for servers: [RPCServer]) async -> [Token] { diff --git a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/TokensProcessingPipeline.swift b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/TokensProcessingPipeline.swift index e919f49fa..170d5a15d 100644 --- a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/TokensProcessingPipeline.swift +++ b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/TokensProcessingPipeline.swift @@ -57,11 +57,20 @@ public final class WalletDataProcessingPipeline: TokensProcessingPipeline { } } + let whenAttestationXMLChanged = assetDefinitionStore.attestationXMLChange + .receive(on: queue) + .flatMap { [tokensService] _ in + asFuture { + await tokensService.tokens + } + } + let whenTokensHasChanged = tokensService.tokensPublisher .dropFirst() .receive(on: queue) - let whenCollectionHasChanged = Publishers.Merge4(whenTokensHasChanged, whenTickersChanged, whenSignatureOrBodyChanged, whenCurrencyChanged) + let whenCollectionHasChanged = Publishers.Merge5(whenTokensHasChanged, whenTickersChanged, whenSignatureOrBodyChanged, whenAttestationXMLChanged, whenCurrencyChanged) + //TODO: attestations+TokenScript to verify attestationXMLChange triggers reloading .map { $0.map { TokenViewModel(token: $0) } } .flatMapLatest { tokenViewModels in asFuture { await self.applyTickers(tokens: tokenViewModels) ?? [] } } .flatMap { self.applyTokenScriptOverrides(tokens: $0) } diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionStore.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionStore.swift index 3fb5176c7..825df6811 100644 --- a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionStore.swift +++ b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionStore.swift @@ -34,10 +34,14 @@ public class AssetDefinitionStore: NSObject { private var lastContractInPasteboard: String? private var backingStore: AssetDefinitionBackingStore + private var tokenScriptForAttestationStore = TokenScriptForAttestationStore() + //TODO: attestations+TokenScript rename to be for tokens (only)? private let xmlHandlers: AtomicDictionary = .init() + private let xmlHandlersForAttestations: AtomicDictionary = .init() private let baseXmlHandlers: AtomicDictionary = .init() private var signatureChangeSubject: PassthroughSubject = .init() private var bodyChangeSubject: PassthroughSubject = .init() + private var attestationXmlChangeSubject: PassthroughSubject = .init() private var listOfBadTokenScriptFilesSubject: CurrentValueSubject<[TokenScriptFileIndices.FileName], Never> = .init([]) private let networking: AssetDefinitionNetworking private let tokenScriptStatusResolver: TokenScriptStatusResolver @@ -65,6 +69,10 @@ public class AssetDefinitionStore: NSObject { bodyChangeSubject.eraseToAnyPublisher() } + public var attestationXMLChange: AnyPublisher { + attestationXmlChangeSubject.eraseToAnyPublisher() + } + public var assetsSignatureOrBodyChange: AnyPublisher { return Publishers .Merge(signatureChange, bodyChange) @@ -173,6 +181,48 @@ public class AssetDefinitionStore: NSObject { } } + public func fetchXMLForAttestation(withScriptURL url: URL) async { + let request = AssetDefinitionNetworking.GetXmlFileRequest(url: url.rewrittenIfIpfs, lastModifiedDate: nil) + + return await withCheckedContinuation { continuation in + networking.fetchXml(request: request) + .sinkAsync(receiveCompletion: { _ in + //no-op + }, receiveValue: { [weak self] response in + guard let strongSelf = self else { + continuation.resume(returning: ()) + return + } + + switch response { + case .error: + continuation.resume(returning: ()) + return + case .unmodified: + continuation.resume(returning: ()) + return + case .xml(let xml): + //Note that Alamofire converts the 304 to a 200 if caching is enabled (which it is, by default). So we'll never get a 304 here. Checking against Charles proxy will show that a 304 is indeed returned by the server with an empty body. So we compare the contents instead. https://github.com/Alamofire/Alamofire/issues/615 + //TODO: attestations+TokenScript to implement persistance for attestations' TokenScript files + if xml == strongSelf.tokenScriptForAttestationStore[url] { + continuation.resume(returning: ()) + return + } else if functional.isTruncatedXML(xml: xml) { + continuation.resume(returning: ()) + return + } else { + strongSelf.tokenScriptForAttestationStore[url] = xml + //TODO: attestations+TokenScript do we enforce they must be IPFS before downloading? + //We do not invalidate (by removing the downloaded XML for the URL) like we do for TokenScript for tokens because the contents are on iPFS and thus immutable + strongSelf.triggerAttestationXMLChangedSubscribers(forURL: url) + + continuation.resume(returning: ()) + } + } + }) + } + } + private func fetchXML(contract: AlphaWallet.Address, server: RPCServer?, url: URL, useCacheAndFetch: Bool = false, completionHandler: ((Result) -> Void)? = nil) { let lastModified = lastModifiedDateOfCachedAssetDefinitionFile(forContract: contract) let request = AssetDefinitionNetworking.GetXmlFileRequest(url: url, lastModifiedDate: lastModified) @@ -211,6 +261,10 @@ public class AssetDefinitionStore: NSObject { bodyChangeSubject.send(contract) } + private func triggerAttestationXMLChangedSubscribers(forURL url: URL) { + attestationXmlChangeSubject.send(url) + } + private func triggerSignatureChangedSubscribers(forContract contract: AlphaWallet.Address) { signatureChangeSubject.send(contract) } @@ -254,7 +308,7 @@ public class AssetDefinitionStore: NSObject { } public subscript(url: URL) -> String? { - return tokenScriptForAttestationStore [url] + return tokenScriptForAttestationStore[url] } } @@ -280,6 +334,14 @@ extension AssetDefinitionStore: AssetDefinitionStoreProtocol { xmlHandlers[key] = xmlHandler } + public func getXmlHandler(forAttestationAtURL url: URL) -> PrivateXMLHandler? { + return xmlHandlersForAttestations[url] + } + + public func set(xmlHandler: PrivateXMLHandler?, forAttestationAtURL url: URL) { + xmlHandlersForAttestations[url] = xmlHandler + } + public func getBaseXmlHandler(for key: String) -> PrivateXMLHandler? { baseXmlHandlers[key] } @@ -297,6 +359,11 @@ extension AssetDefinitionStore: TokenScriptStatusResolver { public func computeTokenScriptStatus(forContract contract: AlphaWallet.Address, xmlString: String, isOfficial: Bool) -> Promise { tokenScriptStatusResolver.computeTokenScriptStatus(forContract: contract, xmlString: xmlString, isOfficial: isOfficial) } + + public func computeTokenScriptStatus(forAttestationURL url: URL, xmlString: String) -> Promise { + //TODO: attestations+TokenScript to implement computeTokenScriptStatus + return Promise { _ in } + } } public final class InMemoryTokenScriptFilesProvider: BaseTokenScriptFilesProvider { @@ -367,4 +434,4 @@ fileprivate extension AssetDefinitionStore.functional { enum SessionError: Error { case sessionNotFound -} +} \ No newline at end of file diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/TokenScriptForAttestationStore.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/TokenScriptForAttestationStore.swift new file mode 100644 index 000000000..4365a20d0 --- /dev/null +++ b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/TokenScriptForAttestationStore.swift @@ -0,0 +1,13 @@ +// Copyright © 2023 Stormbird PTE. LTD. + +import Foundation + +class TokenScriptForAttestationStore { + //TODO improve storage when we know more about how the TokenScript store for attestations is used + private var storage: [URL: String] = [:] + + subscript(url: URL) -> String? { + get { return storage[url] } + set { storage[url] = newValue } + } +} diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/TokenScriptSignatureVerifier.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/TokenScriptSignatureVerifier.swift index 6efb6e118..4e090eb23 100644 --- a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/TokenScriptSignatureVerifier.swift +++ b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/TokenScriptSignatureVerifier.swift @@ -70,6 +70,11 @@ public class BaseTokenScriptStatusResolver: TokenScriptStatusResolver { } } + public func computeTokenScriptStatus(forAttestationURL url: URL, xmlString: String) -> Promise { + //TODO: attestations+TokenScript to implement computeTokenScriptStatus + return Promise { _ in } + } + private func verificationType(forXml xmlString: String) -> PromiseKit.Promise { if let cachedVerificationType = backingStore.getCacheTokenScriptSignatureVerificationType(forXmlString: xmlString) { return .value(cachedVerificationType) diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/AssetDefinitionStoreProtocol.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/AssetDefinitionStoreProtocol.swift index 478ae11dd..d55a05676 100644 --- a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/AssetDefinitionStoreProtocol.swift +++ b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/AssetDefinitionStoreProtocol.swift @@ -6,10 +6,13 @@ public protocol AssetDefinitionStoreProtocol: TokenScriptStatusResolver { var features: TokenScriptFeatures { get } subscript(contract: AlphaWallet.Address) -> String? { get } + subscript(url: URL) -> String? { get } func isOfficial(contract: AlphaWallet.Address) -> Bool func isCanonicalized(contract: AlphaWallet.Address) -> Bool func getXmlHandler(for key: AlphaWallet.Address) -> PrivateXMLHandler? func set(xmlHandler: PrivateXMLHandler?, for key: AlphaWallet.Address) + func getXmlHandler(forAttestationAtURL url: URL) -> PrivateXMLHandler? + func set(xmlHandler: PrivateXMLHandler?, forAttestationAtURL url: URL) func getBaseXmlHandler(for key: String) -> PrivateXMLHandler? func setBaseXmlHandler(for key: String, baseXmlHandler: PrivateXMLHandler?) func baseTokenScriptFile(for tokenType: TokenType) -> XMLFile? diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/TokenScriptStatusResolver.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/TokenScriptStatusResolver.swift index 4144d0130..4a0c997f8 100644 --- a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/TokenScriptStatusResolver.swift +++ b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/TokenScriptStatusResolver.swift @@ -5,4 +5,5 @@ import PromiseKit public protocol TokenScriptStatusResolver { func computeTokenScriptStatus(forContract contract: AlphaWallet.Address, xmlString: String, isOfficial: Bool) -> Promise + func computeTokenScriptStatus(forAttestationURL url: URL, xmlString: String) -> Promise } diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/XMLHandler.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/XMLHandler.swift index 5a3210568..42ac5b323 100644 --- a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/XMLHandler.swift +++ b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/XMLHandler.swift @@ -111,6 +111,29 @@ public enum TokenLevelTokenScriptDisplayStatus { // Interface to extract data from non fungible token // swiftlint:disable type_body_length public class PrivateXMLHandler { + enum Target { + case token(AlphaWallet.Address) + //TODO: attestations+TokenScript to implement. Is the key the script's URL? + case attestation(URL) + + var isFifaTicketContract: Bool { + switch self { + case .token(let contractAddress): + return contractAddress.isFifaTicketContract + case .attestation: + return false + } + } + var isUEFATicketContract: Bool { + switch self { + case .token(let contractAddress): + return contractAddress.isUEFATicketContract + case .attestation: + return false + } + } + } + private static let emptyXMLString = "" private static let emptyXML = try! Kanna.XML(xml: emptyXMLString, encoding: .utf8) fileprivate static let tokenScriptNamespace = TokenScript.supportedTokenScriptNamespace @@ -120,7 +143,7 @@ public class PrivateXMLHandler { private let signatureNamespacePrefix = "ds:" private let xhtmlNamespacePrefix = "xhtml:" private let xmlContext = PrivateXMLHandler.createXmlContext(withLang: PrivateXMLHandler.lang) - private let contractAddress: AlphaWallet.Address + private let target: Target var server: RPCServerOrAny? //Explicit type so that the variable autocompletes with AppCode private lazy var selections = extractSelectionsForToken() @@ -235,7 +258,13 @@ public class PrivateXMLHandler { let selection = XMLHandler.getExcludeSelectionId(fromActionElement: actionElement, xmlContext: xmlContext).flatMap { id in self.selections.first { $0.id == id } } - results.append(.init(type: .tokenScript(contract: contractAddress, title: name, viewHtml: (html: html, style: style), attributes: attributes, transactionFunction: functionOrigin, selection: selection))) + switch target { + case .token(let contractAddress): + results.append(.init(type: .tokenScript(contract: contractAddress, title: name, viewHtml: (html: html, style: style), attributes: attributes, transactionFunction: functionOrigin, selection: selection))) + case .attestation: + //TODO: attestations+TokenScript to implement support for `actions + break + } } } if fromActionAsTopLevel.isEmpty { @@ -277,7 +306,13 @@ public class PrivateXMLHandler { let addressElements = XMLHandler.getAddressElements(fromContractElement: eventSourceContractElement, xmlContext: xmlContext) optionalContract = addressElements.first?.text.flatMap({ AlphaWallet.Address(string: $0.trimmed) }) } else { - optionalContract = contractAddress + switch target { + case .token(let contractAddress): + optionalContract = contractAddress + case .attestation: + //TODO: attestations+TokenScript to implement support for `activityCards` + optionalContract = nil + } } guard let contract = optionalContract, let origin = Origin(forEthereumEventElement: ethereumEventElement, asnModuleNamedTypeElement: asnModuleNamedElement, contract: contract, xmlContext: xmlContext) else { return nil } switch origin { @@ -319,10 +354,14 @@ public class PrivateXMLHandler { }() private lazy var _labelInSingularForm: String? = { - if contractAddress.sameContract(as: Constants.katContractAddress) { - return Constants.katNameFallback + switch target { + case .token(let contractAddress): + if contractAddress.sameContract(as: Constants.katContractAddress) { + return Constants.katNameFallback + } + case .attestation: + break } - if let labelStringElement = XMLHandler.getLabelStringElement(fromElement: tokenElement, xmlContext: xmlContext), let label = labelStringElement.text { return label } else { @@ -341,11 +380,15 @@ public class PrivateXMLHandler { lazy var labelInPluralForm: String? = { var labelInPluralForm: String? threadSafe.performSync { - if contractAddress.sameContract(as: Constants.katContractAddress) { - labelInPluralForm = Constants.katNameFallback - return + switch target { + case .token(let contractAddress): + if contractAddress.sameContract(as: Constants.katContractAddress) { + labelInPluralForm = Constants.katNameFallback + return + } + case .attestation: + break } - if let nameElement = XMLHandler.getLabelElementForPluralForm(fromElement: tokenElement, xmlContext: xmlContext), let name = nameElement.text { labelInPluralForm = name } else { @@ -383,7 +426,7 @@ public class PrivateXMLHandler { private init(contract: AlphaWallet.Address, xmlString: String?, baseTokenType: TokenType?, isOfficial: Bool, isCanonicalized: Bool, assetDefinitionStore: AssetDefinitionStoreProtocol) { let xmlString = xmlString ?? "" - self.contractAddress = contract + self.target = Target.token(contract) self.isOfficial = isOfficial self.isCanonicalized = isCanonicalized self.baseTokenType = baseTokenType @@ -441,6 +484,58 @@ public class PrivateXMLHandler { self.server = _server } + private init(forAttestationURL url: URL, xmlString: String?, assetDefinitionStore: AssetDefinitionStoreProtocol) { + let xmlString = xmlString ?? "" + self.target = Target.attestation(url) + self.isOfficial = false + self.isCanonicalized = false + self.baseTokenType = nil + self.features = assetDefinitionStore.features + + var _xml: XMLDocument! + var _tokenScriptStatus: Promise! + var _hasValidTokenScriptFile: Bool! + var _server: RPCServerOrAny? + let _xmlContext = xmlContext + let features = self.features + + threadSafe.performSync { + //We still compute the TokenScript status even if xmlString is empty because it might be considered empty because there's a conflict + let tokenScriptStatusPromise = assetDefinitionStore.computeTokenScriptStatus(forAttestationURL: url, xmlString: xmlString) + _tokenScriptStatus = tokenScriptStatusPromise + if let tokenScriptStatus = tokenScriptStatusPromise.value { + let (xml, hasValidTokenScriptFile) = PrivateXMLHandler.storeXmlAccordingToTokenScriptStatus(xmlString: xmlString, tokenScriptStatus: tokenScriptStatus, features: features) + _xml = xml + _hasValidTokenScriptFile = hasValidTokenScriptFile + //TODO: attestations+TokenScript no specific server in TokenScript for attestation right? + _server = .any + } else { + _xml = (try? Kanna.XML(xml: xmlString, encoding: .utf8)) ?? PrivateXMLHandler.emptyXML + _hasValidTokenScriptFile = true + //TODO: attestations+TokenScript is there a specific server for attestation's TokenScript? + _server = .any + tokenScriptStatusPromise.done { tokenScriptStatus in + let (xml, hasValidTokenScriptFile) = PrivateXMLHandler.storeXmlAccordingToTokenScriptStatus(xmlString: xmlString, tokenScriptStatus: tokenScriptStatus, features: features) + _xml = xml + _hasValidTokenScriptFile = hasValidTokenScriptFile + //TODO: attestations+TokenScript no specific server in TokenScript for attestation right? + _server = .any + //TODO: attestations+TokenScript is there a need to invalidate the signature status here? + }.cauterize() + } + } + + self.xml = _xml + self.tokenScriptStatus = _tokenScriptStatus + self.hasValidTokenScriptFile = _hasValidTokenScriptFile! + self.server = _server + } + + convenience init(forAttestationURL url: URL, assetDefinitionStore: AssetDefinitionStoreProtocol) { + let xmlString = assetDefinitionStore[url] + self.init(forAttestationURL: url, xmlString: xmlString, assetDefinitionStore: assetDefinitionStore) + } + private func extractHtml(fromViewElement element: XMLElement) -> (html: String, style: String) { let (style: style, script: script, body: body) = XMLHandler.getTokenScriptTokenViewContents(fromViewElement: element, xmlContext: xmlContext, xhtmlNamespacePrefix: xhtmlNamespacePrefix) let sanitizedHtml = sanitize(html: body) @@ -546,13 +641,13 @@ public class PrivateXMLHandler { case .erc20: actions = [.erc20Send, .erc20Receive] case .erc721: - if contractAddress.isUEFATicketContract { + if target.isUEFATicketContract { actions = [.nftRedeem, .nonFungibleTransfer] } else { actions = [.nonFungibleTransfer] } case .erc875: - if contractAddress.isFifaTicketContract { + if target.isFifaTicketContract { actions = [.nftRedeem, .nftSell, .nonFungibleTransfer] } else { actions = [.nftSell, .nonFungibleTransfer] @@ -569,13 +664,13 @@ public class PrivateXMLHandler { case .erc20, .nativeCryptocurrency: actions = [.erc20Send, .erc20Receive] case .erc721, .erc721ForTickets: - if contractAddress.isUEFATicketContract { + if target.isUEFATicketContract { actions = [.nftRedeem, .nonFungibleTransfer] } else { actions = [.nonFungibleTransfer] } case .erc875: - if contractAddress.isFifaTicketContract { + if target.isFifaTicketContract { actions = [.nftRedeem, .nftSell, .nonFungibleTransfer] } else { actions = [.nftSell, .nonFungibleTransfer] @@ -635,14 +730,24 @@ public class PrivateXMLHandler { } private func extractFields(fromElementContainingAttributes element: XMLElement) -> [AttributeId: AssetAttribute] { - var fields = [AttributeId: AssetAttribute]() - for each in XMLHandler.getAttributeElements(fromAttributeElement: element, xmlContext: xmlContext) { - guard let name = each["name"] else { continue } - //TODO we pass in server because we are assuming the server used for non-token-holding contracts are the same as the token-holding contract for now. Not always true. We'll have to fix it in the future when TokenScript supports it - guard let attribute = server.flatMap({ AssetAttribute(attribute: each, xmlContext: xmlContext, root: xml, tokenContract: contractAddress, server: $0, contractNamesAndAddresses: contractNamesAndAddresses) }) else { continue } - fields[name] = attribute + switch target { + case .token(let contractAddress): + var fields = [AttributeId: AssetAttribute]() + for each in XMLHandler.getAttributeElements(fromAttributeElement: element, xmlContext: xmlContext) { + guard let name = each["name"] else { continue } + //TODO we pass in server because we are assuming the server used for non-token-holding contracts are the same as the token-holding contract for now. Not always true. We'll have to fix it in the future when TokenScript supports it + guard let attribute = server.flatMap({ AssetAttribute(attribute: each, xmlContext: xmlContext, root: xml, tokenContract: contractAddress, server: $0, contractNamesAndAddresses: contractNamesAndAddresses) }) else { continue } + fields[name] = attribute + } + return fields + case .attestation: + //TODO: attestations+TokenScript to implement support for extractFields + var fields = [AttributeId: AssetAttribute]() + for each in XMLHandler.getAttributeElements(fromAttributeElement: element, xmlContext: xmlContext) { + guard let name = each["name"] else { continue } + } + return fields } - return fields } //TODOis it still necessary to sanitize? Maybe we still need to strip a, button, html? @@ -912,6 +1017,47 @@ public struct XMLHandler { self.init(contract: contract, optionalTokenType: tokenType, assetDefinitionStore: assetDefinitionStore) } + public init(forAttestationURL url: URL, assetDefinitionStore: AssetDefinitionStoreProtocol) { + let features = assetDefinitionStore.features + var privateXMLHandler: PrivateXMLHandler + var baseXMLHandler: PrivateXMLHandler? + if let handler = assetDefinitionStore.getXmlHandler(forAttestationAtURL: url) { + privateXMLHandler = handler + } else { + privateXMLHandler = PrivateXMLHandler(forAttestationURL: url, assetDefinitionStore: assetDefinitionStore) + assetDefinitionStore.set(xmlHandler: privateXMLHandler, forAttestationAtURL: url) + } + + //TODO: attestations+TokenScript tokenType not used? Relevant? + let tokenType: TokenType? = nil + if features.isActivityEnabled, let tokenType = tokenType { + //let tokenTypeForBaseXml: TokenType + //if privateXMLHandler.hasValidTokenScriptFile, let tokenTypeInXml = privateXMLHandler.tokenType.flatMap({ TokenType(tokenInterfaceType: $0) }) { + // tokenTypeForBaseXml = tokenTypeInXml + //} else { + // tokenTypeForBaseXml = tokenType + //} + + ////Key cannot be just `contract`, because the type can change (from the overriding TokenScript file) + //let key = "\(contract.eip55String)-\(tokenTypeForBaseXml.rawValue)" + //if let handler = assetDefinitionStore.getBaseXmlHandler(for: key) { + // baseXMLHandler = handler + //} else { + // if let xml = assetDefinitionStore.baseTokenScriptFile(for: tokenTypeForBaseXml) { + // baseXMLHandler = PrivateXMLHandler(contract: contract, baseXml: xml, baseTokenType: tokenTypeForBaseXml, assetDefinitionStore: assetDefinitionStore) + // assetDefinitionStore.setBaseXmlHandler(for: key, baseXmlHandler: baseXMLHandler) + // } else { + // baseXMLHandler = nil + // } + //} + } else { + baseXMLHandler = nil + } + + self.baseXMLHandler = baseXMLHandler + self.privateXMLHandler = privateXMLHandler + } + //private because we don't want client code creating XMLHandler(s) to be able to accidentally pass in a nil TokenType private init(contract: AlphaWallet.Address, optionalTokenType tokenType: TokenType?, assetDefinitionStore: AssetDefinitionStoreProtocol) { let features = assetDefinitionStore.features