diff --git a/AlphaWallet.xcodeproj/project.pbxproj b/AlphaWallet.xcodeproj/project.pbxproj index 9e566e5fc..6706c4c15 100644 --- a/AlphaWallet.xcodeproj/project.pbxproj +++ b/AlphaWallet.xcodeproj/project.pbxproj @@ -184,6 +184,7 @@ 5E7C718043636901114BF76C /* LocalesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7FB99843529061368DA1 /* LocalesViewModel.swift */; }; 5E7C71967C34DD3F207F8126 /* WhatsNewExperimentCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7709286B5D5A37D0156B /* WhatsNewExperimentCoordinator.swift */; }; 5E7C71D1D16FE09032EB4B7E /* TokenObjectTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C702A684DF27DC8ED4E42 /* TokenObjectTest.swift */; }; + 5E7C71D28882F455D2AE62B5 /* AttestationTypeValuePairToJavaScriptConvertor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7240B2AC0FE9E752B516 /* AttestationTypeValuePairToJavaScriptConvertor.swift */; }; 5E7C71DAA5DAFF764F92587D /* SetTransferTokensCardExpiryDateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C727433F7B8E322B3C68A /* SetTransferTokensCardExpiryDateViewController.swift */; }; 5E7C71DC13B2040F5408BF3C /* ImportMagicTokenCardRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C781F82F9E4903C460E33 /* ImportMagicTokenCardRowViewModel.swift */; }; 5E7C71F8050CCF990539B293 /* LockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C79D674D45A07E694CE31 /* LockView.swift */; }; @@ -1127,6 +1128,7 @@ 5E7C71E355BD14E975AF7491 /* TokensDataStoreTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokensDataStoreTest.swift; sourceTree = ""; }; 5E7C7228C9BEB801D4CD34DE /* EtherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EtherTests.swift; sourceTree = ""; }; 5E7C723C21F6376387AD1DCE /* PromptBackupWalletAfterWalletCreationViewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PromptBackupWalletAfterWalletCreationViewViewModel.swift; sourceTree = ""; }; + 5E7C7240B2AC0FE9E752B516 /* AttestationTypeValuePairToJavaScriptConvertor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttestationTypeValuePairToJavaScriptConvertor.swift; sourceTree = ""; }; 5E7C72571AB0FECB26FEB1B1 /* ClearDappBrowserCacheCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClearDappBrowserCacheCoordinator.swift; sourceTree = ""; }; 5E7C727433F7B8E322B3C68A /* SetTransferTokensCardExpiryDateViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetTransferTokensCardExpiryDateViewController.swift; sourceTree = ""; }; 5E7C728B3CA6A429AB5EE5DF /* ContainerViewWithShadow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContainerViewWithShadow.swift; sourceTree = ""; }; @@ -3126,6 +3128,7 @@ 5E7C7878E9E27F0CA05DFF03 /* Coordinators */, 5E7C78D36531AF80D3BAC20E /* ViewModels */, 5E7C7FD2D3F1340BD277AF86 /* FetchTokenScriptFiles.swift */, + 5E7C7240B2AC0FE9E752B516 /* AttestationTypeValuePairToJavaScriptConvertor.swift */, ); path = TokenScript; sourceTree = ""; @@ -5927,6 +5930,7 @@ 5E7C7C7C7B62A23FA7C3669B /* FetchTokenScriptFiles.swift in Sources */, 5E7C72CFCF7C74331AF8B0A4 /* SmartLayerPass.swift in Sources */, 5E7C700996081C138CB69DD3 /* Attestation+Extensions.swift in Sources */, + 5E7C71D28882F455D2AE62B5 /* AttestationTypeValuePairToJavaScriptConvertor.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/AlphaWallet/ActiveWalletCoordinator.swift b/AlphaWallet/ActiveWalletCoordinator.swift index 5170edb63..5d1af8899 100644 --- a/AlphaWallet/ActiveWalletCoordinator.swift +++ b/AlphaWallet/ActiveWalletCoordinator.swift @@ -547,14 +547,26 @@ class ActiveWalletCoordinator: NSObject, Coordinator { } } - private func importAttestation(_ attestation: Attestation, intoWallet address: AlphaWallet.Address) -> Bool { + private func importAttestation(_ attestation: Attestation, intoWallet address: AlphaWallet.Address) async -> Bool { + //TODO not right since the yet-to-be-found-attestation to be replaced might not use the same TokenScript file and might not use the same identifying fields. Probably keep it this way for now. But we can fix this by running through the XMLHandler for each attestation and fetching the correct fields + let collectionIdFieldNames: [String] + let identifyingFieldNames: [String] + await assetDefinitionStore.fetchXMLForAttestationIfScriptURL(attestation) + + if let xmlHandler = assetDefinitionStore.xmlHandler(forAttestation: attestation) { + collectionIdFieldNames = xmlHandler.computeCollectionIdFieldNames(forAttestation: attestation) + identifyingFieldNames = xmlHandler.computeAttestationIdentifyingFieldNames(forAttestation: attestation) + } else { + collectionIdFieldNames = [] + identifyingFieldNames = [] + } + //We allow importing an attestation into a wallet (as long as the attestation receiver logic allows it) even if the wallet is not active - let isSuccessful = attestationsStore.addAttestation(attestation, forWallet: address) + let isSuccessful = await attestationsStore.addAttestation(attestation, forWallet: address, collectionIdFieldNames: collectionIdFieldNames, identifyingFieldNames: identifyingFieldNames) if isSuccessful { SmartLayerPass().handleAddedAttestation(attestation, attestationStore: attestationsStore) ensureServerEnabled(attestation.server) //TODO shouldn't switch tabs if imported to a wallet that is different from active wallet. Just let user know - //TODO: attestations+TokenScript to implement reload like when we download TokenScript files at launch DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in self?.showTab(.tokens) self?.tokensCoordinator?.rootViewController.selectTab(withFilter: .attestations) @@ -1067,22 +1079,22 @@ extension ActiveWalletCoordinator: TokensCoordinatorDelegate { restartUI(withReason: .walletChange, account: account) } - func importAttestation(_ attestation: Attestation) -> Bool { + func importAttestation(_ attestation: Attestation) async -> Bool { if let recipient = attestation.recipient { if recipient.isNull { infoLog("Attestation: \(attestation) for wallet: \(String(describing: attestation.recipient)) recipient is null address. Importing…") - return importAttestation(attestation, intoWallet: wallet.address) + return await importAttestation(attestation, intoWallet: wallet.address) } else if recipient == wallet.address { infoLog("Attestation: \(attestation) for wallet: \(String(describing: attestation.recipient)) recipient matches current wallet. Importing…") - return importAttestation(attestation, intoWallet: wallet.address) + return await importAttestation(attestation, intoWallet: wallet.address) } else if keystore.wallets.contains(where: { $0.address == recipient }) { infoLog("Attestation: \(attestation) for wallet: \(String(describing: attestation.recipient)) recipient matches inactive wallet. Importing…") //TODO have a better UX, show user that it's imported, but to another wallet? - return importAttestation(attestation, intoWallet: recipient) + return await importAttestation(attestation, intoWallet: recipient) } else { if config.development.shouldIgnoreAttestationRecipientAndImportToCurrentWallet { infoLog("Attestation: \(attestation) for wallet: \(String(describing: attestation.recipient)) recipient doesn't match wallet. Importing because overridden by development flag…") - return importAttestation(attestation, intoWallet: wallet.address) + return await importAttestation(attestation, intoWallet: wallet.address) } else { infoLog("Attestation: \(attestation) for wallet: \(String(describing: attestation.recipient)) recipient doesn't match wallet. Skip import") return false @@ -1090,7 +1102,7 @@ extension ActiveWalletCoordinator: TokensCoordinatorDelegate { } } else { infoLog("Attestation: \(attestation) for wallet: \(String(describing: attestation.recipient)) recipient is nil. Importing…") - return importAttestation(attestation, intoWallet: wallet.address) + return await importAttestation(attestation, intoWallet: wallet.address) } } } diff --git a/AlphaWallet/AppCoordinator.swift b/AlphaWallet/AppCoordinator.swift index ad29d1ae2..643da0a81 100644 --- a/AlphaWallet/AppCoordinator.swift +++ b/AlphaWallet/AppCoordinator.swift @@ -271,7 +271,7 @@ class AppCoordinator: NSObject, Coordinator, ApplicationNavigatable { func importAttestation(url: URL) { Task { if let attestation = try? await Attestation.extract(fromUrlString: url.absoluteString) { - _ = activeWalletCoordinator?.importAttestation(attestation) + _ = await activeWalletCoordinator?.importAttestation(attestation) } } } diff --git a/AlphaWallet/Common/Services/Application.swift b/AlphaWallet/Common/Services/Application.swift index 6c33b1717..19dd52024 100644 --- a/AlphaWallet/Common/Services/Application.swift +++ b/AlphaWallet/Common/Services/Application.swift @@ -136,7 +136,9 @@ class Application: WalletDependenciesProvidable { let tokenScriptFeatures = TokenScriptFeatures() Self.copyFeatures(Features.current, toTokenScriptFeatures: tokenScriptFeatures) self.tokenScriptFeatures = tokenScriptFeatures - self.assetDefinitionStore = AssetDefinitionStore(baseTokenScriptFiles: TokenScript.baseTokenScriptFiles, networkService: networkService, blockchainsProvider: blockchainsProvider, features: tokenScriptFeatures) + self.assetDefinitionStore = AssetDefinitionStore(baseTokenScriptFiles: TokenScript.baseTokenScriptFiles, networkService: networkService, blockchainsProvider: blockchainsProvider, features: tokenScriptFeatures, resetFolders: !config.haveMergedAttestationAndTokenTokenScriptFoldersV1) + var config = config + config.haveMergedAttestationAndTokenTokenScriptFoldersV1 = true self.coinTickers = CoinTickers( transporter: BaseApiTransporter(), @@ -433,6 +435,7 @@ class Application: WalletDependenciesProvidable { sessionsProvider.start() let fetchTokenScriptFiles = FetchTokenScriptFilesImpl( + wallet: wallet, assetDefinitionStore: assetDefinitionStore, tokensDataStore: tokensDataStore, sessionsProvider: sessionsProvider) diff --git a/AlphaWallet/Common/Views/ViewModels/TokenCardRowViewModel.swift b/AlphaWallet/Common/Views/ViewModels/TokenCardRowViewModel.swift index 1370bd746..c62392835 100644 --- a/AlphaWallet/Common/Views/ViewModels/TokenCardRowViewModel.swift +++ b/AlphaWallet/Common/Views/ViewModels/TokenCardRowViewModel.swift @@ -109,7 +109,7 @@ struct TokenCardRowViewModel: TokenCardRowViewModelProtocol { } var tokenScriptHtml: String { - let xmlHandler = XMLHandler(contract: tokenHolder.contractAddress, tokenType: tokenHolder.tokenType, assetDefinitionStore: assetDefinitionStore) + let xmlHandler = assetDefinitionStore.xmlHandler(forContract: tokenHolder.contractAddress, tokenType: tokenHolder.tokenType) let html: String let style: String switch tokenView { diff --git a/AlphaWallet/Market/ImportMagicLinkController.swift b/AlphaWallet/Market/ImportMagicLinkController.swift index 12f7cc039..198e7a81f 100644 --- a/AlphaWallet/Market/ImportMagicLinkController.swift +++ b/AlphaWallet/Market/ImportMagicLinkController.swift @@ -313,7 +313,7 @@ final class ImportMagicLinkController { } if let existingToken = await tokensService.tokenViewModel(for: contractAddress, server: server) { - let name = XMLHandler(token: existingToken, assetDefinitionStore: assetDefinitionStore).getLabel(fallback: existingToken.name) + let name = assetDefinitionStore.xmlHandler(forTokenScriptSupportable: existingToken).getLabel(fallback: existingToken.name) await makeTokenHolder(name: name, symbol: existingToken.symbol) } else { let localizedTokenTypeName = R.string.localizable.tokensTitlecase() @@ -343,19 +343,11 @@ final class ImportMagicLinkController { } guard let tokenType = tokenType1 else { return } var tokens = [TokenScript.Token]() - let xmlHandler = XMLHandler(contract: contractAddress, tokenType: tokenType, assetDefinitionStore: assetDefinitionStore) + let xmlHandler = assetDefinitionStore.xmlHandler(forContract: contractAddress, tokenType: tokenType) for i in 0.. + + diff --git a/AlphaWallet/Sell/ViewControllers/EnterSellTokensCardPriceQuantityViewController.swift b/AlphaWallet/Sell/ViewControllers/EnterSellTokensCardPriceQuantityViewController.swift index be6168cc6..3c58f072f 100644 --- a/AlphaWallet/Sell/ViewControllers/EnterSellTokensCardPriceQuantityViewController.swift +++ b/AlphaWallet/Sell/ViewControllers/EnterSellTokensCardPriceQuantityViewController.swift @@ -261,7 +261,7 @@ class EnterSellTokensCardPriceQuantityViewController: UIViewController, TokenVer @objc private func nextButtonTapped() { guard quantityStepper.value > 0 else { - let tokenTypeName = XMLHandler(token: viewModel.token, assetDefinitionStore: assetDefinitionStore).getNameInPluralForm() + let tokenTypeName = assetDefinitionStore.xmlHandler(forTokenScriptSupportable: viewModel.token).getNameInPluralForm() UIAlertController.alert(title: "", message: R.string.localizable.aWalletTokenSellSelectTokenQuantityAtLeastOneTitle(tokenTypeName), alertButtonTitles: [R.string.localizable.oK()], @@ -283,7 +283,7 @@ class EnterSellTokensCardPriceQuantityViewController: UIViewController, TokenVer } guard !noPrice else { - let tokenTypeName = XMLHandler(token: viewModel.token, assetDefinitionStore: assetDefinitionStore).getNameInPluralForm() + let tokenTypeName = assetDefinitionStore.xmlHandler(forTokenScriptSupportable: viewModel.token).getNameInPluralForm() UIAlertController.alert(title: "", message: R.string.localizable.aWalletTokenSellPriceProvideTitle(tokenTypeName), alertButtonTitles: [R.string.localizable.oK()], @@ -367,7 +367,7 @@ extension EnterSellTokensCardPriceQuantityViewController: AmountTextFieldDelegat func changeType(in textField: AmountTextField) { updateTotalCostsLabels() } - + func doneButtonTapped(for textField: AmountTextField) { view.endEditing(true) } diff --git a/AlphaWallet/Sell/ViewModels/EnterSellTokensCardPriceQuantityViewModel.swift b/AlphaWallet/Sell/ViewModels/EnterSellTokensCardPriceQuantityViewModel.swift index d6acd8a56..29369c138 100644 --- a/AlphaWallet/Sell/ViewModels/EnterSellTokensCardPriceQuantityViewModel.swift +++ b/AlphaWallet/Sell/ViewModels/EnterSellTokensCardPriceQuantityViewModel.swift @@ -25,12 +25,12 @@ struct EnterSellTokensCardPriceQuantityViewModel { } var quantityLabelText: String { - let tokenTypeName = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore).getNameInPluralForm() + let tokenTypeName = assetDefinitionStore.xmlHandler(forTokenScriptSupportable: token).getNameInPluralForm() return R.string.localizable.aWalletTokenSellQuantityTitle(tokenTypeName.localizedUppercase) } var pricePerTokenLabelText: String { - let tokenTypeName = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore).getLabel() + let tokenTypeName = assetDefinitionStore.xmlHandler(forTokenScriptSupportable: token).getLabel() return R.string.localizable.aWalletTokenSellPricePerTokenTitle(tokenTypeName.localizedUppercase) } diff --git a/AlphaWallet/Sell/ViewModels/GenerateTransferMagicLinkViewModel.swift b/AlphaWallet/Sell/ViewModels/GenerateTransferMagicLinkViewModel.swift index 3f10b1adf..4ded724e7 100644 --- a/AlphaWallet/Sell/ViewModels/GenerateTransferMagicLinkViewModel.swift +++ b/AlphaWallet/Sell/ViewModels/GenerateTransferMagicLinkViewModel.swift @@ -66,10 +66,10 @@ struct GenerateTransferMagicLinkViewModel { var tokenCountLabelText: String { if magicLinkData.count == 1 { - let tokenTypeName = XMLHandler(contract: magicLinkData.contractAddress, tokenType: magicLinkData.tokenType, assetDefinitionStore: assetDefinitionStore).getLabel() + let tokenTypeName = assetDefinitionStore.xmlHandler(forContract: magicLinkData.contractAddress, tokenType: magicLinkData.tokenType).getLabel() return R.string.localizable.aWalletTokenSellConfirmSingleTokenSelectedTitle(tokenTypeName) } else { - let tokenTypeName = XMLHandler(contract: magicLinkData.contractAddress, tokenType: magicLinkData.tokenType, assetDefinitionStore: assetDefinitionStore).getNameInPluralForm() + let tokenTypeName = assetDefinitionStore.xmlHandler(forContract: magicLinkData.contractAddress, tokenType: magicLinkData.tokenType).getNameInPluralForm() return R.string.localizable.aWalletTokenSellConfirmMultipleTokenSelectedTitle(magicLinkData.count, tokenTypeName) } } diff --git a/AlphaWallet/TokenScript/AttestationTypeValuePairToJavaScriptConvertor.swift b/AlphaWallet/TokenScript/AttestationTypeValuePairToJavaScriptConvertor.swift new file mode 100644 index 000000000..919023b54 --- /dev/null +++ b/AlphaWallet/TokenScript/AttestationTypeValuePairToJavaScriptConvertor.swift @@ -0,0 +1,48 @@ +// Copyright © 2023 Stormbird PTE. LTD. + +import Foundation +import AlphaWalletAddress +import AlphaWalletAttestation + +struct AttestationTypeValuePairToJavaScriptConvertor { + init() {} + //Numbers must be formatted as string (or maybe later a suitable JavaScript big number type), but not as numbers in JavaScript because they can lose precision + func formatAsTokenScriptJavaScript(value: Attestation.TypeValuePair) -> String? { + switch value.value { + case .string(let string): + let string = string.replacingOccurrences(of: "\"", with: "\\\"") + if string.contains("\n") { + //Multiple line JavaScript literals must be quoted with `` instead of single or double quotes + return "`\(string.replacingOccurrences(of: "`", with: "\\`"))`" + } else { + return "\"\(string)\"" + } + case .int(let int): + return String(int) + case .uint(let uint): + return String(uint) + case .address(let address): + return "\"\(address.eip55String)\"" + case .bool(let bool): + return bool ? "true" : "false" + case .bytes(let bytes): + return "\"\(bytes.hexEncoded)\"" + } + } + + static func formatAsTokenScriptJavaScriptGeneralisedTime(date: Date?) -> String { + if let date { + return "\(GeneralisedTime(date: date).formatAsTokenScriptJavaScript)" + } else { + return "null" + } + } + + static func formatAsTokenScriptJavaScriptAddress(address: AlphaWallet.Address?) -> String { + if let address { + return "\"\(address.eip55String)\"" + } else { + return "null" + } + } +} diff --git a/AlphaWallet/TokenScript/FetchTokenScriptFiles.swift b/AlphaWallet/TokenScript/FetchTokenScriptFiles.swift index 23008bff8..91fd1e1d6 100644 --- a/AlphaWallet/TokenScript/FetchTokenScriptFiles.swift +++ b/AlphaWallet/TokenScript/FetchTokenScriptFiles.swift @@ -7,16 +7,15 @@ import AlphaWalletFoundation import AlphaWalletTokenScript public class FetchTokenScriptFilesImpl: FetchTokenScriptFiles { + private let wallet: Wallet 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) { - + public init(wallet: Wallet, assetDefinitionStore: AssetDefinitionStore, tokensDataStore: TokensDataStore, sessionsProvider: SessionsProvider) { + self.wallet = wallet self.assetDefinitionStore = assetDefinitionStore self.tokensDataStore = tokensDataStore self.sessionsProvider = sessionsProvider @@ -49,21 +48,15 @@ public class FetchTokenScriptFilesImpl: FetchTokenScriptFiles { } }.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) + assetDefinitionStore.fetchXMLs(forContractsAndServers: contractsInDatabase) }.store(in: &cancellable) } private func fetchForAttestations() { - let attestations = AttestationsStore.allAttestations() + let attestations = AttestationsStore(wallet: wallet.address).attestations for each in attestations { - if let url = each.scriptUri { - Task { @MainActor in - await assetDefinitionStore.fetchXMLForAttestation(withScriptURL: url) - //TODO: attestations+TokenScript to implement - } + Task { @MainActor in + await assetDefinitionStore.fetchXMLForAttestationIfScriptURL(each) } } } diff --git a/AlphaWallet/Tokens/Collectibles/ViewModels/NFTCollectionViewModel.swift b/AlphaWallet/Tokens/Collectibles/ViewModels/NFTCollectionViewModel.swift index e9b93aa91..d6fe7b803 100644 --- a/AlphaWallet/Tokens/Collectibles/ViewModels/NFTCollectionViewModel.swift +++ b/AlphaWallet/Tokens/Collectibles/ViewModels/NFTCollectionViewModel.swift @@ -28,7 +28,7 @@ final class NFTCollectionViewModel { private let tokensService: TokensProcessingPipeline private let nftProvider: NFTProvider private let config: Config - private (set) lazy var tokenScriptFileStatusHandler: XMLHandler = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore) + private (set) lazy var tokenScriptFileStatusHandler: XMLHandler = assetDefinitionStore.xmlHandler(forTokenScriptSupportable: token) private let tokenImageFetcher: TokenImageFetcher let activitiesService: ActivitiesServiceType diff --git a/AlphaWallet/Tokens/Coordinators/TokensCoordinator.swift b/AlphaWallet/Tokens/Coordinators/TokensCoordinator.swift index f53413606..a07f4ea43 100644 --- a/AlphaWallet/Tokens/Coordinators/TokensCoordinator.swift +++ b/AlphaWallet/Tokens/Coordinators/TokensCoordinator.swift @@ -22,7 +22,7 @@ protocol TokensCoordinatorDelegate: CanOpenURL, SendTransactionDelegate, BuyCryp func didSelectAccount(account: Wallet, in coordinator: TokensCoordinator) func viewWillAppearOnce(in coordinator: TokensCoordinator) - func importAttestation(_ attestation: Attestation) -> Bool + func importAttestation(_ attestation: Attestation) async -> Bool } class TokensCoordinator: Coordinator { @@ -243,8 +243,8 @@ class TokensCoordinator: Coordinator { } private func displayAttestation(_ attestation: Attestation) { - infoLog("[Attestation] Display attestation: \(attestation)") - let vc = AttestationViewController(attestation: attestation) + infoLog("[Attestation] Display attestation: \(attestation) scriptURI TokenScript file in (it might be overridden): \(String(describing: assetDefinitionStore.debugFilenameHoldingAttestationScriptUri(forAttestation: attestation)))") + let vc = AttestationViewController(attestation: attestation, wallet: wallet, assetDefinitionStore: assetDefinitionStore) vc.delegate = self vc.hidesBottomBarWhenPushed = true vc.navigationItem.largeTitleDisplayMode = .never @@ -252,7 +252,9 @@ class TokensCoordinator: Coordinator { } private func importAttestation(_ attestation: Attestation) { - _ = delegate?.importAttestation(attestation) + Task { @MainActor in + _ = await delegate?.importAttestation(attestation) + } } } diff --git a/AlphaWallet/Tokens/NftAssetDisplayHelper.swift b/AlphaWallet/Tokens/NftAssetDisplayHelper.swift index 8d3d69561..8686977de 100644 --- a/AlphaWallet/Tokens/NftAssetDisplayHelper.swift +++ b/AlphaWallet/Tokens/NftAssetDisplayHelper.swift @@ -320,7 +320,7 @@ extension NftAssetDisplayHelper.functional { tokenAttributeValues: AssetAttributeValues, assetDefinitionStore: AssetDefinitionStore) -> AnyPublisher<[OpenSeaNonFungibleTrait], Never> { - let xmlHandler = XMLHandler(contract: tokenHolder.contractAddress, tokenType: tokenHolder.tokens[0].tokenType, assetDefinitionStore: assetDefinitionStore) + let xmlHandler = assetDefinitionStore.xmlHandler(forContract: tokenHolder.contractAddress, tokenType: tokenHolder.tokenType) return tokenAttributeValues.resolveAllAttributes() .map { resolvedTokenAttributeNameValues in let tokenLevelAttributeIdsAndNames = xmlHandler.fieldIdsAndNamesExcludingBase diff --git a/AlphaWallet/Tokens/ViewControllers/AttestationViewController.swift b/AlphaWallet/Tokens/ViewControllers/AttestationViewController.swift index 2511440e7..cdf122b1e 100644 --- a/AlphaWallet/Tokens/ViewControllers/AttestationViewController.swift +++ b/AlphaWallet/Tokens/ViewControllers/AttestationViewController.swift @@ -1,8 +1,11 @@ // Copyright © 2023 Stormbird PTE. LTD. +import Combine import UIKit import AlphaWalletAttestation import AlphaWalletFoundation +import AlphaWalletTokenScript +import BigInt protocol AttestationViewControllerDelegate: AnyObject, CanOpenURL { } @@ -11,19 +14,61 @@ class AttestationViewController: UIViewController { private let containerView: ScrollableStackView = ScrollableStackView() private let attributesStackView = GridStackView(viewModel: .init(edgeInsets: .init(top: 0, left: 16, bottom: 15, right: 16))) private let attestation: Attestation + private let wallet: Wallet + private let assetDefinitionStore: AssetDefinitionStore + private var tokenScriptRendererView: TokenInstanceWebView? + private var cancelable = Set() weak var delegate: AttestationViewControllerDelegate? - init(attestation: Attestation) { + init(attestation: Attestation, wallet: Wallet, assetDefinitionStore: AssetDefinitionStore) { self.attestation = attestation - + self.wallet = wallet + self.assetDefinitionStore = assetDefinitionStore super.init(nibName: nil, bundle: nil) - title = attestation.name + configure() + subscribeForEthereumEventChanges() + } + + required init?(coder aDecoder: NSCoder) { + return nil + } + + override func viewWillDisappear(_ animated: Bool) { + if let tokenScriptRendererView { + tokenScriptRendererView.stopLoading() + } + } + + // swiftlint:disable function_body_length + private func configure() { + let xmlHandler = assetDefinitionStore.xmlHandler(forAttestation: attestation) + let tokenScriptViewHtml: String + let tokenScriptViewStyle: String + if let xmlHandler { + let (html, style) = xmlHandler.tokenViewHtml + if html.isEmpty { + tokenScriptRendererView?.removeFromSuperview() + tokenScriptRendererView = nil + tokenScriptViewHtml = "" + tokenScriptViewStyle = "" + } else { + tokenScriptRendererView = functional.createTokenScriptRendererView(attestation: attestation, wallet: wallet, assetDefinitionStore: assetDefinitionStore) + tokenScriptViewHtml = html + tokenScriptViewStyle = style + } + } else { + tokenScriptRendererView?.removeFromSuperview() + tokenScriptRendererView = nil + tokenScriptViewHtml = "" + tokenScriptViewStyle = "" + } + + title = xmlHandler?.getAttestationName() ?? attestation.name view.backgroundColor = Configuration.Color.Semantic.searchBarBackground var subviews: [UIView] = [] - let detailsHeader = TokenInfoHeaderView() detailsHeader.configure(viewModel: TokenInfoHeaderViewModel(title: R.string.localizable.semifungiblesDetails())) subviews.append(detailsHeader) @@ -35,50 +80,95 @@ class AttestationViewController: UIViewController { subviews.append(issuerAddressRow) issuerAddressRow.delegate = self + if let xmlHandler, let description = xmlHandler.getAttestationDescription() { + let descriptionRow = functional.createDetailRow(title: "", value: TokenAttributeViewModel.defaultValueAttributedString(description)) + subviews.append(descriptionRow) + } + let attributesHeader = TokenInfoHeaderView() attributesHeader.configure(viewModel: TokenInfoHeaderViewModel(title: R.string.localizable.attestationsAttributes())) subviews.append(attributesHeader) var attributeViews: [NonFungibleTraitView] = [] - for each in attestation.data { + let data: [Attestation.TypeValuePair] + let fieldsSpecificationFromTokenScript: Bool + if let xmlHandler { + let attributes = xmlHandler.resolveAttestationAttributes(forAttestation: attestation) + data = attributes + fieldsSpecificationFromTokenScript = true + } else { + data = attestation.data + fieldsSpecificationFromTokenScript = false + } + for each in data { let attributeView = functional.createAttributeView(name: each.type.name, value: each.value.stringValue) attributeViews.append(attributeView) } - let dateFormatter = Date.formatter(with: "dd MMM yyyy h:mm:ss a") - let validFromView = functional.createAttributeView(name: R.string.localizable.attestationsValidFrom(), value: dateFormatter.string(from: attestation.time)) - attributeViews.append(validFromView) - let expirationTimeString: String - if let expirationTime = attestation.expirationTime { - expirationTimeString = dateFormatter.string(from: expirationTime) - } else { - expirationTimeString = "—" + + if !fieldsSpecificationFromTokenScript { + let dateFormatter = Date.formatter(with: "dd MMM yyyy h:mm:ss a") + let validFromView = functional.createAttributeView(name: R.string.localizable.attestationsValidFrom(), value: dateFormatter.string(from: attestation.time)) + attributeViews.append(validFromView) + let expirationTimeString: String + if let expirationTime = attestation.expirationTime { + expirationTimeString = dateFormatter.string(from: expirationTime) + } else { + expirationTimeString = "—" + } + let validUntilView = functional.createAttributeView(name: R.string.localizable.attestationsValidUntil(), value: expirationTimeString) + attributeViews.append(validUntilView) } - let validUntilView = functional.createAttributeView(name: R.string.localizable.attestationsValidUntil(), value: expirationTimeString) - attributeViews.append(validUntilView) attributesStackView.set(subviews: attributeViews) subviews.append(attributesStackView) + containerView.stackView.removeAllArrangedSubviews() + //remove from superview to remove constraints on it. This is especially important when show the TokenScript view, then when the TokenScript is updated and the view needs to removed + containerView.removeFromSuperview() containerView.stackView.addArrangedSubviews(subviews) view.addSubview(containerView) - NSLayoutConstraint.activate([ - containerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + var constraints: [NSLayoutConstraint] = [ containerView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), containerView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), containerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), - ]) + ] + if let tokenScriptRendererView { + view.addSubview(tokenScriptRendererView) + + constraints.append(contentsOf: [ + containerView.topAnchor.constraint(equalTo: tokenScriptRendererView.bottomAnchor), + + tokenScriptRendererView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + tokenScriptRendererView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + tokenScriptRendererView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + ]) + + //The actual value doesn't matter as long as it's the same + let dummyId: BigUInt = 0 + let tokenScriptHtml = wrapWithHtmlViewport(html: tokenScriptViewHtml, style: tokenScriptViewStyle, forTokenId: dummyId) + tokenScriptRendererView.loadHtml(tokenScriptHtml) + tokenScriptRendererView.updateWithAttestation(attestation, withId: dummyId) + } else { + constraints.append(containerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)) + } + NSLayoutConstraint.activate(constraints) - showIssuerKeyVerificationButton() + showIssuerKeyVerificationButton(xmlHandler: xmlHandler) } + // swiftlint:enable function_body_length - required init?(coder aDecoder: NSCoder) { - return nil + private func subscribeForEthereumEventChanges() { + assetDefinitionStore.attestationXMLChange + .sink { [weak self] _ in + self?.configure() + }.store(in: &cancelable) } - private func showIssuerKeyVerificationButton() { - let issuerKeyVerificationButton = Self.functional.createIssuerKeyVerificationButton(isVerified: attestation.isValidAttestationIssuer) + private func showIssuerKeyVerificationButton(xmlHandler: XMLHandler?) { + let verificationStatus: AttestationVerificationStatus = computeVerificationStatus(forAttestation: attestation, xmlHandler: xmlHandler) + let issuerKeyVerificationButton = Self.functional.createIssuerKeyVerificationButton(verificationStatus: verificationStatus) self.navigationItem.rightBarButtonItem = UIBarButtonItem(customView: issuerKeyVerificationButton) } @@ -106,21 +196,27 @@ fileprivate extension AttestationViewController.functional { return view } - static func createIssuerKeyVerificationButton(isVerified: Bool) -> UIButton { + static func createIssuerKeyVerificationButton(verificationStatus: AttestationVerificationStatus) -> UIButton { let title: String let image: UIImage? let tintColor: UIColor let button = UIButton(type: .system) - if isVerified { + switch verificationStatus { + case .trustedIssuer: //TODO localize title = "Trusted issuer" image = R.image.verified() tintColor = Configuration.Color.Semantic.textFieldContrastText - } else { + case .untrustedIssuer: //TODO localize title = "Not trusted" image = R.image.unverified() tintColor = Configuration.Color.Semantic.defaultErrorText + case .tokenScriptHasMatchingIssuer: + //TODO localize + title = "" + image = nil + tintColor = Configuration.Color.Semantic.textFieldContrastText } button.setTitle(title, for: .normal) button.setImage(image?.withRenderingMode(.alwaysOriginal), for: .normal) @@ -132,6 +228,14 @@ fileprivate extension AttestationViewController.functional { button.titleEdgeInsets = .init(top: 0, left: 0, bottom: 0, right: -12) return button } + + static func createTokenScriptRendererView(attestation: Attestation, wallet: Wallet, assetDefinitionStore: AssetDefinitionStore) -> TokenInstanceWebView { + let webView = TokenInstanceWebView(server: attestation.server, wallet: wallet, assetDefinitionStore: assetDefinitionStore) + webView.translatesAutoresizingMaskIntoConstraints = false + //TODO implement delegate if we need to use it + //webView.delegate = self + return webView + } } extension AttestationViewController: TokenAttributeViewDelegate { diff --git a/AlphaWallet/Tokens/ViewControllers/AttestationsViewController.swift b/AlphaWallet/Tokens/ViewControllers/AttestationsViewController.swift index 1723ead57..1438a4e77 100644 --- a/AlphaWallet/Tokens/ViewControllers/AttestationsViewController.swift +++ b/AlphaWallet/Tokens/ViewControllers/AttestationsViewController.swift @@ -22,11 +22,13 @@ class AttestationsViewController: UIViewController { return tableView }() private let attestations: [Attestation] + private let assetDefinitionStore: AssetDefinitionStore weak var delegate: AttestationsViewControllerDelegate? - init(attestations: [Attestation]) { + init(attestations: [Attestation], assetDefinitionStore: AssetDefinitionStore) { self.attestations = attestations + self.assetDefinitionStore = assetDefinitionStore super.init(nibName: nil, bundle: nil) @@ -58,7 +60,7 @@ extension AttestationsViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let attestation = attestations[indexPath.row] let cell: AttestationViewCell = tableView.dequeueReusableCell(for: indexPath) - cell.configure(viewModel: AttestationViewCellViewModel(attestation: attestation)) + cell.configure(viewModel: AttestationViewCellViewModel(attestation: attestation, assetDefinitionStore: assetDefinitionStore)) return cell } } diff --git a/AlphaWallet/Tokens/ViewControllers/TokenInstanceActionViewController.swift b/AlphaWallet/Tokens/ViewControllers/TokenInstanceActionViewController.swift index 4fb0bf790..16d3584af 100644 --- a/AlphaWallet/Tokens/ViewControllers/TokenInstanceActionViewController.swift +++ b/AlphaWallet/Tokens/ViewControllers/TokenInstanceActionViewController.swift @@ -135,6 +135,7 @@ class TokenInstanceActionViewController: UIViewController, TokenVerifiableStatus //TODO this will only contain values that has been resolved and might not refresh properly when the values are 1st resolved or updated //TODO rename this. Not actually `existingAttributeValues`, but token attributes let existingAttributeValues = tokenHolder.values + //TODO why does this resolution not go through an XMLHandler? let cardLevelAttributeValues = assetDefinitionStore .assetAttributeResolver .resolve(withTokenIdOrEvent: tokenHolder.tokens[0].tokenIdOrEvent, diff --git a/AlphaWallet/Tokens/ViewControllers/VerifiableStatusViewController.swift b/AlphaWallet/Tokens/ViewControllers/VerifiableStatusViewController.swift index f0f8128fc..92f9f6724 100644 --- a/AlphaWallet/Tokens/ViewControllers/VerifiableStatusViewController.swift +++ b/AlphaWallet/Tokens/ViewControllers/VerifiableStatusViewController.swift @@ -147,7 +147,7 @@ protocol TokenVerifiableStatusViewController: VerifiableStatusViewController { extension TokenVerifiableStatusViewController { var tokenScriptFileStatus: Promise { - XMLHandler.functional.tokenScriptStatus(forContract: contract, assetDefinitionStore: assetDefinitionStore) + assetDefinitionStore.tokenScriptStatus(forContract: contract) } } @@ -160,6 +160,6 @@ protocol OptionalTokenVerifiableStatusViewController: VerifiableStatusViewContro extension OptionalTokenVerifiableStatusViewController { var tokenScriptFileStatus: Promise { guard let contract = contract else { return .value(.type0NoTokenScript) } - return XMLHandler.functional.tokenScriptStatus(forContract: contract, assetDefinitionStore: assetDefinitionStore) + return assetDefinitionStore.tokenScriptStatus(forContract: contract) } } diff --git a/AlphaWallet/Tokens/ViewModels/AttestationViewCellViewModel.swift b/AlphaWallet/Tokens/ViewModels/AttestationViewCellViewModel.swift index 3447f5448..35242d22d 100644 --- a/AlphaWallet/Tokens/ViewModels/AttestationViewCellViewModel.swift +++ b/AlphaWallet/Tokens/ViewModels/AttestationViewCellViewModel.swift @@ -6,24 +6,11 @@ import Combine struct AttestationViewCellViewModel { private let attestation: Attestation + private let assetDefinitionStore: AssetDefinitionStore - var titleAttributedString: NSAttributedString { - return NSAttributedString(string: attestation.name, attributes: [ - .foregroundColor: Configuration.Color.Semantic.defaultForegroundText, - .font: Screen.TokenCard.Font.title - ]) - } - - var detailsAttributedString: NSAttributedString { - var subtitle = "" - for each in attestation.data { - subtitle += "\(each.type.name): \(each.value.stringValue) " - } - return NSAttributedString(string: subtitle.trimmed, attributes: [ - .foregroundColor: Configuration.Color.Semantic.defaultSubtitleText, - .font: Screen.TokenCard.Font.subtitle - ]) - } + //Cannot be computed properties because we need to include it in the hash for NSDiffableDataSourceSnapshot's computation when we go from no-TokenScript -> TokenScript or if the TokenScript file changes(add/deleted) + let titleAttributedString: NSAttributedString + let detailsAttributedString: NSAttributedString var iconImage: TokenImagePublisher { switch attestation.attestationType { @@ -41,10 +28,44 @@ struct AttestationViewCellViewModel { let accessoryType: UITableViewCell.AccessoryType = .none - init(attestation: Attestation) { + init(attestation: Attestation, assetDefinitionStore: AssetDefinitionStore) { self.attestation = attestation + self.assetDefinitionStore = assetDefinitionStore + self.titleAttributedString = functional.computeTitleAttributedString(attestation: attestation, assetDefinitionStore: assetDefinitionStore) + self.detailsAttributedString = functional.computeDetailsAttributedString(attestation: attestation, assetDefinitionStore: assetDefinitionStore) } } extension AttestationViewCellViewModel: Hashable { } + +extension AttestationViewCellViewModel { + enum functional {} +} + +fileprivate extension AttestationViewCellViewModel.functional { + static func computeDetailsAttributedString(attestation: Attestation, assetDefinitionStore: AssetDefinitionStore) -> NSAttributedString { + let data: [Attestation.TypeValuePair] + if let xmlHandler = assetDefinitionStore.xmlHandler(forAttestation: attestation) { + data = xmlHandler.resolveAttestationAttributes(forAttestation: attestation) + } else { + data = attestation.data + } + var subtitle = "" + for each in data { + subtitle += "\(each.type.name): \(each.value.stringValue) " + } + return NSAttributedString(string: subtitle.trimmed, attributes: [ + .foregroundColor: Configuration.Color.Semantic.defaultSubtitleText, + .font: Screen.TokenCard.Font.subtitle + ]) + } + + static func computeTitleAttributedString(attestation: Attestation, assetDefinitionStore: AssetDefinitionStore) -> NSAttributedString { + let name = assetDefinitionStore.xmlHandler(forAttestation: attestation)?.getAttestationName() ?? attestation.name + return NSAttributedString(string: name, attributes: [ + .foregroundColor: Configuration.Color.Semantic.defaultForegroundText, + .font: Screen.TokenCard.Font.title + ]) + } +} \ No newline at end of file diff --git a/AlphaWallet/Tokens/ViewModels/FungibleTokenTabViewModel.swift b/AlphaWallet/Tokens/ViewModels/FungibleTokenTabViewModel.swift index eea31d8ac..794cfdbd4 100644 --- a/AlphaWallet/Tokens/ViewModels/FungibleTokenTabViewModel.swift +++ b/AlphaWallet/Tokens/ViewModels/FungibleTokenTabViewModel.swift @@ -23,7 +23,7 @@ class FungibleTokenTabViewModel { private let assetDefinitionStore: AssetDefinitionStore private var cancelable = Set() private let tokensService: TokensService - lazy var tokenScriptFileStatusHandler = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore) + lazy var tokenScriptFileStatusHandler = assetDefinitionStore.xmlHandler(forTokenScriptSupportable: token) let session: WalletSession let tabBarItems: [TabBarItem] diff --git a/AlphaWallet/Tokens/ViewModels/TokenCardWebViewModel.swift b/AlphaWallet/Tokens/ViewModels/TokenCardWebViewModel.swift index ba7c957a3..6b9407364 100644 --- a/AlphaWallet/Tokens/ViewModels/TokenCardWebViewModel.swift +++ b/AlphaWallet/Tokens/ViewModels/TokenCardWebViewModel.swift @@ -17,7 +17,7 @@ struct TokenCardWebViewModel { var contentsBackgroundColor: UIColor = Configuration.Color.Semantic.defaultViewBackground var tokenScriptHtml: String { - let xmlHandler = XMLHandler(contract: tokenHolder.contractAddress, tokenType: tokenHolder.tokenType, assetDefinitionStore: assetDefinitionStore) + let xmlHandler = assetDefinitionStore.xmlHandler(forContract: tokenHolder.contractAddress, tokenType: tokenHolder.tokenType) let html: String let style: String switch tokenView { diff --git a/AlphaWallet/Tokens/ViewModels/TokensViewModel.swift b/AlphaWallet/Tokens/ViewModels/TokensViewModel.swift index e8969c68a..5235f01e1 100644 --- a/AlphaWallet/Tokens/ViewModels/TokensViewModel.swift +++ b/AlphaWallet/Tokens/ViewModels/TokensViewModel.swift @@ -178,7 +178,7 @@ final class TokensViewModel { let titleWithListOfBadTokenScriptFiles = Publishers.CombineLatest(title, assetDefinitionStore.listOfBadTokenScriptFiles) let firstNonZeroTokens = sectionViewModelsSubject.filter { - let (sectionsViewModels, _, _) = functional.generateDisplayState(tokens: self.tokens, attestations: self.attestations, tokensFilter: self.tokensFilter, filterInUserInterface: self.filterInUserInterface, walletConnectSessions: self.walletConnectSessions, isSearchActive: self.isSearchActive, tokenImageFetcher: self.tokenImageFetcher) + let (sectionsViewModels, _, _) = functional.generateDisplayState(tokens: self.tokens, attestations: self.attestations, tokensFilter: self.tokensFilter, filterInUserInterface: self.filterInUserInterface, walletConnectSessions: self.walletConnectSessions, isSearchActive: self.isSearchActive, tokenImageFetcher: self.tokenImageFetcher, assetDefinitionStore: self.assetDefinitionStore) //We could simplify the above code to just generate and the list of tokens for `.tokens` and perform less work, but it's not called often, so lets keep it simple if let s = sectionsViewModels.first(where: { $0.section == .tokens }) { return !s.views.isEmpty @@ -196,7 +196,7 @@ final class TokensViewModel { //Prevent crash, keeping updates serialized so receiving end can update with atomic state .receive(on: RunLoop.main) .map { _, summary, blockiesImage, data -> TokensViewModel.ViewState in - let (sectionsViewModels, filteredTokens, sections) = functional.generateDisplayState(tokens: self.tokens, attestations: self.attestations, tokensFilter: self.tokensFilter, filterInUserInterface: self.filterInUserInterface, walletConnectSessions: self.walletConnectSessions, isSearchActive: self.isSearchActive, tokenImageFetcher: self.tokenImageFetcher) + let (sectionsViewModels, filteredTokens, sections) = functional.generateDisplayState(tokens: self.tokens, attestations: self.attestations, tokensFilter: self.tokensFilter, filterInUserInterface: self.filterInUserInterface, walletConnectSessions: self.walletConnectSessions, isSearchActive: self.isSearchActive, tokenImageFetcher: self.tokenImageFetcher, assetDefinitionStore: self.assetDefinitionStore) let isConsoleButtonHidden = data.1.isEmpty return TokensViewModel.ViewState( title: data.0, @@ -758,7 +758,7 @@ fileprivate extension TokensViewModel.functional { } } - static func buildSectionViewModels(sections: [TokensViewModel.Section], filteredTokens: [TokensViewModel.TokenOrRpcServer], collectiblePairs: [TokensViewModel.CollectiblePairs], filter: WalletFilter, tokenImageFetcher: TokenImageFetcher) -> [TokensViewModel.SectionViewModel] { + static func buildSectionViewModels(sections: [TokensViewModel.Section], filteredTokens: [TokensViewModel.TokenOrRpcServer], collectiblePairs: [TokensViewModel.CollectiblePairs], filter: WalletFilter, tokenImageFetcher: TokenImageFetcher, assetDefinitionStore: AssetDefinitionStore) -> [TokensViewModel.SectionViewModel] { return sections.enumerated().map { (sectionIndex, section) -> TokensViewModel.SectionViewModel in let numberOfItems = numberOfItems(for: sectionIndex, sections: sections, filteredTokens: filteredTokens, collectiblePairs: collectiblePairs, filter: filter) guard numberOfItems > 0 else { @@ -767,7 +767,7 @@ fileprivate extension TokensViewModel.functional { let viewModels = (0 ..< numberOfItems).map { row -> TokensViewModel.ViewModelType in let indexPath = IndexPath(row: row, section: sectionIndex) - return TokensViewModel.functional.viewModel(for: indexPath, sections: sections, filteredTokens: filteredTokens, collectiblePairs: collectiblePairs, tokenImageFetcher: tokenImageFetcher) + return TokensViewModel.functional.viewModel(for: indexPath, sections: sections, filteredTokens: filteredTokens, collectiblePairs: collectiblePairs, tokenImageFetcher: tokenImageFetcher, assetDefinitionStore: assetDefinitionStore) } return TokensViewModel.SectionViewModel(section: section, views: viewModels) @@ -788,7 +788,7 @@ fileprivate extension TokensViewModel.functional { } } - static func viewModel(for indexPath: IndexPath, sections: [TokensViewModel.Section], filteredTokens: [TokensViewModel.TokenOrRpcServer], collectiblePairs: [TokensViewModel.CollectiblePairs], tokenImageFetcher: TokenImageFetcher) -> TokensViewModel.ViewModelType { + static func viewModel(for indexPath: IndexPath, sections: [TokensViewModel.Section], filteredTokens: [TokensViewModel.TokenOrRpcServer], collectiblePairs: [TokensViewModel.CollectiblePairs], tokenImageFetcher: TokenImageFetcher, assetDefinitionStore: AssetDefinitionStore) -> TokensViewModel.ViewModelType { switch sections[indexPath.section] { case .search, .walletSummary, .filters, .activeWalletSession: return .undefined @@ -814,7 +814,7 @@ fileprivate extension TokensViewModel.functional { return .nonFungible(viewModel) } case .attestation(let attestation): - return .attestation(AttestationViewCellViewModel(attestation: attestation)) + return .attestation(AttestationViewCellViewModel(attestation: attestation, assetDefinitionStore: assetDefinitionStore)) } case .collectiblePairs: let pair = collectiblePairs[indexPath.row] @@ -841,10 +841,10 @@ fileprivate extension TokensViewModel.functional { } } - static func generateDisplayState(tokens: [TokenViewModel], attestations: [Attestation], tokensFilter: TokensFilter, filterInUserInterface: WalletFilter, walletConnectSessions: Int, isSearchActive: Bool, tokenImageFetcher: TokenImageFetcher) -> ([TokensViewModel.SectionViewModel], [TokensViewModel.TokenOrRpcServer], [TokensViewModel.Section]) { + static func generateDisplayState(tokens: [TokenViewModel], attestations: [Attestation], tokensFilter: TokensFilter, filterInUserInterface: WalletFilter, walletConnectSessions: Int, isSearchActive: Bool, tokenImageFetcher: TokenImageFetcher, assetDefinitionStore: AssetDefinitionStore) -> ([TokensViewModel.SectionViewModel], [TokensViewModel.TokenOrRpcServer], [TokensViewModel.Section]) { let filteredTokens = filteredAndSortedTokens(tokens: tokens, attestations: attestations, tokensFilter: tokensFilter, filter: filterInUserInterface) let sections = refreshSections(walletConnectSessions: walletConnectSessions, isSearchActive: isSearchActive, filter: filterInUserInterface) - let sectionsViewModels = buildSectionViewModels(sections: sections, filteredTokens: filteredTokens, collectiblePairs: collectiblePairs(filteredTokens: filteredTokens), filter: filterInUserInterface, tokenImageFetcher: tokenImageFetcher) + let sectionsViewModels = buildSectionViewModels(sections: sections, filteredTokens: filteredTokens, collectiblePairs: collectiblePairs(filteredTokens: filteredTokens), filter: filterInUserInterface, tokenImageFetcher: tokenImageFetcher, assetDefinitionStore: assetDefinitionStore) return (sectionsViewModels, filteredTokens, sections) } } diff --git a/AlphaWallet/Tokens/Views/TokenInstanceWebView.swift b/AlphaWallet/Tokens/Views/TokenInstanceWebView.swift index 8836ed58f..e1e8866b2 100644 --- a/AlphaWallet/Tokens/Views/TokenInstanceWebView.swift +++ b/AlphaWallet/Tokens/Views/TokenInstanceWebView.swift @@ -4,8 +4,11 @@ import Foundation import UIKit import WebKit import Combine -import AlphaWalletFoundation +import AlphaWalletABI +import AlphaWalletAttestation import AlphaWalletCore +import AlphaWalletFoundation +import AlphaWalletLogger import AlphaWalletTokenScript import BigInt import PromiseKit @@ -139,7 +142,7 @@ class TokenInstanceWebView: UIView, TokenScriptLocalRefsSource { } } - func update(withId id: BigUInt, resolvedTokenAttributeNameValues: [AttributeId: AssetInternalValue], resolvedCardAttributeNameValues: [AttributeId: AssetInternalValue], isFirstUpdate: Bool = true) { + func update(withId id: BigUInt, resolvedTokenAttributeNameValues: [AttributeId: AssetInternalValue], resolvedCardAttributeNameValues: [AttributeId: AssetInternalValue], attestation: Attestation? = nil, isFirstUpdate: Bool = true) { var tokenData = [AttributeId: String]() let convertor = AssetAttributeToJavaScriptConvertor() for (name, value) in resolvedTokenAttributeNameValues { @@ -158,11 +161,28 @@ class TokenInstanceWebView: UIView, TokenScriptLocalRefsSource { let cardDataString = cardData.map { name, value in "\(name): \(value)," }.joined() //TODO remove this soon since it's no longer in the JavaScript API let combinedData = tokenData.merging(cardData, uniquingKeysWith: { _, new in new }) - let combinedDataString = combinedData.map { name, value in "\(name): \(value)," }.joined() - var string = "\nweb3.tokens.data.currentInstance = {\n" - string += combinedDataString - string += "\n}" + var string: String + //TODO currently only a token or attestation can be exposed, mutually exclusively + if let attestation { + string = "\nweb3.tokens.data.currentInstance = \n" + string += """ + { + "chainId":\(attestation.chainId), + "ownerAddress":"\(wallet.address.eip55String)", + "rawAttestation":"\(functional.fixRawAttestationFormat(rawAttestation: Attestation.extractRawAttestation(fromUrlString: attestation.source) ?? attestation.source))", + "attestationSig":"\(attestation.signature.hexEncoded)", + "attestation":"\(attestation.abiEncoded.hexEncoded)", + "attest":\(attestation.messageJson) + } + """ + string += "\n" + } else { + let combinedDataString = combinedData.map { name, value in "\(name): \(value)," }.joined() + string = "\nweb3.tokens.data.currentInstance = {\n" + string += combinedDataString + string += "\n}" + } string += "\nweb3.tokens.data.token = {\n" string += tokenDataString @@ -204,9 +224,13 @@ class TokenInstanceWebView: UIView, TokenScriptLocalRefsSource { } } + func updateWithAttestation(_ attestation: Attestation, withId id: BigUInt, isFirstUpdate: Bool = true) { + update(withId: id, resolvedTokenAttributeNameValues: .init(), resolvedCardAttributeNameValues: .init(), attestation: attestation, isFirstUpdate: isFirstUpdate) + } + private func unresolvedAttributesDependentOnProps(tokenHolder: TokenHolder) -> [AttributeId: AssetAttributeSyntaxValue] { guard !localRefs.isEmpty else { return .init() } - let xmlHandler = XMLHandler(contract: tokenHolder.contractAddress, tokenType: tokenHolder.tokenType, assetDefinitionStore: assetDefinitionStore) + let xmlHandler = assetDefinitionStore.xmlHandler(forContract: tokenHolder.contractAddress, tokenType: tokenHolder.tokenType) let attributes = xmlHandler.fields.filter { $0.value.isDependentOnProps && lastCardLevelAttributeValues?[$0.key] == nil } return assetDefinitionStore @@ -230,7 +254,7 @@ class TokenInstanceWebView: UIView, TokenScriptLocalRefsSource { case .tokenId: results[each.javaScriptName] = .uint(tokenHolder.tokens[0].id) case .label: - let localizedNameFromAssetDefinition = XMLHandler(contract: tokenHolder.contractAddress, tokenType: tokenHolder.tokenType, assetDefinitionStore: assetDefinitionStore).getLabel(fallback: tokenHolder.name) + let localizedNameFromAssetDefinition = assetDefinitionStore.xmlHandler(forContract: tokenHolder.contractAddress, tokenType: tokenHolder.tokenType).getLabel(fallback: tokenHolder.name) results[each.javaScriptName] = .string(localizedNameFromAssetDefinition) case .symbol: results[each.javaScriptName] = .string(tokenHolder.symbol) @@ -308,7 +332,7 @@ extension TokenInstanceWebView: WKScriptMessageHandler { private func checkPropsNameClashErrorWithCardAttributes() -> String? { guard let lastTokenHolder = lastTokenHolder else { return nil } - let xmlHandler = XMLHandler(contract: lastTokenHolder.contractAddress, tokenType: lastTokenHolder.tokenType, assetDefinitionStore: assetDefinitionStore) + let xmlHandler = assetDefinitionStore.xmlHandler(forContract: lastTokenHolder.contractAddress, tokenType: lastTokenHolder.tokenType) let attributes = xmlHandler.fields let attributeIds: [AttributeId] if let lastCardLevelAttributeValues = lastCardLevelAttributeValues { @@ -420,3 +444,81 @@ extension TokenInstanceWebView { webView.evaluateJavaScript(script, completionHandler: nil) } } + +fileprivate extension Attestation { + var messageJson: String { + let result: String + if let messageVersion = easMessageVersion, messageVersion >= 1 { + return """ + { + "version": \(messageVersion), + "time": \(AttestationTypeValuePairToJavaScriptConvertor.formatAsTokenScriptJavaScriptGeneralisedTime(date: time)), + "data": \(TokenInstanceWebView.functional.convertAttestationDataToTokenScriptJson(data)), + "expirationTime": \(AttestationTypeValuePairToJavaScriptConvertor.formatAsTokenScriptJavaScriptGeneralisedTime(date: expirationTime)), + "recipient": \(AttestationTypeValuePairToJavaScriptConvertor.formatAsTokenScriptJavaScriptAddress(address: recipient)), + "refUID": "\(refUID)", + "revocable": \(revocable), + "schema": "\(schemaUid.value)" + } + """ + } else { + result = """ + { + "time": \(AttestationTypeValuePairToJavaScriptConvertor.formatAsTokenScriptJavaScriptGeneralisedTime(date: time)), + "data": \(TokenInstanceWebView.functional.convertAttestationDataToTokenScriptJson(data)), + "expirationTime": \(AttestationTypeValuePairToJavaScriptConvertor.formatAsTokenScriptJavaScriptGeneralisedTime(date: expirationTime)), + "recipient": \(AttestationTypeValuePairToJavaScriptConvertor.formatAsTokenScriptJavaScriptAddress(address: recipient)), + "refUID": "\(refUID)", + "revocable": \(revocable), + "schema": "\(schemaUid.value)" + } + """ + } + return result + } +} + +extension TokenInstanceWebView { + enum functional {} +} + +fileprivate extension TokenInstanceWebView.functional { + static func convertAttestationDataToTokenScriptJson(_ data: [Attestation.TypeValuePair]) -> String { + var result = [String: String]() + let convertor = AttestationTypeValuePairToJavaScriptConvertor() + for each in data { + if let value = convertor.formatAsTokenScriptJavaScript(value: each) { + result[each.type.name] = value + } + } + let resultString = result.map { name, value in "\"\(name)\": \(value)" }.joined(separator: ",") + return "{\n\(resultString)\n}\n" + } + + static func fixRawAttestationFormat(rawAttestation: String) -> String { + //Important to undo/substitute this as Smart Layer API might need it + return rawAttestation.replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "+", with: "-") + } +} + +//TODO remove AlphaWalletABI's dependency on TrustKeystore and then move this into Attestation (we can't do it now to avoid adding dependency to AlphaWalletAttestation on TrustKeystore) +fileprivate extension Attestation { + var abiEncoded: Data { + let encoder = ABIEncoder() + do { + try encoder.encode(tuple: [ + ABIValue.bytes(Data(hex: schemaUid.value)), + ABIValue.address2(recipient ?? AlphaWalletFoundation.Constants.nullAddress), + ABIValue.uint(bits: 256, BigUInt(easAttestationTime)), + ABIValue.uint(bits: 256, BigUInt(easAttestationExpirationTime)), + ABIValue.bool(revocable), + ABIValue.bytes(Data(hex: refUID)), + ABIValue.dynamicBytes(Data(hex: easAttestationData)), + ]) + return encoder.data + } catch { + infoLog("[Attestation] Failed to ABI-encode attestation: \(error)") + return Data() + } + } +} \ No newline at end of file diff --git a/AlphaWallet/Transfer/ViewControllers/TransferTokensCardQuantitySelectionViewController.swift b/AlphaWallet/Transfer/ViewControllers/TransferTokensCardQuantitySelectionViewController.swift index c9b8f36c0..f387ecbcf 100644 --- a/AlphaWallet/Transfer/ViewControllers/TransferTokensCardQuantitySelectionViewController.swift +++ b/AlphaWallet/Transfer/ViewControllers/TransferTokensCardQuantitySelectionViewController.swift @@ -47,7 +47,7 @@ class TransferTokensCardQuantitySelectionViewController: UIViewController, Token self.viewModel = viewModel self.assetDefinitionStore = assetDefinitionStore - + let tokenType = OpenSeaBackedNonFungibleTokenHandling(token: viewModel.token, assetDefinitionStore: assetDefinitionStore, tokenViewType: .viewIconified) switch tokenType { case .backedByOpenSea: @@ -106,7 +106,7 @@ class TransferTokensCardQuantitySelectionViewController: UIViewController, Token @objc private func nextButtonTapped() { if quantityStepper.value == 0 { - let tokenTypeName = XMLHandler(token: viewModel.token, assetDefinitionStore: assetDefinitionStore).getNameInPluralForm() + let tokenTypeName = assetDefinitionStore.xmlHandler(forTokenScriptSupportable: viewModel.token).getNameInPluralForm() UIAlertController.alert(title: "", message: R.string.localizable.aWalletTokenTransferSelectTokenQuantityAtLeastOneTitle(tokenTypeName), alertButtonTitles: [R.string.localizable.oK()], diff --git a/AlphaWallet/Transfer/ViewModels/SetTransferTokensCardExpiryDateViewControllerViewModel.swift b/AlphaWallet/Transfer/ViewModels/SetTransferTokensCardExpiryDateViewControllerViewModel.swift index 81ac8e85c..7cbf608aa 100644 --- a/AlphaWallet/Transfer/ViewModels/SetTransferTokensCardExpiryDateViewControllerViewModel.swift +++ b/AlphaWallet/Transfer/ViewModels/SetTransferTokensCardExpiryDateViewControllerViewModel.swift @@ -10,7 +10,7 @@ struct SetTransferTokensCardExpiryDateViewModel { let assetDefinitionStore: AssetDefinitionStore var headerTitle: String { - let tokenTypeName = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore).getNameInPluralForm() + let tokenTypeName = assetDefinitionStore.xmlHandler(forTokenScriptSupportable: token).getNameInPluralForm() return R.string.localizable.aWalletTokenTransferSelectQuantityTitle(tokenTypeName) } diff --git a/AlphaWallet/Transfer/ViewModels/SetTransferTokensCardExpiryDateViewModel.swift b/AlphaWallet/Transfer/ViewModels/SetTransferTokensCardExpiryDateViewModel.swift index 2ff303b72..7cbf608aa 100644 --- a/AlphaWallet/Transfer/ViewModels/SetTransferTokensCardExpiryDateViewModel.swift +++ b/AlphaWallet/Transfer/ViewModels/SetTransferTokensCardExpiryDateViewModel.swift @@ -8,9 +8,9 @@ struct SetTransferTokensCardExpiryDateViewModel { let token: Token let tokenHolder: TokenHolder let assetDefinitionStore: AssetDefinitionStore - + var headerTitle: String { - let tokenTypeName = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore).getNameInPluralForm() + let tokenTypeName = assetDefinitionStore.xmlHandler(forTokenScriptSupportable: token).getNameInPluralForm() return R.string.localizable.aWalletTokenTransferSelectQuantityTitle(tokenTypeName) } diff --git a/AlphaWallet/Transfer/ViewModels/TransferTokensCardQuantitySelectionViewModel.swift b/AlphaWallet/Transfer/ViewModels/TransferTokensCardQuantitySelectionViewModel.swift index b2ab9f2c7..ffb42266f 100644 --- a/AlphaWallet/Transfer/ViewModels/TransferTokensCardQuantitySelectionViewModel.swift +++ b/AlphaWallet/Transfer/ViewModels/TransferTokensCardQuantitySelectionViewModel.swift @@ -10,7 +10,7 @@ struct TransferTokensCardQuantitySelectionViewModel { let assetDefinitionStore: AssetDefinitionStore let session: WalletSession var headerTitle: String { - let tokenTypeName = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore).getNameInPluralForm() + let tokenTypeName = assetDefinitionStore.xmlHandler(forTokenScriptSupportable: token).getNameInPluralForm() return R.string.localizable.aWalletTokenTransferSelectQuantityTitle(tokenTypeName) } @@ -19,7 +19,7 @@ struct TransferTokensCardQuantitySelectionViewModel { } var subtitleText: String { - let tokenTypeName = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore).getNameInPluralForm() + let tokenTypeName = assetDefinitionStore.xmlHandler(forTokenScriptSupportable: token).getNameInPluralForm() return R.string.localizable.aWalletTokenTransferQuantityTitle(tokenTypeName.localizedUppercase) } } diff --git a/AlphaWalletTests/Coordinators/ActiveWalletViewTests.swift b/AlphaWalletTests/Coordinators/ActiveWalletViewTests.swift index 5c6bca643..ba0b8a092 100644 --- a/AlphaWalletTests/Coordinators/ActiveWalletViewTests.swift +++ b/AlphaWalletTests/Coordinators/ActiveWalletViewTests.swift @@ -80,7 +80,7 @@ extension AnyCAIP10AccountProvidable { extension AssetDefinitionStore { static func make() -> AssetDefinitionStore { - return .init(networkService: FakeNetworkService(), blockchainsProvider: BlockchainsProviderImplementation.make(servers: [.main]), features: TokenScriptFeatures()) + return .init(networkService: FakeNetworkService(), blockchainsProvider: BlockchainsProviderImplementation.make(servers: [.main]), features: TokenScriptFeatures(), resetFolders: false) } } diff --git a/AlphaWalletTests/Ens/EnsResolverTests.swift b/AlphaWalletTests/Ens/EnsResolverTests.swift index 6e8051664..cd00cd72a 100644 --- a/AlphaWalletTests/Ens/EnsResolverTests.swift +++ b/AlphaWalletTests/Ens/EnsResolverTests.swift @@ -43,6 +43,7 @@ class EnsResolverTests: XCTestCase { let address = try! await resolver.getENSAddressFromResolver(for: ensName) XCTAssertTrue(address.sameContract(as: "0xde0B295669a9FD93d5F28D9Ec85E40f4cb697BAe"), "ENS name did not resolve correctly") expectation.fulfill() + } await fulfillment(of: expectations, timeout: 20) } diff --git a/AlphaWalletTests/TokenScriptClient/AssetDefinitionDiskBackingStoreWithOverridesTests.swift b/AlphaWalletTests/TokenScriptClient/AssetDefinitionDiskBackingStoreWithOverridesTests.swift index b5be06aeb..0d10cb19b 100644 --- a/AlphaWalletTests/TokenScriptClient/AssetDefinitionDiskBackingStoreWithOverridesTests.swift +++ b/AlphaWalletTests/TokenScriptClient/AssetDefinitionDiskBackingStoreWithOverridesTests.swift @@ -9,12 +9,12 @@ import AlphaWalletTokenScript class AssetDefinitionDiskBackingStoreWithOverridesTests: XCTestCase { func testBackingStoreWithOverrides() { let overridesStore = AssetDefinitionInMemoryBackingStore() - let store = AssetDefinitionDiskBackingStoreWithOverrides(overridesStore: overridesStore) + let store = AssetDefinitionDiskBackingStoreWithOverrides(overridesStore: overridesStore, resetFolders: false) let address = AlphaWallet.Address.make() - XCTAssertNil(store[address]) - overridesStore[address] = "xml1" - XCTAssertEqual(store[address], "xml1") - overridesStore[address] = nil - XCTAssertNil(store[address]) + XCTAssertNil(store.getXml(byContract: address)) + overridesStore.storeOfficialXmlForToken(address, xml: "xml1", fromUrl: URL(string: "http://google.com")!) + XCTAssertEqual(store.getXml(byContract: address), "xml1") + overridesStore.deleteXmlFileDownloadedFromOfficialRepo(forContract: address) + XCTAssertNil(store.getXml(byContract: address)) } } diff --git a/AlphaWalletTests/TokenScriptClient/AssetDefinitionStoreTests.swift b/AlphaWalletTests/TokenScriptClient/AssetDefinitionStoreTests.swift index f02d90fcc..d7cd1416c 100644 --- a/AlphaWalletTests/TokenScriptClient/AssetDefinitionStoreTests.swift +++ b/AlphaWalletTests/TokenScriptClient/AssetDefinitionStoreTests.swift @@ -4,7 +4,7 @@ import Foundation import XCTest @testable import AlphaWallet import AlphaWalletFoundation -import AlphaWalletTokenScript +@testable import AlphaWalletTokenScript class AssetDefinitionStoreTests: XCTestCase { func testConvertsModifiedDateToStringForHTTPHeaderIfModifiedSince() { @@ -13,16 +13,17 @@ class AssetDefinitionStoreTests: XCTestCase { } func testXMLAccess() { - let store = AssetDefinitionStore(backingStore: AssetDefinitionInMemoryBackingStore(), networkService: FakeNetworkService(), blockchainsProvider: BlockchainsProviderImplementation.make(servers: [.main]), features: TokenScriptFeatures()) + let store = AssetDefinitionStore(backingStore: AssetDefinitionInMemoryBackingStore(), networkService: FakeNetworkService(), blockchainsProvider: BlockchainsProviderImplementation.make(servers: [.main]), features: TokenScriptFeatures(), resetFolders: false) let address = AlphaWallet.Address.make() - XCTAssertNil(store[address]) - store[address] = "xml1" - XCTAssertEqual(store[address], "xml1") + + XCTAssertNil(store.getXml(byContract: address)) + store.storeOfficialXmlForToken(address, xml: "xml1", fromUrl: URL(string: "http://google.com")!) + XCTAssertEqual(store.getXml(byContract: address), "xml1") } func testShouldNotCallCompletionBlockWithCacheCaseIfNotAlreadyCached() { let contractAddress = AlphaWallet.Address.make() - let store = AssetDefinitionStore(backingStore: AssetDefinitionInMemoryBackingStore(), networkService: FakeNetworkService(), blockchainsProvider: BlockchainsProviderImplementation.make(servers: [.main]), features: TokenScriptFeatures()) + let store = AssetDefinitionStore(backingStore: AssetDefinitionInMemoryBackingStore(), networkService: FakeNetworkService(), blockchainsProvider: BlockchainsProviderImplementation.make(servers: [.main]), features: TokenScriptFeatures(), resetFolders: false) let expectation = XCTestExpectation(description: "cached case should not be called") expectation.isInverted = true store.fetchXML(forContract: contractAddress, server: nil, useCacheAndFetch: true) { [weak self] result in @@ -39,8 +40,8 @@ class AssetDefinitionStoreTests: XCTestCase { func testShouldCallCompletionBlockWithCacheCaseIfAlreadyCached() { let contractAddress = AlphaWallet.Address.ethereumAddress(eip55String: "0x0000000000000000000000000000000000000001") - let store = AssetDefinitionStore(backingStore: AssetDefinitionInMemoryBackingStore(), networkService: FakeNetworkService(), blockchainsProvider: BlockchainsProviderImplementation.make(servers: [.main]), features: TokenScriptFeatures()) - store[contractAddress] = "something" + let store = AssetDefinitionStore(backingStore: AssetDefinitionInMemoryBackingStore(), networkService: FakeNetworkService(), blockchainsProvider: BlockchainsProviderImplementation.make(servers: [.main]), features: TokenScriptFeatures(), resetFolders: false) + store.storeOfficialXmlForToken(contractAddress, xml: "something", fromUrl: URL(string: "http://google.com")!) let expectation = XCTestExpectation(description: "cached case should be called") store.fetchXML(forContract: contractAddress, server: nil, useCacheAndFetch: true) { [weak self] result in guard self != nil else { return } diff --git a/AlphaWalletTests/TokenScriptClient/TokenScriptSignatureVerifierTest.swift b/AlphaWalletTests/TokenScriptClient/TokenScriptSignatureVerifierTest.swift index 4260cf20d..5f6bbefba 100644 --- a/AlphaWalletTests/TokenScriptClient/TokenScriptSignatureVerifierTest.swift +++ b/AlphaWalletTests/TokenScriptClient/TokenScriptSignatureVerifierTest.swift @@ -16,7 +16,7 @@ class TokenScriptSignatureVerifierTest: XCTestCase { } let expectation = XCTestExpectation(description: "validation passes") - let tokenScriptSignatureVerifier = TokenScriptSignatureVerifier(tokenScriptFilesProvider: InMemoryTokenScriptFilesProvider(), networkService: FakeNetworkService(), features: TokenScriptFeatures(), reachability: FakeReachabilityManager(false)) + let tokenScriptSignatureVerifier = TokenScriptSignatureVerifier(baseTokenScriptFiles: BaseTokenScriptFiles(baseTokenScriptFiles: [:]), networkService: FakeNetworkService(), features: TokenScriptFeatures(), reachability: FakeReachabilityManager(false)) // swiftlint:disable:next line_length let uniswapTokenTSML = "\r\n \r\n Uniswap SAI V1\r\n \r\n \r\n 0x09cabEC1eAd1c0Ba254B09efb3EE13841712bE14 \r\n \r\n\r\n \r\n \r\n \r\n \r\n\r\n \r\n \r\n \r\n About\r\n \r\n \r\n \r\n \r\n
\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\nutEDbVcQgJPdJl8Woj8cgjEzSgLGmq2OsooCQE+xgyE=\r\n\r\n\r\n\r\nZyfUnOJRpmDtEnBv5nhuGN9Os2uA8IRFa/onp5/tNkdaHaUSp6qOyGaO+p0q40axpCJYruDuSvz+\r\nxCzKYTaHG6WKa4o3c0GhcH2r1N4o1G4RJXXWGIBt6oZfgMs8dSstaZKU36EvBhXcPoBKnqrA+dUi\r\nFbAkyYZSjdH80JlqoCXEo+ltHgfCLPHpJzetb+wYv8xeGH9e5Wpw/n6lbSmjrtYfoy3Xdw20nEz0\r\nk01+RzdWnryyW4FBGYQqvyot0/qyn0isO6wFcXQDHcllKg53I84KWkVwhZSMIqsywKUovF22Jlsr\r\nZ67VawKBdnIPewvhiTXd/Kkxj4mO8C4BddB0NA==\r\n\r\n\r\nAlphaWallet\r\n\r\n\r\n\r\ntDHBH8jKLPEjpXsy/V9/XsXBYmc64SXx6IWG9CdJzm+iSrk3Od2ZZTS3DsR5+hp9hk6UwRyb1XKG\r\n+TMrBODfxIIWricmnhxYMqiyvwDhecm4RU4YFteekBFAsuhEGCJBtmJSrle5G3iE/9FwvTfw/cxo\r\nyAydv85OWc4UkxkfjzaXVqGGKCzSFhRm48HwG51/1nmC1mmPh070EMY4Km4N/ieJZ8egLjDAIZEI\r\nEY5Cj7ig9PPnGf2pF21/z7vm3zQViXi6XJIBn1E5CTXzDW1y1BYe0QI+dxxY0o+97mwisZu7fVfB\r\n/rJJm3g7Ye4/lITkZeRMP+OLYyG1pathItlrVQ==\r\n\r\nAQAB\r\n\r\n\r\n\r\n\r\nMIIFUDCCBDigAwIBAgISAy095xt55U0Cc0++zYon7TT0MA0GCSqGSIb3DQEBCwUAMEoxCzAJBgNV\r\nBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQDExpMZXQncyBFbmNyeXB0IEF1\r\ndGhvcml0eSBYMzAeFw0xOTA5MjQxMTA1MTdaFw0xOTEyMjMxMTA1MTdaMBMxETAPBgNVBAMMCCou\r\nYXcuYXBwMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtDHBH8jKLPEjpXsy/V9/XsXB\r\nYmc64SXx6IWG9CdJzm+iSrk3Od2ZZTS3DsR5+hp9hk6UwRyb1XKG+TMrBODfxIIWricmnhxYMqiy\r\nvwDhecm4RU4YFteekBFAsuhEGCJBtmJSrle5G3iE/9FwvTfw/cxoyAydv85OWc4UkxkfjzaXVqGG\r\nKCzSFhRm48HwG51/1nmC1mmPh070EMY4Km4N/ieJZ8egLjDAIZEIEY5Cj7ig9PPnGf2pF21/z7vm\r\n3zQViXi6XJIBn1E5CTXzDW1y1BYe0QI+dxxY0o+97mwisZu7fVfB/rJJm3g7Ye4/lITkZeRMP+OL\r\nYyG1pathItlrVQIDAQABo4ICZTCCAmEwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUF\r\nBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBRaKDKFkPztPgQee3418kofZddy\r\nMTAfBgNVHSMEGDAWgBSoSmpjBH3duubRObemRWXv86jsoTBvBggrBgEFBQcBAQRjMGEwLgYIKwYB\r\nBQUHMAGGImh0dHA6Ly9vY3NwLmludC14My5sZXRzZW5jcnlwdC5vcmcwLwYIKwYBBQUHMAKGI2h0\r\ndHA6Ly9jZXJ0LmludC14My5sZXRzZW5jcnlwdC5vcmcvMBsGA1UdEQQUMBKCCCouYXcuYXBwggZh\r\ndy5hcHAwTAYDVR0gBEUwQzAIBgZngQwBAgEwNwYLKwYBBAGC3xMBAQEwKDAmBggrBgEFBQcCARYa\r\naHR0cDovL2Nwcy5sZXRzZW5jcnlwdC5vcmcwggEEBgorBgEEAdZ5AgQCBIH1BIHyAPAAdwB0ftqD\r\nMa0zEJEhnM4lT0Jwwr/9XkIgCMY3NXnmEHvMVgAAAW1jKVxwAAAEAwBIMEYCIQCHuWOuAiqXC7Bn\r\nnYLS4BDrOrVeObYC7zcQvru7Aqx5DAIhAP9ukUp9nnQBWgAgTdfK+GdhHxboIaxJz7456ws6myft\r\nAHUAY/Lbzeg7zCzPC3KEJ1drM6SNYXePvXWmOLHHaFRL2I0AAAFtYyladwAABAMARjBEAiBaxydV\r\nJMY5/hQyrjaMMonJgQBhEKBHvv4FbthX+lZfpAIga4sB0hDaoT4knZfVhVP/u/Uv47t6z1+TEWnL\r\n/TresTwwDQYJKoZIhvcNAQELBQADggEBAGSPK0ivDprmvO72TVLZsuk/JDhCmXQcYe6cRGPiX7WL\r\nc0B6wfLaxb0rrQdmGpTiTEHS6wEa6tOMEsfxutPWeOxlqFPU97QHhLrdBlf4IDfk8i1Em3rpPPQu\r\n3M2u5nfeRXvsIxyB5vLQvuR/NwCQqA2bwkCrlLz7dr1iGem35DGI0ikIkdODTPI+RpwHK8b2iApA\r\nw6XaVGA99eCJS2dHqeyHPAc1Yf+Klv+z0FyM38ZUfazRRIQ17LtolM1U/9Ynld20SXtCrIFbcmbo\r\nw3piXuHLlDRcRxWqdL33yPoTEbPuLtS6vqDXefYP0RiYpQHHwJz4E6q5VCbK6LgILnIyX+M=\r\n\r\n\r\nMIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/MSQwIgYDVQQK\r\nExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4X\r\nDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0NlowSjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxl\r\ndCdzIEVuY3J5cHQxIzAhBgNVBAMTGkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkq\r\nhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4\r\nS0EFq6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8SMx+yk13\r\nEiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0Z8h/pZq4UmEUEz9l6YKH\r\ny9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWAa6xK8xuQSXgvopZPKiAlKQTGdMDQMc2P\r\nMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQAB\r\no4IBfTCCAXkwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEE\r\nczBxMDIGCCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNvbTA7\r\nBggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9kc3Ryb290Y2F4My5w\r\nN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAwVAYDVR0gBE0wSzAIBgZngQwBAgEw\r\nPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcCARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNy\r\neXB0Lm9yZzA8BgNVHR8ENTAzMDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9P\r\nVENBWDNDUkwuY3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF\r\nAAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJouM2VcGfl96S8\r\nTihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/wApIvJSwtmVi4MFU5aMqrSDE\r\n6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwuX4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPM\r\nTZ+sOPAveyxindmjkW8lGy+QsRlGPfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M\r\n+X+Q7UNKEkROb3N6KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==\r\n\r\n\r\n\r\n" tokenScriptSignatureVerifier.verifyXMLSignatureViaAPI(xml: uniswapTokenTSML) { result in @@ -37,7 +37,7 @@ class TokenScriptSignatureVerifierTest: XCTestCase { } let expectation = XCTestExpectation(description: "validation passes") - let tokenScriptSignatureVerifier = TokenScriptSignatureVerifier(tokenScriptFilesProvider: InMemoryTokenScriptFilesProvider(), networkService: FakeNetworkService(), features: TokenScriptFeatures(), reachability: FakeReachabilityManager(false)) + let tokenScriptSignatureVerifier = TokenScriptSignatureVerifier(baseTokenScriptFiles: BaseTokenScriptFiles(baseTokenScriptFiles: [:]), networkService: FakeNetworkService(), features: TokenScriptFeatures(), reachability: FakeReachabilityManager(false)) // swiftlint:disable:next line_length let entryTokenXMLSignedWithInvalidKey = "\n \n \n Ticket\n Tickets\n \n \n Boleto de admisión\n Boleto de admisiónes\n \n \n 入場券\n 入場券\n \n \n \n 0x63cCEF733a093E5Bd773b41C96D3eCE361464942\n 0xFB82A5a2922A249f32222316b9D1F5cbD3838678\n 0x7c81DF31BB2f54f03A56Ab25c952bF3Fa39bDF46\n 0x2B58A9403396463404c2e397DBF37c5EcCAb43e5\n \n \n \n \n \n \n \n \n \n \n Expired Ticket\n Expired Tickets\n \n 已经过期的票\n \n expired\n \n \n \n \n \n \n
\n \n

Enter Satoshi's villa with this special token!\n \"\"\n

\n
\n\n
\n \n \n\n
\n\n \n \n \n \n \n Enter\n 入場\n Entrar\n \n \n \n \n \n\n

Welcome to Craig Wright's house!

\n
Preparing to unlock the entrance door.
\n
\n
\n\n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n Time\n 时间\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n\n\n\n\n\n\n\n\n\n\nz+I6NxdALVtlc3TuUo2QEeV9rwyAmKB4UtQWkTLQhpE=\n\n\n\nE9H57w4vE8dP/7gZYMLm4z+AohkuIWgvqiGfCnMmy87T25Q1utzlFHE71isNvdkBooBHKnFaLm4X\n7HRSdjNyNt8UIqp8ekJZt4zK/GyUZ9Fk290A4Bey6YEko2atQSCq1wRKRbbhbyT1rggYdSVfGc85\nbQ2fnLzLETITeo5Cbp6ShcWUhdPqCSSFOoIKoIpIJG/lXNa/IFUWtBs07K6S5G77O/mPXkugzpjw\nASDyefe7Z2FPgHyebrOkyDHaS3ycnqJB11r0I4ve5j9cVsjarxfRzyGzbZri/qvbr7ov3J0HDXyl\nIQ/01ct32V6UIXlqPgsk12IcqtY16BwQalIx0A==\n\n\nShong Wang\n\n\n\nql1jleB5eU+FiIK50QwMc1pS6ieMxqciwdYHdOalEyLZZ0Swt+3Ls24g1Ktar+aJJjSJO8OjUNse\nBqECPSP2rGYIfNN7echgXEJtr9MZ5vz/G+zEyZB4lfJaBb3iGreQerdgktcjla6PC/bSiFrWs9zA\ntMp1uowMWV2T5LY7N1SYw6uLP3c8CYog7u14zwHrVgWt8MeKfgSznXyVb3A9REYo7qqnAsJq4zfR\nUfM1KQeobcLZVaPSafK6BRn0tGDoxX76ffwILRUxtMg6Ivh/OYT5cOihKhVWFRrl1/2wLoMSb+hY\nnui97tkJyEVV79c/sR1moMcsqDyaXsHc/Mz0aQ==\n\nAQAB\n\n\n\n\nMIIE/DCCAuSgAwIBAgIDFBtpMA0GCSqGSIb3DQEBCwUAMHkxEDAOBgNVBAoTB1Jvb3QgQ0ExHjAc\nBgNVBAsTFWh0dHA6Ly93d3cuY2FjZXJ0Lm9yZzEiMCAGA1UEAxMZQ0EgQ2VydCBTaWduaW5nIEF1\ndGhvcml0eTEhMB8GCSqGSIb3DQEJARYSc3VwcG9ydEBjYWNlcnQub3JnMB4XDTE5MDQxNTEzNDAy\nNVoXDTE5MTAxMjEzNDAyNVowFTETMBEGA1UEAxMKc2hvbmcud2FuZzCCASIwDQYJKoZIhvcNAQEB\nBQADggEPADCCAQoCggEBAKpdY5XgeXlPhYiCudEMDHNaUuonjManIsHWB3TmpRMi2WdEsLfty7Nu\nINSrWq/miSY0iTvDo1DbHgahAj0j9qxmCHzTe3nIYFxCba/TGeb8/xvsxMmQeJXyWgW94hq3kHq3\nYJLXI5Wujwv20oha1rPcwLTKdbqMDFldk+S2OzdUmMOriz93PAmKIO7teM8B61YFrfDHin4Es518\nlW9wPURGKO6qpwLCauM30VHzNSkHqG3C2VWj0mnyugUZ9LRg6MV++n38CC0VMbTIOiL4fzmE+XDo\noSoVVhUa5df9sC6DEm/oWJ7ove7ZCchFVe/XP7EdZqDHLKg8ml7B3PzM9GkCAwEAAaOB8DCB7TAM\nBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIDqDA0BgNVHSUELTArBggrBgEFBQcDAgYIKwYBBQUH\nAwEGCWCGSAGG+EIEAQYKKwYBBAGCNwoDAzAzBggrBgEFBQcBAQQnMCUwIwYIKwYBBQUHMAGGF2h0\ndHA6Ly9vY3NwLmNhY2VydC5vcmcvMDEGA1UdHwQqMCgwJqAkoCKGIGh0dHA6Ly9jcmwuY2FjZXJ0\nLm9yZy9yZXZva2UuY3JsMC8GA1UdEQQoMCaCCnNob25nLndhbmegGAYIKwYBBQUHCAWgDAwKc2hv\nbmcud2FuZzANBgkqhkiG9w0BAQsFAAOCAgEAMDUSBLJIbJWlq7nkyMIvyOsb7LvFGsTxh5LzK9qU\n0rGwKRbArMddz/n23N6FXDr0QwFw1lrkI5dmJYOawAUF7XfXcscO2QmgerQnRWBFa2yFI4J5ML3V\n986frNEho+JzNY0cj5JTDsyll6l4hRUvJuYOpqDc2tE3nCw6zu1caWCbWI5oC5+1CgdKznEqaMbr\nGH4Ao2ah/EJuk/F4GXok6dmzLY5c6PpElv2n+tvXc4Xbj1qatflcqk3dxkjJXVU/ebRGSl/vvkm8\nPkB/g6d9Tq48twsjqEzr8wsTrDr/+suS4JBMjYT66PyNNEtsWClJ/jYdXOedRlFLdJlkNe3gVb+9\nUYJ9/StAy+8dlRH+7Q/bBPrt2GzI3fgT7LKEKtTsVhS1+XrljXqh5BZdrZEwYlyROQ4yR+doBp7E\nm3MxJTzJWdI/Rm7w9cmQT5Nai6JU4oPYkya8lOvNXZveN/PYFN0Ie3PQ9lVBp/ABpgILjjQOZraD\npn/rc/F8mH3P0xc5+6hKqFXDEBTWy/PabBs3QSyua3Db25Olq2KXC6hUVt/tvRRA1QVQAbhuGhqJ\nV63WCiZw9mNP21d8YemZairuhBZ/DEDMZs9WGQMN6oaBawVCkQyk1hVPLvq/pD8jekXKYc2YYp8P\nNzwOeODqfDg+dlvdL+7R0kJ6VsTsJYwFur4=\n\n\nMIIHPTCCBSWgAwIBAgIBADANBgkqhkiG9w0BAQQFADB5MRAwDgYDVQQKEwdSb290IENBMR4wHAYD\nVQQLExVodHRwOi8vd3d3LmNhY2VydC5vcmcxIjAgBgNVBAMTGUNBIENlcnQgU2lnbmluZyBBdXRo\nb3JpdHkxITAfBgkqhkiG9w0BCQEWEnN1cHBvcnRAY2FjZXJ0Lm9yZzAeFw0wMzAzMzAxMjI5NDla\nFw0zMzAzMjkxMjI5NDlaMHkxEDAOBgNVBAoTB1Jvb3QgQ0ExHjAcBgNVBAsTFWh0dHA6Ly93d3cu\nY2FjZXJ0Lm9yZzEiMCAGA1UEAxMZQ0EgQ2VydCBTaWduaW5nIEF1dGhvcml0eTEhMB8GCSqGSIb3\nDQEJARYSc3VwcG9ydEBjYWNlcnQub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA\nziLA4kZ97DYoB1CW8qAzQIxL8TtmPzHlawI229Z89vGIj053NgVBlfkJ8BLPRoZzYLdufujAWGSu\nzbCtRRcMY/pnCujW0r8+55jE8Ez64AO7NV1sId6eINm6zWYyN3L69wj1x81YyY7nDl7qPv4coRQK\nFWyGhFtkZip6qUtTefWIonvuLwphK42yfk1WpRPs6tqSnqxEQR5YYGUFZvjARL3LlPdCfgv3ZWiY\nUQXw8wWRBB0bF4LsyFe7w2t6iPGwcswlWyCR7BYCEo8y6RcYSNDHBS4CMEK4JZwFaz+qOqfrU0j3\n6NK2B5jcG8Y0f3/JHIJ6BVgrCFvzOKKrF11myZjXnhCLotLddJr3cQxyYN/Nb5gznZY0dj4kepKw\nDpUeb+agRThHqtdB7Uq3EvbXG4OKDy7YCbZZ16oE/9KTfWgu3YtLq1i6L43qlaegw1SJpfvbi1Ei\nnbLDvhG+LJGGi5Z4rSDTii8aP8bQUWWHIbEZAWV/RRyH9XzQQUxPKZgh/TMfdQwEUfoZd9vUFBzu\ngcMd9Zi3aQaRIt0AUMyBMawSB3s42mhb5ivUfslfrejrckzzAeVLIL+aplfKkQABi6F1ITe1Yw1n\nPkZPcCBnzsXWWdsC4PDSy826YreQQejdIOQpvGQpQsgi3Hia/0PsmBsJUUtaWsJx8cTLc6nloQsC\nAwEAAaOCAc4wggHKMB0GA1UdDgQWBBQWtTIb1Mfz4OaO873SsDrusjkY0TCBowYDVR0jBIGbMIGY\ngBQWtTIb1Mfz4OaO873SsDrusjkY0aF9pHsweTEQMA4GA1UEChMHUm9vdCBDQTEeMBwGA1UECxMV\naHR0cDovL3d3dy5jYWNlcnQub3JnMSIwIAYDVQQDExlDQSBDZXJ0IFNpZ25pbmcgQXV0aG9yaXR5\nMSEwHwYJKoZIhvcNAQkBFhJzdXBwb3J0QGNhY2VydC5vcmeCAQAwDwYDVR0TAQH/BAUwAwEB/zAy\nBgNVHR8EKzApMCegJaAjhiFodHRwczovL3d3dy5jYWNlcnQub3JnL3Jldm9rZS5jcmwwMAYJYIZI\nAYb4QgEEBCMWIWh0dHBzOi8vd3d3LmNhY2VydC5vcmcvcmV2b2tlLmNybDA0BglghkgBhvhCAQgE\nJxYlaHR0cDovL3d3dy5jYWNlcnQub3JnL2luZGV4LnBocD9pZD0xMDBWBglghkgBhvhCAQ0ESRZH\nVG8gZ2V0IHlvdXIgb3duIGNlcnRpZmljYXRlIGZvciBGUkVFIGhlYWQgb3ZlciB0byBodHRwOi8v\nd3d3LmNhY2VydC5vcmcwDQYJKoZIhvcNAQEEBQADggIBACjH7pyCArpcgBLKNQodgW+JapnM8mgP\nf6fhjViVPr3yBsOQWqy1YPaZQwGjiHCcnWKdpIevZ1gNMDY75q1I08t0AoZxPuIrA2jxNGJARjtT\n6ij0rPtmlVOKTV39O9lg18p5aTuxZZKmxoGCXJzN600BiqXfEVWqFcofN8CCmHBh22p8lqOOLlQ+\nTyGpkO/cgr/c6EWtTZBzCDyUZbAEmXZ/4rzCahWqlwQ3JNgelE5tDlG+1sSPypZt90Pf6DBlJzt7\nu0NDY8RD97LsaMzhGY4i+5jhe1o+ATc7iwiwovOVThrLm82asduycPAtStvYsONvRUgzEv/+PDIq\nVPfE94rwiCPCR/5kenHA0R6mY7AHfqQv0wGP3J8rtsYIqQ+TSCX8Ev2fQtzzxD72V7DX3WnRBnc0\nCkvSyqD/HMaMyRa+xMwyN2hzXwj7UfdJUzYFCpUCTPJ5GhD22Dp1nPMd8aINcGeGG7MW9S/lpOt5\nhvk9C8JzC6WZrG/8Z7jlLwumGCSNe9FINSkYQKyTYOGWhlC0elnYjyELn8+CkcY7v2vcB5G5l1Yj\nqrZslMZIBjzkzk6q5PYvCdxTby78dOs6Y5nCpqyJvKeyRKANihDjbPIky/qbn3BHLt4Ui9SyIAmW\nomTxJBzcoTWcFbLUvFUufQb1nA5V9FrWk9p2rSVzTMVD\n\n\n\n" diff --git a/AlphaWalletTests/TokenScriptClient/XMLHandlerTest.swift b/AlphaWalletTests/TokenScriptClient/XMLHandlerTest.swift index f9ee60c5a..24dcc4209 100644 --- a/AlphaWalletTests/TokenScriptClient/XMLHandlerTest.swift +++ b/AlphaWalletTests/TokenScriptClient/XMLHandlerTest.swift @@ -12,7 +12,7 @@ import BigInt import AlphaWalletAddress import AlphaWalletCore import AlphaWalletFoundation -import AlphaWalletTokenScript +@testable import AlphaWalletTokenScript // swiftlint:disable type_body_length class XMLHandlerTest: XCTestCase { @@ -20,15 +20,14 @@ class XMLHandlerTest: XCTestCase { func testParser() { let assetDefinitionStore = AssetDefinitionStore.make() - let token = XMLHandler(contract: AlphaWalletFoundation.Constants.nullAddress, tokenType: .erc20, assetDefinitionStore: assetDefinitionStore).getToken( + let token = assetDefinitionStore.xmlHandler(forContract: AlphaWalletFoundation.Constants.nullAddress, tokenType: .erc20).getToken( name: "", symbol: "", fromTokenIdOrEvent: .tokenId(tokenId: BigUInt(tokenHex, radix: 16)!), index: UInt16(1), inWallet: .make(), server: .main, - tokenType: TokenType.erc875, - assetDefinitionStore: assetDefinitionStore + tokenType: TokenType.erc875 ) XCTAssertNotNil(token) } @@ -903,13 +902,13 @@ class XMLHandlerTest: XCTestCase { """ let contractAddress = AlphaWallet.Address(string: "0xA66A3F08068174e8F005112A8b2c7A507a822335")! - let store = AssetDefinitionStore(backingStore: AssetDefinitionInMemoryBackingStore(), networkService: FakeNetworkService(), blockchainsProvider: BlockchainsProviderImplementation.make(servers: [.main]), features: TokenScriptFeatures()) + let store = AssetDefinitionStore(backingStore: AssetDefinitionInMemoryBackingStore(), networkService: FakeNetworkService(), blockchainsProvider: BlockchainsProviderImplementation.make(servers: [.main]), features: TokenScriptFeatures(), resetFolders: false) - store[contractAddress] = xml - let xmlHandler = XMLHandler(contract: contractAddress, tokenType: .erc20, assetDefinitionStore: store) + store.storeOfficialXmlForToken(contractAddress, xml: xml, fromUrl: URL(string: "http://google.com")!) + let xmlHandler = store.xmlHandler(forContract: contractAddress, tokenType: .erc20) let tokenId = BigUInt("0000000000000000000000000000000002000000000000000000000000000000", radix: 16)! let server: RPCServer = .main - let token = xmlHandler.getToken(name: "Some name", symbol: "Some symbol", fromTokenIdOrEvent: .tokenId(tokenId: tokenId), index: 1, inWallet: .make(), server: server, tokenType: TokenType.erc875, assetDefinitionStore: store) + let token = xmlHandler.getToken(name: "Some name", symbol: "Some symbol", fromTokenIdOrEvent: .tokenId(tokenId: tokenId), index: 1, inWallet: .make(), server: server, tokenType: TokenType.erc875) let values = token.values XCTAssertEqual(values["locality"]?.stringValue, "Saint Petersburg") @@ -917,11 +916,11 @@ class XMLHandlerTest: XCTestCase { // swiftlint:enable function_body_length func testNoAssetDefinition() { - let store = AssetDefinitionStore(backingStore: AssetDefinitionInMemoryBackingStore(), networkService: FakeNetworkService(), blockchainsProvider: BlockchainsProviderImplementation.make(servers: [.main]), features: TokenScriptFeatures()) - let xmlHandler = XMLHandler(contract: AlphaWalletFoundation.Constants.nullAddress, tokenType: .erc875, assetDefinitionStore: store) + let store = AssetDefinitionStore(backingStore: AssetDefinitionInMemoryBackingStore(), networkService: FakeNetworkService(), blockchainsProvider: BlockchainsProviderImplementation.make(servers: [.main]), features: TokenScriptFeatures(), resetFolders: false) + let xmlHandler = store.xmlHandler(forContract: AlphaWalletFoundation.Constants.nullAddress, tokenType: .erc875) let tokenId = BigUInt("0000000000000000000000000000000002000000000000000000000000000000", radix: 16)! let server: RPCServer = .main - let token = xmlHandler.getToken(name: "Some name", symbol: "Some symbol", fromTokenIdOrEvent: .tokenId(tokenId: tokenId), index: 1, inWallet: .make(), server: server, tokenType: TokenType.erc721, assetDefinitionStore: store) + let token = xmlHandler.getToken(name: "Some name", symbol: "Some symbol", fromTokenIdOrEvent: .tokenId(tokenId: tokenId), index: 1, inWallet: .make(), server: server, tokenType: TokenType.erc721) let values = token.values XCTAssertTrue(values.isEmpty) } diff --git a/AlphaWalletTokenScript.podspec b/AlphaWalletTokenScript.podspec index 8c8ca0203..ac3a9859b 100644 --- a/AlphaWalletTokenScript.podspec +++ b/AlphaWalletTokenScript.podspec @@ -32,8 +32,10 @@ Pod::Spec.new do |s| s.dependency 'PromiseKit/CorePromise' s.dependency 'AlphaWalletAddress' s.dependency 'AlphaWalletABI' + s.dependency 'AlphaWalletAttestation' s.dependency 'AlphaWalletCore' s.dependency 'AlphaWalletLogger' s.dependency 'AlphaWalletOpenSea' s.dependency 'AlphaWalletWeb3' + s.dependency 'CryptoSwift' end diff --git a/Podfile.lock b/Podfile.lock index 367f0f3c0..6e4abab7d 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -78,12 +78,14 @@ PODS: - AlphaWalletTokenScript (1.0.0): - AlphaWalletABI - AlphaWalletAddress + - AlphaWalletAttestation - AlphaWalletCore - AlphaWalletLogger - AlphaWalletOpenSea - AlphaWalletWeb3 - APIKit - BigInt + - CryptoSwift - Kanna - PromiseKit/CorePromise - AlphaWalletTrackAPICalls (1.0.0) @@ -453,7 +455,7 @@ SPEC CHECKSUMS: AlphaWalletNotifications: 6e6a5f1cf0fc598c1c917dfb527a608b049a0a96 AlphaWalletOpenSea: e6ee4efb03db0dfebc69dfe23db33fa33bba8918 AlphaWalletShareExtensionCore: e5fc88c6120d5b77b3ba44a37e7b58da5ccbb7b6 - AlphaWalletTokenScript: b4e5d7acdc85ed6915d8bf07c82abf20836cee50 + AlphaWalletTokenScript: f28807083e5c625dc415349a710eda2677a64105 AlphaWalletTrackAPICalls: 0e18ad5b389b054ee30673e7f1caf095a17c1b33 AlphaWalletTrustWalletCoreExtensions: 976653ec0116dbcafa6ff1fc93ebdc3a469e5f57 AlphaWalletWeb3: 6112b8d749368d3a8208bbec4220b13e003e4f40 diff --git a/modules/AlphaWalletABI/AlphaWalletABI/Types/ABIValue.swift b/modules/AlphaWalletABI/AlphaWalletABI/Types/ABIValue.swift index 2acb70363..4e25c423c 100644 --- a/modules/AlphaWalletABI/AlphaWalletABI/Types/ABIValue.swift +++ b/modules/AlphaWalletABI/AlphaWalletABI/Types/ABIValue.swift @@ -7,6 +7,7 @@ import Foundation import AlphaWalletAddress import BigInt +//TODO remove AlphaWalletABI's dependency on TrustKeystore import TrustKeystore public indirect enum ABIValue: Equatable { diff --git a/modules/AlphaWalletAttestation/AlphaWalletAttestation/Attestation.swift b/modules/AlphaWalletAttestation/AlphaWalletAttestation/Attestation.swift index 72cf0b70f..1598885cd 100644 --- a/modules/AlphaWalletAttestation/AlphaWalletAttestation/Attestation.swift +++ b/modules/AlphaWalletAttestation/AlphaWalletAttestation/Attestation.swift @@ -95,7 +95,33 @@ public enum AttestationPropertyValue: Codable, Hashable { } public struct Attestation: Codable, Hashable { - typealias SchemaUid = String + public struct SchemaUid: Hashable, Codable, ExpressibleByStringLiteral { + public let value: String + + public init(stringLiteral value: StringLiteralType) { + self.value = value + } + + public init(value: String) { + self.value = value + } + } + + public struct Schema: ExpressibleByStringLiteral { + public let value: String + + public init(stringLiteral value: StringLiteralType) { + self.value = value + } + + public init(value: String) { + self.value = value + } + } + + public struct AttestationId: Hashable { + public let value: String + } //Redefine here so reduce dependencies static var vitaliklizeConstant: UInt8 = 27 @@ -107,6 +133,11 @@ public struct Attestation: Codable, Hashable { public let type: ABIv2.Element.InOut public let value: AttestationPropertyValue + public init(type: ABIv2.Element.InOut, value: AttestationPropertyValue) { + self.type = type + self.value = value + } + static func mapValue(of output: ABIv2.Element.ParameterType, for value: AnyObject) -> AttestationPropertyValue { switch output { case .address: @@ -174,6 +205,12 @@ public struct Attestation: Codable, Hashable { private let easAttestation: EasAttestation public let isValidAttestationIssuer: Bool + public var easAttestationData: String { easAttestation.data } + public var easAttestationTime: Int { easAttestation.time } + public var easAttestationExpirationTime: Int { easAttestation.expirationTime } + public var attestationId: AttestationId { + AttestationId(value: easAttestation.uid) + } public var recipient: AlphaWallet.Address? { return AlphaWallet.Address(uncheckedAgainstNullAddress: easAttestation.recipient) } @@ -191,6 +228,12 @@ public struct Attestation: Codable, Hashable { public var server: RPCServer { easAttestation.server } //Good for debugging, in case converting to `RPCServer` is done wrongly public var chainId: Int { easAttestation.chainId } + public var version: String { easAttestation.version } + public var refUID: String { easAttestation.refUID } + public var revocable: Bool { easAttestation.revocable } + //EAS's schema here *does* refer to the schema UID + public var schemaUid: SchemaUid { SchemaUid(value: easAttestation.schema) } + public var easMessageVersion: Int? { easAttestation.messageVersion } public var scriptUri: URL? { let url: URL? = data.compactMap { each in if each.type.name == "scriptURI" { @@ -206,6 +249,14 @@ public struct Attestation: Codable, Hashable { }.first return url } + public var signature: Data { + //This expects `v` to be 0x1b/0x1c, so do not implement -27 + if let result: Data = Web3.Utils.marshalSignature(v: easAttestation.v, r: easAttestation.r, s: easAttestation.s) { + return result + } else { + return Data() + } + } private init(data: [TypeValuePair], easAttestation: EasAttestation, isValidAttestationIssuer: Bool, source: String) { self.data = data @@ -220,7 +271,13 @@ public struct Attestation: Codable, Hashable { switch each.value { case .string(let value): return value - case .address, .bool, .bytes, .int, .uint: + case .int(let value): + return String(value) + case .uint(let value): + return String(value) + case .address(let value): + return value.eip55String + case .bool, .bytes: return nil } } else { @@ -230,21 +287,27 @@ public struct Attestation: Codable, Hashable { } public static func extract(fromUrlString urlString: String) async throws -> Attestation { + if let rawAttestation = extractRawAttestation(fromUrlString: urlString) { + return try await Attestation.extract(fromEncodedValue: rawAttestation, source: urlString) + } else { + throw AttestationError.parseAttestationUrlFailed(urlString) + } + } + + public static func extractRawAttestation(fromUrlString urlString: String) -> String? { if let url = URL(string: urlString), let fragment = URLComponents(url: url, resolvingAgainstBaseURL: false)?.fragment, let components = Optional(fragment.split(separator: "=", maxSplits: 1)), components.first == "attestation" { let encodedAttestation = components[1] - let attestation = try await Attestation.extract(fromEncodedValue: String(encodedAttestation), source: urlString) - return attestation + return String(encodedAttestation) } else if let url = URL(string: urlString), let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), let queryItems = urlComponents.queryItems, let ticketItem = queryItems.first(where: { $0.name == "ticket" }) ?? queryItems.first(where: { $0.name == "attestation" }), let encodedAttestation = ticketItem.value { - let attestation = try await Attestation.extract(fromEncodedValue: encodedAttestation, source: urlString) - return attestation + return encodedAttestation } else { - throw AttestationError.parseAttestationUrlFailed(urlString) + return nil } } @@ -307,22 +370,90 @@ public struct Attestation: Codable, Hashable { return Attestation(data: results, easAttestation: attestation, isValidAttestationIssuer: isValidAttestationIssuer, source: source) } + public static func computeAttestationCollectionId(forAttestation attestation: Attestation, collectionIdFields: [AttestationAttribute]) -> String { + var results: [String] = [ + convertSignerAddressToFormatForComputingCollectionId(signer: attestation.signer) + ] + let collectionIdFieldsData = Attestation.resolveAttestationAttributes(forAttestation: attestation, withAttestationFields: collectionIdFields) + for each in collectionIdFieldsData { + results.append(each.value.stringValue) + } + let collectionId = results.joined() + let hash = collectionId.sha3(.keccak256) + return hash + } + + public static func convertSignerAddressToFormatForComputingCollectionId(signer: AlphaWallet.Address?) -> String { + //We drop both the 0x, and the leading 04 + signer?.eip55String.lowercased().drop0x.dropLeading04 ?? "" + } + enum functional {} } +fileprivate var attestationDateFormatter = Date.formatter(with: "dd MMM yyyy h:mm:ss a") +extension Attestation { + public static func resolveAttestationAttributes(forAttestation attestation: Attestation, withAttestationFields attestationFields: [AttestationAttribute]) -> [Attestation.TypeValuePair] { + return attestationFields.compactMap { eachField -> Attestation.TypeValuePair? in + let label = eachField.label + let path = eachField.path + if path.hasPrefix("data.") { + let dataFieldName = path.dropDataPrefix + let match = attestation.data.first { each in + return each.type.name == dataFieldName + } + return match.flatMap { Attestation.TypeValuePair(type: ABIv2.Element.InOut(name: label, type: $0.type.type), value: $0.value) } + } else { + switch path { + case "name": + return Attestation.TypeValuePair(type: ABIv2.Element.InOut(name: label, type: .string), value: AttestationPropertyValue.string("EAS Attestation")) + case "version": + return Attestation.TypeValuePair(type: ABIv2.Element.InOut(name: label, type: .string), value: AttestationPropertyValue.string(attestation.version)) + case "chainId": + return Attestation.TypeValuePair(type: ABIv2.Element.InOut(name: label, type: .uint(bits: 256)), value: AttestationPropertyValue.uint(BigUInt(attestation.chainId))) + case "signer": + //We need signer for computation of collectionId or attestation ID (for re-issue, replacements) + return Attestation.TypeValuePair(type: ABIv2.Element.InOut(name: label, type: .string), value: AttestationPropertyValue.string(attestation.signer.eip55String)) + case "verifyingContract": + //TODO be much better if we can specify optionals for types such as Address so when we display in the UI, we can format them better (eg. "0x0000000…") is useless when displayed, it'd be better if we middle truncate them + return Attestation.TypeValuePair(type: ABIv2.Element.InOut(name: label, type: .string), value: AttestationPropertyValue.string(attestation.verifyingContract?.eip55String ?? "")) + case "recipient": + return Attestation.TypeValuePair(type: ABIv2.Element.InOut(name: label, type: .string), value: AttestationPropertyValue.string(attestation.recipient?.eip55String ?? "")) + case "refUID": + return Attestation.TypeValuePair(type: ABIv2.Element.InOut(name: label, type: .string), value: AttestationPropertyValue.string(attestation.refUID)) + case "revocable": + return Attestation.TypeValuePair(type: ABIv2.Element.InOut(name: label, type: .bool), value: AttestationPropertyValue.bool(attestation.revocable)) + case "schema": + return Attestation.TypeValuePair(type: ABIv2.Element.InOut(name: label, type: .string), value: AttestationPropertyValue.string(attestation.schemaUid.value)) + case "time": + //TODO not good to convert to string here, but type constraints + let string = attestationDateFormatter.string(from: attestation.time) + return Attestation.TypeValuePair(type: ABIv2.Element.InOut(name: label, type: .string), value: AttestationPropertyValue.string(string)) + case "expirationTime": + //TODO not good to convert to string here, but type constraints + let string = attestation.expirationTime.map { attestationDateFormatter.string(from: $0) } ?? "—" + return Attestation.TypeValuePair(type: ABIv2.Element.InOut(name: label, type: .string), value: AttestationPropertyValue.string(string)) + default: + return nil + } + } + } + } +} + //For testing extension Attestation.functional { - internal static func extractTypesFromSchemaForTesting(_ schema: String) -> [ABIv2.Element.InOut]? { + internal static func extractTypesFromSchemaForTesting(_ schema: Attestation.Schema) -> [ABIv2.Element.InOut]? { return extractTypesFromSchema(schema) } } fileprivate extension Attestation.functional { struct SchemaRecord { - let uid: String + let uid: Attestation.SchemaUid let resolver: AlphaWallet.Address let revocable: Bool - let schema: String + let schema: Attestation.Schema } static func getKeySchemaUid(server: RPCServer) throws -> Attestation.SchemaUid { @@ -376,11 +507,8 @@ fileprivate extension Attestation.functional { static func unzipAttestation(_ zipped: String) throws -> Data { //Instead of the usual use of / and +, it might use _ and - instead. So we need to normalize it for parsing let normalizedZipped = zipped.replacingOccurrences(of: "_", with: "/").replacingOccurrences(of: "-", with: "+").paddedForBase64Encoded - //Can't check `zipped.isGzipped`, it's false (sometimes?), but it works, so just don't check - guard let compressed = Data(base64Encoded: normalizedZipped) else { - throw Attestation.AttestationInternalError.unzipAttestationFailed(zipped: zipped) - } + guard let compressed = Data(base64Encoded: normalizedZipped) else { throw Attestation.AttestationInternalError.unzipAttestationFailed(zipped: zipped) } do { return try compressed.gunzipped() } catch { @@ -426,7 +554,9 @@ fileprivate extension Attestation.functional { throw Attestation.AttestationInternalError.reconstructSignatureFailed(attestation: attestation, v: v, r: r, s: s) } let ethereumAddress = Web3.Utils.hashECRecover(hash: eip712.digest, signature: sig) - return ethereumAddress.flatMap { AlphaWallet.Address(address: $0) } + return ethereumAddress.flatMap { + AlphaWallet.Address(address: $0) + } } static func extractAttestationData(attestation: EasAttestation) async throws -> [Attestation.TypeValuePair] { @@ -440,7 +570,8 @@ fileprivate extension Attestation.functional { ] infoLog("[Attestation] schema UID not provided: \(attestation.schema), so we assume stock ticket schema: \(types)") } else { - let schemaRecord = try await getSchemaRecord(keySchemaUid: attestation.schema, server: attestation.server) + //EAS's schema here *does* refer to the schema UID + let schemaRecord = try await getSchemaRecord(keySchemaUid: Attestation.SchemaUid(value: attestation.schema), server: attestation.server) infoLog("[Attestation] Found schemaRecord: \(schemaRecord) with schema: \(schemaRecord.schema)") guard let localTypes: [ABIv2.Element.InOut] = extractTypesFromSchema(schemaRecord.schema) else { throw Attestation.AttestationInternalError.extractAttestationDataFailed(attestation: attestation) @@ -460,12 +591,16 @@ fileprivate extension Attestation.functional { } } - static func extractTypesFromSchema(_ schema: String) -> [ABIv2.Element.InOut]? { - let rawList = schema - .components(separatedBy: ",") - .map { $0.components(separatedBy: " ") } + static func extractTypesFromSchema(_ schema: Attestation.Schema) -> [ABIv2.Element.InOut]? { + let rawList = schema.value + .components(separatedBy: ",") + .map { + $0.components(separatedBy: " ") + } let result: [ABIv2.Element.InOut] = rawList.compactMap { each in - guard each.count == 2 else { return nil } + guard each.count == 2 else { + return nil + } let typeString = { //See https://github.com/AlphaWallet/alpha-wallet-android/blob/86692639f2bef2acb890524645d80b3910141148/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java#L3051 if each[0].hasPrefix("uint") || each[0].hasPrefix("int") { @@ -494,20 +629,20 @@ fileprivate extension Attestation.functional { static func validateSigner(customResolverContractAddress: AlphaWallet.Address, signerAddress: AlphaWallet.Address, server: RPCServer) async throws -> Bool { let rootKeyUID = try getRootKeyUid(server: server) let abiString = """ - [ - { - "constant": false, - "inputs": [ - {"name": "rootKeyUID","type": "bytes32"}, + [ + { + "constant": false, + "inputs": [ + {"name": "rootKeyUID","type": "bytes32"}, {"name": "signerAddress","type": "address"} - ], - "name": "validateSignature", - "outputs": [{"name": "", "type": "bool"}], - "type": "function" + ], + "name": "validateSignature", + "outputs": [{"name": "", "type": "bool"}], + "type": "function" } ] """ - let parameters = [rootKeyUID, EthereumAddress(address: signerAddress)] as [AnyObject] + let parameters = [rootKeyUID.value, EthereumAddress(address: signerAddress)] as [AnyObject] let result: [String: Any] do { result = try await Attestation.callSmartContract(server, customResolverContractAddress, "validateSignature", abiString, parameters) @@ -534,12 +669,13 @@ fileprivate extension Attestation.functional { //Schema the schema pointed to by a schema UID can't change, we can hardcode some private static var hardcodedSchemaRecords: [Attestation.SchemaUid: SchemaRecord] = [ //KeyDecription is verbatim from the schema definition - "0x4455598d3ec459c4af59335f7729fea0f50ced46cb1cd67914f5349d44142ec1": SchemaRecord(uid: "0x4455598d3ec459c4af59335f7729fea0f50ced46cb1cd67914f5349d44142ec1", resolver: AlphaWallet.Address(string: "0x0Ed88b8AF0347fF49D7e09AA56bD5281165225B6")!, revocable: true, schema: "string KeyDecription,bytes ASN1Key,bytes PublicKey"), - "0x5f0437f7c1db1f8e575732ca52cc8ad899b3c9fe38b78b67ff4ba7c37a8bf3b4": SchemaRecord(uid: "0x5f0437f7c1db1f8e575732ca52cc8ad899b3c9fe38b78b67ff4ba7c37a8bf3b4", resolver: AlphaWallet.Address(string: "0xF0768c269b015C0A246157c683f9377eF571dCD3")!, revocable: true, schema: "string KeyDescription,bytes ASN1Key,bytes PublicKey"), + Attestation.SchemaUid("0x4455598d3ec459c4af59335f7729fea0f50ced46cb1cd67914f5349d44142ec1"): SchemaRecord(uid: Attestation.SchemaUid("0x4455598d3ec459c4af59335f7729fea0f50ced46cb1cd67914f5349d44142ec1"), resolver: AlphaWallet.Address(string: "0x0Ed88b8AF0347fF49D7e09AA56bD5281165225B6")!, revocable: true, schema: Attestation.Schema("string KeyDecription,bytes ASN1Key,bytes PublicKey")), + "0x5f0437f7c1db1f8e575732ca52cc8ad899b3c9fe38b78b67ff4ba7c37a8bf3b4": SchemaRecord(uid: Attestation.SchemaUid("0x5f0437f7c1db1f8e575732ca52cc8ad899b3c9fe38b78b67ff4ba7c37a8bf3b4"), resolver: AlphaWallet.Address(string: "0xF0768c269b015C0A246157c683f9377eF571dCD3")!, revocable: true, schema: "string KeyDescription,bytes ASN1Key,bytes PublicKey"), "0x7f6fb09beb1886d0b223e9f15242961198dd360021b2c9f75ac879c0f786cafd": SchemaRecord(uid: "0x7f6fb09beb1886d0b223e9f15242961198dd360021b2c9f75ac879c0f786cafd", resolver: Constants.nullAddress, revocable: true, schema: "string eventId,string ticketId,uint8 ticketClass,bytes commitment"), "0x0630f3342772bf31b669bdbc05af0e9e986cf16458f292dfd3b57564b3dc3247": SchemaRecord(uid: "0x0630f3342772bf31b669bdbc05af0e9e986cf16458f292dfd3b57564b3dc3247", resolver: Constants.nullAddress, revocable: true, schema: "string devconId,string ticketIdString,uint8 ticketClass,bytes commitment"), "0xba8aaaf91d1f63d998fb7da69449d9a314bef480e9555710c77d6e594e73ca7a": SchemaRecord(uid: "0xba8aaaf91d1f63d998fb7da69449d9a314bef480e9555710c77d6e594e73ca7a", resolver: Constants.nullAddress, revocable: true, schema: "string eventId,string ticketId,uint8 ticketClass,bytes commitment,string scriptUri"), ] + static func getSchemaRecord(keySchemaUid: Attestation.SchemaUid, server: RPCServer) async throws -> SchemaRecord { if let hardcoded = hardcodedSchemaRecords[keySchemaUid] { infoLog("[Attestation] Using hardcoded schema record and skipping JSON-RPC call for keySchemaUid: \(keySchemaUid) returning schemaRecord: \(hardcoded)") @@ -547,30 +683,30 @@ fileprivate extension Attestation.functional { } let registryContract = try getEasSchemaContract(server: server) let abiString = """ - [ - { - "constant": false, - "inputs": [ - {"name": "keySchemaUid", "type": "bytes32"}, - ], - "name": "getSchema", + [ + { + "constant": false, + "inputs": [ + {"name": "keySchemaUid", "type": "bytes32"}, + ], + "name": "getSchema", "outputs": [ { "components": [ {"name": "uid", "type": "bytes32"}, - {"name": "resolver", "type": "address"}, - {"name": "revocable", "type": "bool"}, + {"name": "resolver", "type": "address"}, + {"name": "revocable", "type": "bool"}, {"name": "schema", "type": "string"}, ], "name": "", "type": "tuple", } ], - "type": "function" + "type": "function" } ] """ - let parameters = [keySchemaUid] as [AnyObject] + let parameters = [keySchemaUid.value] as [AnyObject] let functionName = "getSchema" let cacheKey = "\(registryContract).\(functionName) \(parameters) \(server.chainID) \(abiString)" if let cached = cachedSchemaRecords[cacheKey] { @@ -583,11 +719,12 @@ fileprivate extension Attestation.functional { //TODO figure out why this fails on device, but works on simulator throw Attestation.AttestationInternalError.schemaRecordNotFound(keySchemaUid: keySchemaUid, server: server) } - if let uid = ((result["0"] as? [AnyObject])?[0] as? Data)?.toHexString(), + if let uidString = ((result["0"] as? [AnyObject])?[0] as? Data)?.toHexString(), let resolver = (result["0"] as? [AnyObject])?[1] as? EthereumAddress, let revocable = (result["0"] as? [AnyObject])?[2] as? Bool, let schema = (result["0"] as? [AnyObject])?[3] as? String { - let record = SchemaRecord(uid: uid, resolver: AlphaWallet.Address(address: resolver), revocable: revocable, schema: schema) + let uid = Attestation.SchemaUid(value: uidString) + let record = SchemaRecord(uid: uid, resolver: AlphaWallet.Address(address: resolver), revocable: revocable, schema: Attestation.Schema(value: schema)) cachedSchemaRecords[cacheKey] = record return record } else { @@ -613,3 +750,12 @@ fileprivate extension Data { return map({ String(format: "%02x", $0) }).joined() } } + +fileprivate extension String { + public var dropDataPrefix: String { + if count > 5 && substring(with: 0..<5) == "data." { + return String(dropFirst(5)) + } + return self + } +} diff --git a/modules/AlphaWalletAttestation/AlphaWalletAttestation/AttestationAttribute.swift b/modules/AlphaWalletAttestation/AlphaWalletAttestation/AttestationAttribute.swift new file mode 100644 index 000000000..381574f3f --- /dev/null +++ b/modules/AlphaWalletAttestation/AlphaWalletAttestation/AttestationAttribute.swift @@ -0,0 +1,13 @@ +// Copyright © 2023 Stormbird PTE. LTD. + +import Foundation + +public struct AttestationAttribute { + public let label: String + public let path: String + + public init(label: String, path: String) { + self.label = label + self.path = path + } +} \ No newline at end of file diff --git a/modules/AlphaWalletAttestation/AlphaWalletAttestation/AttestationsStore.swift b/modules/AlphaWalletAttestation/AlphaWalletAttestation/AttestationsStore.swift index 1ea454e00..b0e304889 100644 --- a/modules/AlphaWalletAttestation/AlphaWalletAttestation/AttestationsStore.swift +++ b/modules/AlphaWalletAttestation/AlphaWalletAttestation/AttestationsStore.swift @@ -25,26 +25,49 @@ public class AttestationsStore { return functional.readAttestations(from: fileUrl).flatMap { $0.value } } - public func addAttestation(_ attestation: Attestation, forWallet address: AlphaWallet.Address) -> Bool { + //TODO we pass in `identifyingFieldNames` and `collectionIdFieldNames` to compare because this code is in AlphaWalletAttestation and we don't have access to TokenScript here. Leaky, or good? + public func addAttestation(_ attestation: Attestation, forWallet address: AlphaWallet.Address, collectionIdFieldNames: [String], identifyingFieldNames: [String]) async -> Bool { var allAttestations = functional.readAttestations(from: Self.fileUrl) do { var attestationsForWallet: [Attestation] = allAttestations[address] ?? [] - guard !attestations.contains(attestation) else { + if attestations.contains(attestation) { infoLog("[Attestation] Attestation already exist. Skipping") return false + } else if let attestationToReplace = await findAttestationWithSameIdentity(attestation, collectionIdFieldNames: collectionIdFieldNames, identifyingFieldNames: identifyingFieldNames, inAttestations: attestationsForWallet) { + attestationsForWallet = functional.arrayReplacingAttestation(array: attestationsForWallet, old: attestationToReplace, replacement: attestation) + allAttestations[address] = attestationsForWallet + try saveAttestations(attestations: allAttestations) + attestations = attestationsForWallet + infoLog("[Attestation] Imported attestation and replaced previous") + return true + } else { + attestationsForWallet.append(attestation) + allAttestations[address] = attestationsForWallet + try saveAttestations(attestations: allAttestations) + attestations = attestationsForWallet + infoLog("[Attestation] Imported attestation") + return true } - 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 } } + private func findAttestationWithSameIdentity(_ attestation: Attestation, collectionIdFieldNames: [String], identifyingFieldNames: [String], inAttestations attestations: [Attestation]) async -> Attestation? { + let attestationIdFields: [AttestationAttribute] = identifyingFieldNames.map { AttestationAttribute(label: $0, path: $0) } + let collectionIdFields: [AttestationAttribute] = collectionIdFieldNames.map { AttestationAttribute(label: $0, path: $0) } + guard !attestationIdFields.isEmpty && !collectionIdFields.isEmpty else { return nil } + let identityForNewAttestation = functional.computeIdentity(forAttestation: attestation, collectionIdFields: collectionIdFields, attestationIdFields: attestationIdFields) + for each in attestations { + let identityForExistingAttestation = functional.computeIdentity(forAttestation: each, collectionIdFields: collectionIdFields, attestationIdFields: attestationIdFields) + if identityForExistingAttestation == identityForNewAttestation { + return each + } + } + return nil + } + public func removeAttestation(_ attestation: Attestation, forWallet address: AlphaWallet.Address) { var allAttestations = functional.readAttestations(from: Self.fileUrl) do { @@ -85,4 +108,21 @@ fileprivate extension AttestationsStore.functional { let result: [Attestation] = allAttestations[address] ?? [] return result } + + static func arrayReplacingAttestation(array: [Attestation], old: Attestation, replacement: Attestation) -> [Attestation] { + var result = array + if let index = result.firstIndex(of: old) { + result[index] = replacement + } + return result + } + + //TODO better in AlphaWalletTokenScript. But we can't move it due to dependency + static func computeIdentity(forAttestation attestation: Attestation, collectionIdFields: [AttestationAttribute], attestationIdFields: [AttestationAttribute]) -> String { + let idFieldsData = Attestation.resolveAttestationAttributes(forAttestation: attestation, withAttestationFields: attestationIdFields) + let collectionId = Attestation.computeAttestationCollectionId(forAttestation: attestation, collectionIdFields: collectionIdFields) + let identity = String(attestation.chainId) + collectionId + idFieldsData.map { $0.value.stringValue }.joined() + //We should hash too, but exclude it because it introduces a dependency on CryptoSwift and we don't need it as we can compare pre-hash + return identity + } } diff --git a/modules/AlphaWalletAttestation/AlphaWalletAttestation/EasAttestation.swift b/modules/AlphaWalletAttestation/AlphaWalletAttestation/EasAttestation.swift index 83d118113..716162d13 100644 --- a/modules/AlphaWalletAttestation/AlphaWalletAttestation/EasAttestation.swift +++ b/modules/AlphaWalletAttestation/AlphaWalletAttestation/EasAttestation.swift @@ -12,6 +12,7 @@ struct EasAttestation: Codable, Hashable { let v: UInt8 let signer: AlphaWallet.Address let uid: String + ///This is the schema UID, not the schema, but we keep the name to be consistent with EAS terminology let schema: String let recipient: String let time: Int diff --git a/modules/AlphaWalletCore/AlphaWalletCore/Extensions/String+Extensions.swift b/modules/AlphaWalletCore/AlphaWalletCore/Extensions/String+Extensions.swift index 2f7b177c6..100033a5c 100644 --- a/modules/AlphaWalletCore/AlphaWalletCore/Extensions/String+Extensions.swift +++ b/modules/AlphaWalletCore/AlphaWalletCore/Extensions/String+Extensions.swift @@ -105,6 +105,13 @@ extension String { } } + public var dropLeading04: String { + if count > 2 && substring(with: 0..<2) == "04" { + return String(dropFirst(2)) + } + return self + } + //Base64 encoding must be in multiples of 4. `Data(base64Encoded:)` doesn't parse it otherwise public var paddedForBase64Encoded: String { let paddingCount = (4 - (count % 4)) % 4 diff --git a/modules/AlphaWalletCore/AlphaWalletCore/Extensions/URL+Extensions.swift b/modules/AlphaWalletCore/AlphaWalletCore/Extensions/URL+Extensions.swift index 841542cc5..24e299ccc 100644 --- a/modules/AlphaWalletCore/AlphaWalletCore/Extensions/URL+Extensions.swift +++ b/modules/AlphaWalletCore/AlphaWalletCore/Extensions/URL+Extensions.swift @@ -34,6 +34,16 @@ extension URL { } } + public var isIpfs: Bool { + if scheme == "ipfs" { + return true + } + if host == "alphawallet.infura-ipfs.io" { + return true + } + return false + } + public var rewrittenIfIpfs: URL { return rewriteIfIpfsOrNil ?? self } diff --git a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Alerts/PriceAlertDataStore.swift b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Alerts/PriceAlertDataStore.swift index 1afc54c3f..e039a7e32 100644 --- a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Alerts/PriceAlertDataStore.swift +++ b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Alerts/PriceAlertDataStore.swift @@ -32,7 +32,7 @@ public class PriceAlertDataStore: PriceAlertDataStoreType { public var alerts: [PriceAlert] { storage.value } - + private enum Keys { static func alertsKey(wallet: Wallet) -> String { return "alerts-\(wallet.address.eip55String)" diff --git a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Initializers/MigrationInitializerForOneChainPerDatabase.swift b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Initializers/MigrationInitializerForOneChainPerDatabase.swift index 832da005b..321a205ae 100644 --- a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Initializers/MigrationInitializerForOneChainPerDatabase.swift +++ b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Initializers/MigrationInitializerForOneChainPerDatabase.swift @@ -72,7 +72,7 @@ public class MigrationInitializerForOneChainPerDatabase: Initializer { guard let oldObject = oldObject else { return } guard let newObject = newObject else { return } if let contract = (oldObject["contract"] as? String).flatMap({ AlphaWallet.Address(uncheckedAgainstNullAddress: $0) }), let type = (oldObject["rawType"] as? String).flatMap({ TokenType(rawValue: $0) }) { - let tokenTypeName = XMLHandler(contract: contract, tokenType: type, assetDefinitionStore: strongSelf.assetDefinitionStore).getLabel(fallback: "") + let tokenTypeName = strongSelf.assetDefinitionStore.xmlHandler(forContract: contract, tokenType: type).getLabel(fallback: "") if !tokenTypeName.isEmpty { newObject["name"] = "" } diff --git a/modules/AlphaWalletFoundation/AlphaWalletFoundation/NFT/OpenSea/Types/OpenSeaNonFungibleTokenHandling.swift b/modules/AlphaWalletFoundation/AlphaWalletFoundation/NFT/OpenSea/Types/OpenSeaNonFungibleTokenHandling.swift index 06a004927..93be9c517 100644 --- a/modules/AlphaWalletFoundation/AlphaWalletFoundation/NFT/OpenSea/Types/OpenSeaNonFungibleTokenHandling.swift +++ b/modules/AlphaWalletFoundation/AlphaWalletFoundation/NFT/OpenSea/Types/OpenSeaNonFungibleTokenHandling.swift @@ -18,7 +18,7 @@ public enum OpenSeaBackedNonFungibleTokenHandling { public init(token: TokenScriptSupportable, assetDefinitionStore: AssetDefinitionStore, tokenViewType: TokenView) { self = { if !token.balanceNft.isEmpty && token.balanceNft[0].balance.hasPrefix("{") { - let xmlHandler = XMLHandler(contract: token.contractAddress, tokenType: token.type, assetDefinitionStore: assetDefinitionStore) + let xmlHandler = assetDefinitionStore.xmlHandler(forContract: token.contractAddress, tokenType: token.type) let view: String switch tokenViewType { case .viewIconified: diff --git a/modules/AlphaWalletFoundation/AlphaWalletFoundation/RPC/CallSmartContractFunction.swift b/modules/AlphaWalletFoundation/AlphaWalletFoundation/RPC/CallSmartContractFunction.swift index af855abf4..1139210f6 100644 --- a/modules/AlphaWalletFoundation/AlphaWalletFoundation/RPC/CallSmartContractFunction.swift +++ b/modules/AlphaWalletFoundation/AlphaWalletFoundation/RPC/CallSmartContractFunction.swift @@ -101,7 +101,7 @@ public func callSmartContract(withServer server: RPCServer, contract contractAdd let promise: Promise<[String: Any]> = promiseCreator.callPromise(options: web3Options) .recover(on: callSmartContractQueue, { error -> Promise<[String: Any]> in - //NOTE: We only want to log rate limit errors above + //NOTE: We only want to log rate limit errors above guard case AlphaWalletWeb3.Web3Error.rateLimited = error else { throw error } warnLog("[API] Rate limited by RPC node server: \(server)") diff --git a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Settings/Types/Config.swift b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Settings/Types/Config.swift index 83af3d8f8..e4ec62709 100644 --- a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Settings/Types/Config.swift +++ b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Settings/Types/Config.swift @@ -192,6 +192,7 @@ public struct Config { static let homePageURL = "homePageURL" static let sendAnalyticsEnabled = "sendAnalyticsEnabled" static let sendCrashReportingEnabled = "sendCrashReportingEnabled" + static let haveMergedAttestationAndTokenTokenScriptFoldersV1 = "haveMergedAttestationAndTokenTokenScriptFoldersV1 " } public let defaults: UserDefaults @@ -218,6 +219,18 @@ public struct Config { } } + public var haveMergedAttestationAndTokenTokenScriptFoldersV1: Bool { + get { + guard let value = defaults.value(forKey: Keys.haveMergedAttestationAndTokenTokenScriptFoldersV1) as? Bool else { + return false + } + return value + } + set { + defaults.set(newValue, forKey: Keys.haveMergedAttestationAndTokenTokenScriptFoldersV1) + } + } + public var isSendCrashReportingEnabled: Bool { sendCrashReportingEnabled ?? false } diff --git a/modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/EventSourceForActivities.swift b/modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/EventSourceForActivities.swift index 583cd799c..13a088adf 100644 --- a/modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/EventSourceForActivities.swift +++ b/modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/EventSourceForActivities.swift @@ -116,7 +116,7 @@ final class EventSourceForActivities { private func map(token: Token) async -> [Token] { guard let session = sessionsProvider.session(for: token.server) else { return [] } - let xmlHandler = XMLHandler(contract: token.contractAddress, tokenType: token.type, assetDefinitionStore: assetDefinitionStore) + let xmlHandler = assetDefinitionStore.xmlHandler(forContract: token.contractAddress, tokenType: token.type) guard xmlHandler.hasAssetDefinition, let server = xmlHandler.server else { return [] } switch server { case .any: diff --git a/modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/Models/TokenScriptSupportable.swift b/modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/Models/TokenScriptSupportable.swift index d48dcceec..cee6602d0 100644 --- a/modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/Models/TokenScriptSupportable.swift +++ b/modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/Models/TokenScriptSupportable.swift @@ -23,7 +23,7 @@ public protocol TokenScriptSupportable { public extension TokenAdaptor { func title(token: TokenScriptSupportable) -> String { - let localizedNameFromAssetDefinition = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore).getLabel(fallback: token.name) + let localizedNameFromAssetDefinition = assetDefinitionStore.xmlHandler(forTokenScriptSupportable: token).getLabel(fallback: token.name) return title(token: token, localizedNameFromAssetDefinition: localizedNameFromAssetDefinition, symbol: token.symbol) } @@ -33,7 +33,7 @@ public extension TokenAdaptor { } func titleInPluralForm(token: TokenScriptSupportable) -> String { - let localizedNameFromAssetDefinition = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore).getNameInPluralForm(fallback: token.name) + let localizedNameFromAssetDefinition = assetDefinitionStore.xmlHandler(forTokenScriptSupportable: token).getNameInPluralForm(fallback: token.name) return title(token: token, localizedNameFromAssetDefinition: localizedNameFromAssetDefinition, symbol: token.symbol) } @@ -75,7 +75,7 @@ public extension TokenAdaptor { return "\(token.valueBI) (\(symbol))".uppercased() } } - let xmlHandler = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore) + let xmlHandler = assetDefinitionStore.xmlHandler(forTokenScriptSupportable: token) func _compositeTokenName(fallback: String = "") -> String { let localizedNameFromAssetDefinition = xmlHandler.getNameInPluralForm(fallback: fallback) @@ -124,7 +124,7 @@ public extension TokenAdaptor { return "\(token.valueBI) (\(symbol))".uppercased() } } - let xmlHandler = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore) + let xmlHandler = assetDefinitionStore.xmlHandler(forTokenScriptSupportable: token) func _compositeTokenName(fallback: String = "") -> String { let localizedNameFromAssetDefinition = xmlHandler.getNameInPluralForm(fallback: fallback) @@ -160,7 +160,7 @@ public extension TokenAdaptor { } func symbolInPluralForm2(token: TokenScriptSupportable) -> String { - let localizedNameFromAssetDefinition = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore).getNameInPluralForm(fallback: token.name) + let localizedNameFromAssetDefinition = assetDefinitionStore.xmlHandler(forTokenScriptSupportable: token).getNameInPluralForm(fallback: token.name) return symbol(token: token, localizedNameFromAssetDefinition: localizedNameFromAssetDefinition) } @@ -223,8 +223,8 @@ public func compositeTokenName(forContract contract: AlphaWallet.Address, fromCo return compositeName } -extension XMLHandler { - public init(token: TokenScriptSupportable, assetDefinitionStore: AssetDefinitionStoreProtocol) { - self.init(contract: token.contractAddress, tokenType: token.type, assetDefinitionStore: assetDefinitionStore) +extension TokenScriptResolver { + public func xmlHandler(forTokenScriptSupportable token: TokenScriptSupportable) -> XMLHandler { + xmlHandler(forContract: token.contractAddress, tokenType: token.type) } -} \ No newline at end of file +} diff --git a/modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/TokenScript+UI.swift b/modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/TokenScript+UI.swift index 7d1bb1b30..0d18165ad 100644 --- a/modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/TokenScript+UI.swift +++ b/modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/TokenScript+UI.swift @@ -18,13 +18,8 @@ extension TokenScript { public static func performTokenScriptAction(_ action: TokenInstanceAction, token: FoundationToken, tokenId: TokenId, tokenHolder: TokenHolder, userEntryIds: [String], fetchUserEntries: [Promise], localRefsSource: TokenScriptLocalRefsSource, assetDefinitionStore: AssetDefinitionStore, keystore: Keystore, server: RPCServer, session: WalletSession, confirmTokenScriptActionTransactionDelegate: ConfirmTokenScriptActionTransactionDelegate?, navigationController: UINavigationController) { guard action.hasTransactionFunction else { return } - let xmlHandler = XMLHandler(contract: token.contractAddress, tokenType: token.type, assetDefinitionStore: assetDefinitionStore) - let tokenLevelAttributeValues = xmlHandler.resolveAttributesBypassingCache( - withTokenIdOrEvent: tokenHolder.tokens[0].tokenIdOrEvent, - server: server, - account: session.account.address, - assetDefinitionStore: assetDefinitionStore) - + let xmlHandler = assetDefinitionStore.xmlHandler(forContract: token.contractAddress, tokenType: token.type) + let tokenLevelAttributeValues = xmlHandler.resolveAttributesBypassingCache(withTokenIdOrEvent: tokenHolder.tokens[0].tokenIdOrEvent, server: server, account: session.account.address) let resolveTokenLevelSubscribableAttributes = Array(tokenLevelAttributeValues.values).filterToSubscribables.createPromiseForSubscribeOnce() firstly { @@ -60,6 +55,7 @@ extension TokenScript { private static func resolveActionAttributeValues(action: TokenInstanceAction, withUserEntryValues userEntryValues: [AttributeId: String], tokenLevelTokenIdOriginAttributeValues: [AttributeId: AssetAttributeSyntaxValue], tokenHolder: TokenHolder, server: RPCServer, session: WalletSession, localRefsSource: TokenScriptLocalRefsSource, assetDefinitionStore: AssetDefinitionStore) -> Promise<[AttributeId: AssetInternalValue]> { //TODO Not reading/writing from/to cache here because we haven't worked out volatility of attributes yet. So we assume all attributes used by an action as volatile, have to fetch the latest //Careful to only resolve (and wait on) attributes that the smart contract function invocation is dependent on. Some action-level attributes might only be used for display + //TODO why does this resolution not go through an XMLHandler? let attributeNameValues = assetDefinitionStore.assetAttributeResolver.resolve(withTokenIdOrEvent: tokenHolder.tokens[0].tokenIdOrEvent, userEntryValues: userEntryValues, server: server, account: session.account.address, additionalValues: tokenLevelTokenIdOriginAttributeValues, localRefs: localRefsSource.localRefs, attributes: action.attributesDependencies).mapValues { $0.value } let attributes = AssetAttributeValues(attributeValues: attributeNameValues) diff --git a/modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/TokenScriptOverridesFileManager.swift b/modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/TokenScriptOverridesFileManager.swift index 376ff10da..276d99a44 100644 --- a/modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/TokenScriptOverridesFileManager.swift +++ b/modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/TokenScriptOverridesFileManager.swift @@ -60,13 +60,14 @@ public class TokenScriptOverridesFileManager { guard let overridesDirectory = overridesDirectory else { return false } //Guard against replacing the indices file. This shouldn't be possible because we should have configured the app to only accept AirDrops for files with known extensions. This is purely defensive guard url.lastPathComponent != TokenScript.indicesFileName else { + //TODO: It is OK to delete the file because it is just named like the indices file, but not actually it. It is in the inbox. But maybe we should check that the actual indices file URL isn't provided here try? fileManager.removeItem(at: url) return true } //TODO improve or remove checking here. getHoldingContracts() below already check for schema support. We might have to show the error in wallet if we keep the file instead. We are deleting the files for now let isTokenScriptOrXml: Bool - switch XMLHandler.functional.checkTokenScriptSchema(forPath: url) { + switch XMLHandler.checkTokenScriptSchema(forPath: url) { case .supportedTokenScriptVersion: isTokenScriptOrXml = true case .unsupportedTokenScriptVersion: @@ -86,10 +87,13 @@ public class TokenScriptOverridesFileManager { let destinationFileInUse = getAllOverridesInDirectory().contains(destinationFileName) do { + //TODO would this removal would trigger an unnecessary change due to other watchers? Can't we just replace the file? But used to be like that try? FileManager.default.removeItem(at: destinationFileName ) try FileManager.default.moveItem(at: url, to: destinationFileName ) if isTokenScriptOrXml, let contents = try? String(contentsOf: destinationFileName) { - if let contracts = XMLHandler.functional.getHoldingContracts(forTokenScript: contents) { + //TODO this could include support for attestation too? This is a lot like AssetDefinitionStore.handleDownloadedOfficialTokenScript() which is for official? + //TODO maybe these logic should be in somewhere else + if let contracts = XMLHandler.getHoldingContracts(forTokenScript: contents) { for (contract, chainId) in contracts { let server = RPCServer(chainID: chainId) notifyImportTokenScriptOverrides(with: .success((contract: contract, server: server, destinationFileInUse: destinationFileInUse, filename: filename))) @@ -131,6 +135,7 @@ public class TokenScriptOverridesFileManager { } public func remove(overrideFile url: URL) { + //TODO remove TokenScript files here from the UI. Hide to handle both token or contract? try? FileManager.default.removeItem(at: url) invalidateTokenScriptOverrides() } @@ -149,6 +154,7 @@ public class TokenScriptOverridesFileManager { guard let directory = overridesDirectory else { return } let watcher = DirectoryContentsWatcher.Local(path: directory.path) do { + //This is to watch when overrides directory is changed, for displaying/refreshing the list of overrides screen try watcher.start { [weak self] results in switch results { case .noChanges: break diff --git a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/AlphaWalletTokensService.swift b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/AlphaWalletTokensService.swift index 80bb86d66..fcdf17c57 100644 --- a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/AlphaWalletTokensService.swift +++ b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/AlphaWalletTokensService.swift @@ -76,6 +76,7 @@ public class AlphaWalletTokensService: TokensService { let primaryKey = TokenObject.generatePrimaryKey(fromContract: token.contractAddress, server: token.server) Task { await tokensDataStore.updateToken(primaryKey: primaryKey, action: .isHidden(isHidden)) + assetDefinitionStore.deleteXmlFileDownloadedFromOfficialRepo(forContract: token.contractAddress) } } diff --git a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/Helpers/TokenAdaptor.swift b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/Helpers/TokenAdaptor.swift index 0a240d42e..9d8d3964c 100644 --- a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/Helpers/TokenAdaptor.swift +++ b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/Helpers/TokenAdaptor.swift @@ -41,11 +41,11 @@ public struct TokenAdaptor { } public func xmlHandler(token: TokenScriptSupportable) -> XMLHandler { - return XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore) + assetDefinitionStore.xmlHandler(forTokenScriptSupportable: token) } public func xmlHandler(contract: AlphaWallet.Address, tokenType: TokenType) -> XMLHandler { - return XMLHandler(contract: contract, tokenType: tokenType, assetDefinitionStore: assetDefinitionStore) + return assetDefinitionStore.xmlHandler(forContract: contract, tokenType: tokenType) } public func tokenScriptOverrides(token: TokenScriptSupportable) -> TokenScriptOverrides { @@ -71,14 +71,9 @@ public struct TokenAdaptor { public func getTokenHolder(token: TokenScriptSupportable) -> TokenHolder { //TODO id 1 for fungibles. Might come back to bite us? let hardcodedTokenIdForFungibles = BigUInt(1) - let xmlHandler = XMLHandler(contract: token.contractAddress, tokenType: token.type, assetDefinitionStore: assetDefinitionStore) + let xmlHandler = assetDefinitionStore.xmlHandler(forContract: token.contractAddress, tokenType: token.type) //TODO Event support, if/when designed for fungibles - let values = xmlHandler.resolveAttributesBypassingCache( - withTokenIdOrEvent: .tokenId(tokenId: hardcodedTokenIdForFungibles), - server: token.server, - account: wallet.address, - assetDefinitionStore: assetDefinitionStore) - + let values = xmlHandler.resolveAttributesBypassingCache(withTokenIdOrEvent: .tokenId(tokenId: hardcodedTokenIdForFungibles), server: token.server, account: wallet.address) let tokenScriptToken = TokenScript.Token( tokenIdOrEvent: .tokenId(tokenId: hardcodedTokenIdForFungibles), tokenType: token.type, @@ -214,22 +209,9 @@ public struct TokenAdaptor { } //TODO pass lang into here - private func getToken(name: String, - symbol: String, - forTokenIdOrEvent tokenIdOrEvent: TokenIdOrEvent, - index: UInt16, - token: TokenScriptSupportable) -> TokenScript.Token { - + private func getToken(name: String, symbol: String, forTokenIdOrEvent tokenIdOrEvent: TokenIdOrEvent, index: UInt16, token: TokenScriptSupportable) -> TokenScript.Token { let xmlHandler = xmlHandler(token: token) - return xmlHandler.getToken( - name: name, - symbol: symbol, - fromTokenIdOrEvent: tokenIdOrEvent, - index: index, - inWallet: wallet.address, - server: token.server, - tokenType: token.type, - assetDefinitionStore: assetDefinitionStore) + return xmlHandler.getToken(name: name, symbol: symbol, fromTokenIdOrEvent: tokenIdOrEvent, index: index, inWallet: wallet.address, server: token.server, tokenType: token.type) } private func getFirstMatchingEvent(nonFungible: NonFungibleFromJson, token: TokenScriptSupportable, isSourcedFromEvents: Bool) async -> EventInstanceValue? { @@ -276,14 +258,8 @@ public struct TokenAdaptor { let event = await getFirstMatchingEvent(nonFungible: nonFungible, token: token, isSourcedFromEvents: isSourcedFromEvents) let tokenIdOrEvent: TokenIdOrEvent = getTokenIdOrEvent(event: event, nonFungible: nonFungible) - let xmlHandler = xmlHandler(token: token) - var values = xmlHandler.resolveAttributesBypassingCache( - withTokenIdOrEvent: tokenIdOrEvent, - server: token.server, - account: wallet.address, - assetDefinitionStore: assetDefinitionStore) - + var values = xmlHandler.resolveAttributesBypassingCache(withTokenIdOrEvent: tokenIdOrEvent, server: token.server, account: wallet.address) values.setTokenId(string: nonFungible.tokenId) if let date = nonFungible.collectionCreatedDate { //Storing as GeneralisedTime because we only support that for date/time formats in TokenScript. We are using the same `values` infrastructure diff --git a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/Logic/IsInterfaceSupported165.swift b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/Logic/IsInterfaceSupported165.swift index a5f4f6cb1..70ca4d291 100644 --- a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/Logic/IsInterfaceSupported165.swift +++ b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/Logic/IsInterfaceSupported165.swift @@ -11,6 +11,7 @@ public class IsInterfaceSupported165 { private let fileName: String private let queue = DispatchQueue(label: "org.alphawallet.swift.isInterfaceSupported165") private lazy var storage: Storage<[String: Bool]> = .init(fileName: fileName, storage: FileStorage(fileExtension: "json"), defaultValue: [:]) + private var inFlightPromises: [String: AnyPublisher] = [:] private let blockchainProvider: BlockchainProvider diff --git a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/TokensProcessingPipeline.swift b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/TokensProcessingPipeline.swift index af99b220a..e26f09609 100644 --- a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/TokensProcessingPipeline.swift +++ b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/TokensProcessingPipeline.swift @@ -60,19 +60,20 @@ public final class WalletDataProcessingPipeline: TokensProcessingPipeline { } let whenAttestationXMLChanged = assetDefinitionStore.attestationXMLChange - .receive(on: queue) - .flatMap { [tokensService] _ in - return asFuture { - await tokensService.tokens - } + .receive(on: queue) + //Essential to not block the UI because this publisher emits values too frequently, especially at launch + .throttle(for: .seconds(10), scheduler: queue, latest: true) + .flatMap { [tokensService] _ in + return asFuture { + return await tokensService.tokens } + } let whenTokensHasChanged = tokensService.tokensPublisher .dropFirst() .receive(on: queue) 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/AlphaWalletFoundation/AlphaWalletFoundation/Types/RealmConfiguration.swift b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Types/RealmConfiguration.swift index 89900a95d..5c39b60a5 100644 --- a/modules/AlphaWalletFoundation/AlphaWalletFoundation/Types/RealmConfiguration.swift +++ b/modules/AlphaWalletFoundation/AlphaWalletFoundation/Types/RealmConfiguration.swift @@ -53,7 +53,7 @@ public struct RealmConfiguration { } return config } - + private static func _RLMRealmPathInCacheFolderForFile(_ fileName: String) throws -> String { let fileManager = FileManager.default @@ -85,7 +85,7 @@ public struct RealmConfiguration { } extension FileManager { - + public func removeAllItems(directory: URL) { do { let urls = try contentsOfDirectory(at: directory, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/AttestationVerificationStatus.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/AttestationVerificationStatus.swift new file mode 100644 index 000000000..a6e9b43dd --- /dev/null +++ b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/AttestationVerificationStatus.swift @@ -0,0 +1,23 @@ +// Copyright © 2023 Stormbird PTE. LTD. + +import Foundation +import AlphaWalletAttestation +import AlphaWalletTokenScript + +public enum AttestationVerificationStatus { + case trustedIssuer + case tokenScriptHasMatchingIssuer + case untrustedIssuer +} + +public func computeVerificationStatus(forAttestation attestation: Attestation, xmlHandler: XMLHandler?) -> AttestationVerificationStatus { + if attestation.isValidAttestationIssuer { + return .trustedIssuer + } else { + if xmlHandler == nil { + return .untrustedIssuer + } else { + return .tokenScriptHasMatchingIssuer + } + } +} diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetAttribute.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetAttribute.swift index 88a280290..502b639e4 100644 --- a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetAttribute.swift +++ b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetAttribute.swift @@ -130,7 +130,7 @@ public struct AssetAttribute { private static func getContract(fromEthereumFunctionElement ethereumFunctionElement: XMLElement, forTokenContract contract: AlphaWallet.Address, server: RPCServerOrAny, contractNamesAndAddresses: [String: [(AlphaWallet.Address, RPCServer)]]) -> AlphaWallet.Address? { if let functionOriginContractName = ethereumFunctionElement["contract"].nilIfEmpty { - return XMLHandler.functional.getNonTokenHoldingContract(byName: functionOriginContractName, server: server, fromContractNamesAndAddresses: contractNamesAndAddresses) + return XMLHandler.getNonTokenHoldingContract(byName: functionOriginContractName, server: server, fromContractNamesAndAddresses: contractNamesAndAddresses) } else { //TODO falling back to the token contract should only be for activity cards return contract diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionBackingStore.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionBackingStore.swift index 077fb5108..10b4852f6 100644 --- a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionBackingStore.swift +++ b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionBackingStore.swift @@ -2,26 +2,33 @@ import Foundation import AlphaWalletAddress +import AlphaWalletAttestation public protocol AssetDefinitionBackingStore: AnyObject { var delegate: AssetDefinitionBackingStoreDelegate? { get set } - var badTokenScriptFileNames: [TokenScriptFileIndices.FileName] { get } - var conflictingTokenScriptFileNames: (official: [TokenScriptFileIndices.FileName], overrides: [TokenScriptFileIndices.FileName], all: [TokenScriptFileIndices.FileName]) { get } - var contractsWithTokenScriptFileFromOfficialRepo: [AlphaWallet.Address] { get } + var resolver: TokenScriptResolver? { get set } + var badTokenScriptFileNames: [Filename] { get } + var conflictingTokenScriptFileNames: (official: [Filename], overrides: [Filename], all: [Filename]) { get } - subscript(contract: AlphaWallet.Address) -> String? { get set } + //Development/debug only + func debugGetPathToScriptUriFile(url: URL) -> URL? + func getXml(byContract contract: AlphaWallet.Address) -> String? + //TODO we might only call this for attestations at the moment, but will actually work for tokens too, as long as we form the URL + func getXml(byScriptUri url: URL) -> String? + func getXmls(bySchemaId schemaUid: Attestation.SchemaUid) -> [String] + func storeOfficialXmlForToken(_ contract: AlphaWallet.Address, xml: String, fromUrl url: URL) + func storeOfficialXmlForAttestation(_ attestation: Attestation, withURL url: URL, xml: String) + func deleteXmlFileDownloadedFromOfficialRepo(forContract contract: AlphaWallet.Address) func lastModifiedDateOfCachedAssetDefinitionFile(forContract contract: AlphaWallet.Address) -> Date? - func forEachContractWithXML(_ body: (AlphaWallet.Address) -> Void) func isOfficial(contract: AlphaWallet.Address) -> Bool func isCanonicalized(contract: AlphaWallet.Address) -> Bool func hasConflictingFile(forContract contract: AlphaWallet.Address) -> Bool - func hasOutdatedTokenScript(forContract contract: AlphaWallet.Address) -> Bool func getCacheTokenScriptSignatureVerificationType(forXmlString xmlString: String) -> TokenScriptSignatureVerificationType? func writeCacheTokenScriptSignatureVerificationType(_ verificationType: TokenScriptSignatureVerificationType, forContract contract: AlphaWallet.Address, forXmlString xmlString: String) - func deleteFileDownloadedFromOfficialRepoFor(contract: AlphaWallet.Address) } public protocol AssetDefinitionBackingStoreDelegate: AnyObject { - func invalidateAssetDefinition(forContractAndServer contractAndServer: AddressAndOptionalRPCServer) + func tokenScriptChanged(forContractAndServer contractAndServer: AddressAndOptionalRPCServer) + func tokenScriptChanged(forAttestationSchemaUid schemaUid: Attestation.SchemaUid) func badTokenScriptFilesChanged(in: AssetDefinitionBackingStore) } diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionDiskBackingStore.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionDiskBackingStore.swift index bc4a1884d..4c8d2c907 100644 --- a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionDiskBackingStore.swift +++ b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionDiskBackingStore.swift @@ -2,9 +2,12 @@ import Foundation import AlphaWalletAddress +import AlphaWalletAttestation import AlphaWalletCore +import AlphaWalletLogger +import CryptoKit -public class AssetDefinitionDiskBackingStore: AssetDefinitionBackingStore { +public class AssetDefinitionDiskBackingStore { public static let officialDirectoryName = "assetDefinitions" private let documentsDirectory = URL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]) @@ -12,7 +15,9 @@ public class AssetDefinitionDiskBackingStore: AssetDefinitionBackingStore { lazy var directory = documentsDirectory.appendingPathComponent(assetDefinitionsDirectoryName) private let isOfficial: Bool public weak var delegate: AssetDefinitionBackingStoreDelegate? + public weak var resolver: TokenScriptResolver? private var directoryWatcher: DirectoryContentsWatcherProtocol? + //Most if not all changes should be performed on copy in a functional "computeXXX()" and then returned and assigned back to this property (and persisted). Easier for maintenance private var tokenScriptFileIndices = TokenScriptFileIndices() private var cachedVersionOfXDaiBridgeTokenScript: String? @@ -20,16 +25,16 @@ public class AssetDefinitionDiskBackingStore: AssetDefinitionBackingStore { return directory.appendingPathComponent(TokenScript.indicesFileName) } - public var badTokenScriptFileNames: [TokenScriptFileIndices.FileName] { + public var badTokenScriptFileNames: [Filename] { if isOfficial { //We exclude .xml in the directory for files downloaded from the repo. Because this are based on pre 2019/04 schemas. We should just delete them - return tokenScriptFileIndices.badTokenScriptFileNames.filter { !$0.hasSuffix(".xml") } + return tokenScriptFileIndices.badTokenScriptFileNames.filter { !$0.value.hasSuffix(".xml") } } else { return tokenScriptFileIndices.badTokenScriptFileNames } } - public var conflictingTokenScriptFileNames: (official: [TokenScriptFileIndices.FileName], overrides: [TokenScriptFileIndices.FileName], all: [TokenScriptFileIndices.FileName]) { + public var conflictingTokenScriptFileNames: (official: [Filename], overrides: [Filename], all: [Filename]) { if isOfficial { return (official: tokenScriptFileIndices.conflictingTokenScriptFileNames, overrides: [], all: tokenScriptFileIndices.conflictingTokenScriptFileNames) } else { @@ -37,15 +42,12 @@ public class AssetDefinitionDiskBackingStore: AssetDefinitionBackingStore { } } - public var contractsWithTokenScriptFileFromOfficialRepo: [AlphaWallet.Address] { - guard let urls = try? FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil) else { return .init() } - return urls.compactMap { AlphaWallet.Address(string: $0.deletingPathExtension().lastPathComponent) } - } - - public init(directoryName: String = officialDirectoryName) { + public init(directoryName: String = officialDirectoryName, resetFolders: Bool) { self.assetDefinitionsDirectoryName = directoryName self.isOfficial = assetDefinitionsDirectoryName == AssetDefinitionDiskBackingStore.officialDirectoryName - + if resetFolders { + try? FileManager.default.removeItem(at: directory) + } try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) loadTokenScriptFileIndices() } @@ -55,97 +57,113 @@ public class AssetDefinitionDiskBackingStore: AssetDefinitionBackingStore { } private func loadTokenScriptFileIndices() { - let previousTokenScriptFileIndices = TokenScriptFileIndices.load(fromUrl: indicesFileUrl) ?? .init() - tokenScriptFileIndices = .init() - guard let urls = try? FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil) else { return } - - for eachUrl in urls { - guard eachUrl.pathExtension == XMLHandler.fileExtension || eachUrl.pathExtension == "xml" else { continue } - guard let contents = try? String(contentsOf: eachUrl) else { continue } - let fileName = eachUrl.lastPathComponent - //TODO don't use regex. When we finally use XMLHandler to extract entities, we have to be careful not to create AssetDefinitionStore instances within XMLHandler otherwise infinite recursion by calling this func again - if let contracts = XMLHandler.functional.getHoldingContracts(forTokenScript: contents) { - let entities = XMLHandler.functional.getEntities(forTokenScript: contents) - for (eachContract, _) in contracts { - tokenScriptFileIndices.contractsToFileNames[eachContract, default: []] += [fileName] - } - tokenScriptFileIndices.contractsToEntities[fileName] = entities - tokenScriptFileIndices.trackHash(forFile: fileName, contents: contents) - } else { - var isOldTokenScriptVersion = false - for (contract, fileNames) in previousTokenScriptFileIndices.contractsToOldTokenScriptFileNames where fileNames.contains(fileName) { - let newHash = tokenScriptFileIndices.hash(contents: contents) - if newHash == previousTokenScriptFileIndices.fileHashes[fileName] { - tokenScriptFileIndices.contractsToOldTokenScriptFileNames[contract, default: []] += [fileName] - tokenScriptFileIndices.trackHash(forFile: fileName, contents: contents) - isOldTokenScriptVersion = true - } - } - if !isOldTokenScriptVersion { - for (contract, fileNames) in previousTokenScriptFileIndices.contractsToFileNames where fileNames.contains(fileName) { - let newHash = tokenScriptFileIndices.hash(contents: contents) - if newHash == previousTokenScriptFileIndices.fileHashes[fileName] { - tokenScriptFileIndices.contractsToOldTokenScriptFileNames[contract, default: []] += [fileName] - tokenScriptFileIndices.trackHash(forFile: fileName, contents: contents) - isOldTokenScriptVersion = true - } - } - } - if !isOldTokenScriptVersion { - tokenScriptFileIndices.badTokenScriptFileNames += [fileName] - delegate?.badTokenScriptFilesChanged(in: self) - } - } + if let loaded = TokenScriptFileIndices.load(fromUrl: indicesFileUrl) { + tokenScriptFileIndices = loaded } - tokenScriptFileIndices.copySignatureVerificationTypes(previousTokenScriptFileIndices.signatureVerificationTypes) - writeIndicesToDisk() } private func writeIndicesToDisk() { tokenScriptFileIndices.write(toUrl: indicesFileUrl) } - private func localURLOfXML(for contract: AlphaWallet.Address) -> URL { - assert(isOfficial) - return directory.appendingPathComponent(filename(fromContract: contract)) + private func xmlWitEntityReferencesUnsubstituted(forContract contract: AlphaWallet.Address) -> (FileContentsHash, String)? { + if let hash = tokenScriptFileIndices.contractToHashes[contract]?.first, let contents = readXmlWithHash(hash) { + return (hash, contents) + } else { + return nil + } } - ///Only return XML contents if there is exactly 1 file that matches the contract - private func xml(forContract contract: AlphaWallet.Address) -> String? { - guard let fileName = tokenScriptFileIndices.nonConflictingFileName(forContract: contract) else { return nil } - let path = directory.appendingPathComponent(fileName) + private func readXmlWithHash(_ hash: FileContentsHash) -> String? { + let path = localUrlForXml(forHash: hash) return try? String(contentsOf: path) } - private func filename(fromContract contract: AlphaWallet.Address) -> String { - return "\(contract.eip55String).\(XMLHandler.fileExtension)" + private func localUrlForXml(forHash hash: FileContentsHash) -> URL { + if let filename = tokenScriptFileIndices.hashToOverridesFilename[hash] { + return directory.appendingPathComponent(filename) + } else { + let filename = "\(hash.value).\(XMLHandler.fileExtension)" + return directory.appendingPathComponent(filename) + } } - public subscript(contract: AlphaWallet.Address) -> String? { - get { - if TokenScript.shouldDisableTokenScriptXMLFileReads { - return nil - } - guard var xmlContents = xml(forContract: contract) else { return nil } - guard let fileName = tokenScriptFileIndices.nonConflictingFileName(forContract: contract) else { return xmlContents } - guard let entities = tokenScriptFileIndices.contractsToEntities[fileName] else { return xmlContents } - for each in entities { - //Guard against XML entity injection - guard !each.fileName.contains("/") else { continue } - let url = directory.appendingPathComponent(each.fileName) - guard let contents = try? String(contentsOf: url) else { continue } - xmlContents = (xmlContents as NSString).replacingOccurrences(of: "&\(each.name);", with: contents) + public func watchOverridesDirectoryContentsForChanges() { + precondition(!isOfficial) + guard directoryWatcher == nil else { return } + directoryWatcher = DirectoryContentsWatcher.Local(path: directory.path) + try? directoryWatcher?.start { [weak self] results in + guard let strongSelf = self else { return } + switch results { + case .noChanges: + break + case .updated(let filenames): + for each in filenames { + let file = FileChange.override(filename: Filename(value: each), directory: strongSelf.directory) + strongSelf.handleOverriddenTokenScriptFileChanged(file: file) + } } - return xmlContents } - set(xml) { - if TokenScript.shouldDisableTokenScriptXMLFileWrites { - return - } - guard let xml = xml else { return } - let path = localURLOfXML(for: contract) - try? xml.write(to: path, atomically: true, encoding: .utf8) - handleTokenScriptFileChanged(withFilename: path.lastPathComponent, changeHandler: { _ in }) + } + + private func purgeCacheFor(contractsAndServers: [AddressAndOptionalRPCServer], schemaUids: [Attestation.SchemaUid]) { + //Import to clear the signature cache (which includes conflicts) because a file which was in conflict with another earlier might no longer be + //TODO clear the cache more intelligently rather than purge it entirely. It might be hard or impossible to know which other contracts are affected + tokenScriptFileIndices.signatureVerificationTypes = .init() + for each in Array(Set(contractsAndServers)) { + delegate?.tokenScriptChanged(forContractAndServer: .init(address: each.address, server: nil)) + } + for each in schemaUids { + delegate?.tokenScriptChanged(forAttestationSchemaUid: each) + } + } + + //We don't call this for overrides since they are AirDrop-ed or some similar means where iOS writes them for us + private func writeOfficialXmlToFile(hash: FileContentsHash, xml: String) -> Bool { + precondition(isOfficial) + let path = localUrlForXml(forHash: hash) + do { + try xml.write(to: path, atomically: true, encoding: .utf8) + return true + } catch { + infoLog("[TokenScript] Writing XML to disk failed with XML length: \(xml.count) error: \(error)") + return false + } + } +} + +extension AssetDefinitionDiskBackingStore: AssetDefinitionBackingStore { + public func debugGetPathToScriptUriFile(url: URL) -> URL? { + guard let hash = tokenScriptFileIndices.urlToHash[url] else { return nil } + let path = localUrlForXml(forHash: hash) + return path + } + + public func getXml(byContract contract: AlphaWallet.Address) -> String? { + if TokenScript.shouldDisableTokenScriptXMLFileReads { + return nil + } + guard var (hash, xmlContents) = xmlWitEntityReferencesUnsubstituted(forContract: contract) else { return nil } + //entities are in official TokenScript files, only possible for overrides, but we can check, just a no-op + guard let entities = tokenScriptFileIndices.hashToEntitiesReferenced[hash] else { return xmlContents } + for each in entities { + //Guard against XML entity injection + guard !each.fileName.value.contains("/") else { continue } + let url = directory.appendingPathComponent(each.fileName.value) + guard let contents = try? String(contentsOf: url) else { continue } + xmlContents = (xmlContents as NSString).replacingOccurrences(of: "&\(each.name);", with: contents) + } + return xmlContents + } + + public func storeOfficialXmlForToken(_ contract: AlphaWallet.Address, xml: String, fromUrl url: URL) { + precondition(isOfficial) + if TokenScript.shouldDisableTokenScriptXMLFileWrites { return } + let hash = functional.hash(contents: xml) + if writeOfficialXmlToFile(hash: hash, xml: xml) { + let path = localUrlForXml(forHash: hash) + let file = FileChange.official(filename: Filename(value: path.lastPathComponent), directory: directory, fromUrl: url, fromAttestation: nil) + handleOfficialTokenScriptXmlFileChanged(file: file) } } @@ -155,144 +173,342 @@ public class AssetDefinitionDiskBackingStore: AssetDefinitionBackingStore { ///We don't bother to check if there's a conflict inside this function because if there's a conflict, the files should be ignored anyway public func isCanonicalized(contract: AlphaWallet.Address) -> Bool { - if let filename = tokenScriptFileIndices.contractsToFileNames[contract]?.first { - return filename.hasSuffix(".\(XMLHandler.fileExtension)") + if let hash = tokenScriptFileIndices.contractToHashes[contract]?.first { + let path = localUrlForXml(forHash: hash) + return path.path.hasSuffix(".\(XMLHandler.fileExtension)") } else { //We return true because then it'll be treated as needing a higher security level rather than a non-canonicalized (debug version) return true } + + return false } public func hasConflictingFile(forContract contract: AlphaWallet.Address) -> Bool { return tokenScriptFileIndices.hasConflictingFile(forContract: contract) } - public func hasOutdatedTokenScript(forContract contract: AlphaWallet.Address) -> Bool { - return !tokenScriptFileIndices.contractsToOldTokenScriptFileNames[contract].isEmpty - } - public func getCacheTokenScriptSignatureVerificationType(forXmlString xmlString: String) -> TokenScriptSignatureVerificationType? { - return tokenScriptFileIndices.signatureVerificationTypes[tokenScriptFileIndices.hash(contents: xmlString)] + return tokenScriptFileIndices.signatureVerificationTypes[functional.hash(contents: xmlString)] } public func writeCacheTokenScriptSignatureVerificationType(_ verificationType: TokenScriptSignatureVerificationType, forContract contract: AlphaWallet.Address, forXmlString xmlString: String) { - tokenScriptFileIndices.signatureVerificationTypes[tokenScriptFileIndices.hash(contents: xmlString)] = verificationType - tokenScriptFileIndices.write(toUrl: indicesFileUrl) + defer { writeIndicesToDisk() } + tokenScriptFileIndices.signatureVerificationTypes[functional.hash(contents: xmlString)] = verificationType } - //When we remove a contract from our database, we must remove the TokenScript file (from the standard repo) that is named after it because this file wouldn't be pulled from the server anymore. If the TokenScript file applies to more than 1 contract, having the outdated file around will mean 2 copies of the same file — with 1 outdated, 1 up-to-date, causing TokenScript client to see a conflict - public func deleteFileDownloadedFromOfficialRepoFor(contract: AlphaWallet.Address) { + //Must only return the last modified date for a file if it's for the current schema version otherwise, a file using the old schema might have a more recent timestamp (because it was recently downloaded) than a newer version on the server (which was not yet made available by the time the user downloaded the version with the old schema) + public func lastModifiedDateOfCachedAssetDefinitionFile(forContract contract: AlphaWallet.Address) -> Date? { + precondition(isOfficial) + let dates: [Date] = (tokenScriptFileIndices.contractToHashes[contract] ?? []).compactMap { hash in + let path = localUrlForXml(forHash: hash) + guard let lastModified = try? path.resourceValues(forKeys: [.contentModificationDateKey]) else { return nil } + guard XMLHandler.isTokenScriptSupportedSchemaVersion(path) else { return nil } + return lastModified.contentModificationDate + }.sorted() + //Defensive: if there's more than 1, we use the oldest date + return dates.first + } + + public func deleteXmlFileDownloadedFromOfficialRepo(forContract contract: AlphaWallet.Address) { guard isOfficial else { return } - let filename = self.filename(fromContract: contract) - let url = directory.appendingPathComponent(filename) - try? FileManager.default.removeItem(at: url) - tokenScriptFileIndices.removeHash(forFile: filename) - - var contractsToFileNames = tokenScriptFileIndices.contractsToFileNames - for (eachContract, eachFilenames) in tokenScriptFileIndices.contractsToFileNames where eachFilenames.contains(filename) { - var updatedFilenames = eachFilenames - updatedFilenames.removeAll { $0 == filename } - contractsToFileNames[eachContract] = updatedFilenames + guard let oldHashes = tokenScriptFileIndices.contractToHashes[contract] else { return } + for eachOldHash in oldHashes { + if let fromUrl = tokenScriptFileIndices.urlToHash.first(where: { $0.value == eachOldHash })?.key { + let filename = Filename.convertFromOfficialXmlHash(eachOldHash) + let file = FileChange.official(filename: filename, directory: directory, fromUrl: fromUrl, fromAttestation: nil) + handleOfficialTokenScriptXmlFileChanged(file: file) + } } - tokenScriptFileIndices.contractsToFileNames = contractsToFileNames - tokenScriptFileIndices.contractsToEntities[filename] = nil - tokenScriptFileIndices.removeBadTokenScriptFileName(filename) - tokenScriptFileIndices.removeOldTokenScriptFileName(filename) - writeIndicesToDisk() } - //Must only return the last modified date for a file if it's for the current schema version otherwise, a file using the old schema might have a more recent timestamp (because it was recently downloaded) than a newer version on the server (which was not yet made available by the time the user downloaded the version with the old schema) - public func lastModifiedDateOfCachedAssetDefinitionFile(forContract contract: AlphaWallet.Address) -> Date? { - assert(isOfficial) - let path = localURLOfXML(for: contract) - guard let lastModified = try? path.resourceValues(forKeys: [.contentModificationDateKey]) else { return nil } - guard XMLHandler.functional.isTokenScriptSupportedSchemaVersion(path) else { return nil } - return lastModified.contentModificationDate + public func storeOfficialXmlForAttestation(_ attestation: Attestation, withURL url: URL, xml: String) { + precondition(isOfficial) + let hash = functional.hash(contents: xml) + if writeOfficialXmlToFile(hash: hash, xml: xml) { + let path = localUrlForXml(forHash: hash) + let file = FileChange.official(filename: Filename(value: path.lastPathComponent), directory: directory, fromUrl: url, fromAttestation: attestation) + handleOfficialTokenScriptXmlFileChanged(file: file) + } + } + + public func getXml(byScriptUri url: URL) -> String? { + guard let hash = tokenScriptFileIndices.urlToHash[url] else { return nil } + return readXmlWithHash(hash) + } + + public func getXmls(bySchemaId schemaUid: Attestation.SchemaUid) -> [String] { + //TODO performance issue if there's too many (and big) files for the same schema UID. When would it happen? + return tokenScriptFileIndices.schemaUidToHashes[schemaUid]?.compactMap { readXmlWithHash($0) } ?? [] + } +} + +///For adding/removing/modifying TokenScript files (XML and non-XML) +extension AssetDefinitionDiskBackingStore { + private func handleOfficialTokenScriptXmlFileChanged(file: FileChange) { + precondition(isOfficial) + precondition(!file.isOverride) + //Official TokenScript should be .xml/.tsml and non other file types + guard file.isXml else { return } + defer { writeIndicesToDisk() } + + let schemaUidsAffected: [Attestation.SchemaUid] + let contractsAndServersAffected: [AddressAndOptionalRPCServer] + //TODO force unwrap due to cyclic references + (contractsAndServersAffected, tokenScriptFileIndices, schemaUidsAffected) = functional.computeXmlFileChanged(file: file, tokenScriptFileIndices: tokenScriptFileIndices, resolver: resolver!) + purgeCacheFor(contractsAndServers: contractsAndServersAffected, schemaUids: schemaUidsAffected) + } + + private func handleOverriddenTokenScriptFileChanged(file: FileChange) { + precondition(!isOfficial) + precondition(file.isOverride) + defer { writeIndicesToDisk() } + let contractsAndServersAffected: [AddressAndOptionalRPCServer] + let schemaUidsAffected: [Attestation.SchemaUid] + if file.isXml { + //TODO force unwrap due to cyclic references + (contractsAndServersAffected, tokenScriptFileIndices, schemaUidsAffected) = functional.computeXmlFileChanged(file: file, tokenScriptFileIndices: tokenScriptFileIndices, resolver: resolver!) + } else { + schemaUidsAffected = [] + contractsAndServersAffected = functional.computeOverriddenNonXmlFileChanged(file: file, tokenScriptFileIndices: tokenScriptFileIndices) + } + + purgeCacheFor(contractsAndServers: contractsAndServersAffected, schemaUids: schemaUidsAffected) + //TODO restore support bookkeeping bad TokenScript files? + //delegate?.badTokenScriptFilesChanged(in: self) } +} + +extension AssetDefinitionDiskBackingStore { + enum functional {} +} - public func forEachContractWithXML(_ body: (AlphaWallet.Address) -> Void) { - for (contract, _) in tokenScriptFileIndices.contractsToFileNames { - body(contract) +fileprivate extension AssetDefinitionDiskBackingStore.functional { + //Have to store at the front to "override". This is less important for scriptURIs, but essential for overrides + static func prependHashToFront(hash: FileContentsHash, list: [FileContentsHash]) -> [FileContentsHash] { + return [hash] + list + } + + static func computeOverriddenNonXmlFileChanged(file: FileChange, tokenScriptFileIndices: TokenScriptFileIndices) -> [AddressAndOptionalRPCServer] { + precondition(file.isOverride) + precondition(!file.isXml) + var contractsAndServersAffected: [AddressAndOptionalRPCServer] = [] + let affectedHashes = tokenScriptFileIndices.hashToEntitiesReferenced.filter { _, entities in entities.contains(where: { $0.fileName == file.filename }) }.keys + for each in affectedHashes { + let contracts = tokenScriptFileIndices.contractToHashes.filter { $1.contains(each) }.keys + contractsAndServersAffected.append(contentsOf: contracts.map { AddressAndOptionalRPCServer(address: $0, server: nil) }) } + return contractsAndServersAffected } - public func watchDirectoryContents(changeHandler: @escaping (AddressAndOptionalRPCServer) -> Void) { - guard directoryWatcher == nil else { return } - directoryWatcher = DirectoryContentsWatcher.Local(path: directory.path) - try? directoryWatcher?.start { [weak self] results in - guard let strongSelf = self else { return } - switch results { - case .noChanges: - break - case .updated(let filenames): - for each in filenames { - strongSelf.handleTokenScriptFileChanged(withFilename: each, changeHandler: changeHandler) - } + static func computeAttestationEffectForXmlFileDeleted(oldHash: FileContentsHash, tokenScriptFileIndices: TokenScriptFileIndices) -> (tokenScriptFileIndices: TokenScriptFileIndices, schemaUidsAffected: [Attestation.SchemaUid]) { + let schemaUidsAffected: [Attestation.SchemaUid] = tokenScriptFileIndices.schemaUidToHashes.compactMap({ + if $0.value.contains(oldHash) { + return $0.key + } else { + return nil } + }) + var indices = tokenScriptFileIndices + for eachSchemaUid in schemaUidsAffected { + let hashes = indices.schemaUidToHashes[eachSchemaUid, default: []].filter { $0 != oldHash } + indices.schemaUidToHashes[eachSchemaUid] = hashes } + return (indices, schemaUidsAffected) } - private func handleTokenScriptFileChanged(withFilename fileName: String, changeHandler: @escaping (AddressAndOptionalRPCServer) -> Void) { - let url = directory.appendingPathComponent(fileName) - var contractsAndServersAffected: [AddressAndOptionalRPCServer] - if url.pathExtension == XMLHandler.fileExtension || url.pathExtension == "xml" { - let contractsPreviouslyForThisXmlFile = tokenScriptFileIndices.contractsToFileNames.filter { _, fileNames in - return fileNames.contains(fileName) - }.map { $0.key } - for eachContract in contractsPreviouslyForThisXmlFile { - if var fileNames = tokenScriptFileIndices.contractsToFileNames[eachContract], fileNames.count > 1 { - fileNames.removeAll { $0 == fileName } - tokenScriptFileIndices.contractsToFileNames[eachContract] = fileNames - } else { - tokenScriptFileIndices.contractsToFileNames.removeValue(forKey: eachContract) + //When a file is modified, it is considered deleted + new/changed, in order to remove the old index entries + static func computeXmlFileDeleted(file: FileChange, tokenScriptFileIndices: TokenScriptFileIndices) -> (contractsAndServersAffected: [AddressAndOptionalRPCServer], tokenScriptFileIndices: TokenScriptFileIndices, schemaUidsAffected: [Attestation.SchemaUid]) { + precondition(file.isXml) + var indices = tokenScriptFileIndices + let oldHash: FileContentsHash? + let schemaUidsAffected: [Attestation.SchemaUid] + + switch file { + case .official(let filename, _, _, _): + let hash = FileContentsHash.convertFromOfficialXmlFilename(filename) + //Check if the hash was previously stored so we can tell if there is an old file or not. The reason there is no old file is this is a new file triggering this delete when there's no old file being replaced. (Updates trigger a delete because they are treated as delete+new/change) + let isOldHash = indices.urlToHash.contains(where: { $1 == hash }) + if isOldHash { + oldHash = hash + } else { + oldHash = nil + } + case .override: + oldHash = indices.hashToOverridesFilename.first(where: { $1 == file.filename })?.key + } + + if let oldHash { + indices.urlToHash = indices.urlToHash.filter({ $1 != oldHash }) + indices.hashToOverridesFilename.removeValue(forKey: oldHash) + indices.hashToEntitiesReferenced.removeValue(forKey: oldHash) + let copy = indices.schemaUidToHashes + for (k, hashes) in copy { + indices.schemaUidToHashes[k] = hashes.filter { $0 != oldHash } + } + (indices, schemaUidsAffected) = computeAttestationEffectForXmlFileDeleted(oldHash: oldHash, tokenScriptFileIndices: indices) + } else { + schemaUidsAffected = [] + } + + let contractsPreviouslyForThisXmlFile: [AlphaWallet.Address] + if let oldHash { + contractsPreviouslyForThisXmlFile = Array(indices.contractToHashes.filter({ $1.contains(oldHash) }).keys) + } else { + contractsPreviouslyForThisXmlFile = [] + } + for eachContract in contractsPreviouslyForThisXmlFile { + if var hashes = indices.contractToHashes[eachContract], hashes.count > 1, let oldHash { + hashes.removeAll { $0 == oldHash } + indices.contractToHashes[eachContract] = hashes + } else { + indices.contractToHashes.removeValue(forKey: eachContract) + } + } + return (contractsPreviouslyForThisXmlFile.map { AddressAndOptionalRPCServer(address: $0, server: nil) }, tokenScriptFileIndices: indices, schemaUidsAffected: schemaUidsAffected) + } + + static func computeNewOrUpdatedXmlFile(file: FileChange, xml: String, tokenScriptFileIndices: TokenScriptFileIndices, resolver: TokenScriptResolver) -> (contractsAndServersAffected: [AddressAndOptionalRPCServer], tokenScriptFileIndices: TokenScriptFileIndices, schemaUidsAffected: [Attestation.SchemaUid]) { + precondition(file.isXml) + var indices = tokenScriptFileIndices + var schemaUidsAffected = [Attestation.SchemaUid]() + let contractsPreviouslyForThisXmlFile: [AddressAndOptionalRPCServer] + (contractsPreviouslyForThisXmlFile, indices, schemaUidsAffected) = computeXmlFileDeleted(file: file, tokenScriptFileIndices: indices) + let contractsAndServers: [AddressAndOptionalRPCServer] + let hash = hash(contents: xml) + let hasHoldingContract: Bool + let hasAttestationSupport: Bool + if let holdingContracts: [AddressAndOptionalRPCServer] = XMLHandler.getHoldingContracts(forTokenScript: xml)?.map({ AddressAndOptionalRPCServer(address: $0.0, server: RPCServer(chainID: $0.1)) }) { + hasHoldingContract = true + contractsAndServers = holdingContracts + for eachContractAndServer in contractsAndServers { + indices.contractToHashes[eachContractAndServer.address] = prependHashToFront(hash: hash, list: indices.contractToHashes[eachContractAndServer.address, default: []]) + } + switch file { + case .official(_, _, let url, _): + indices.urlToHash[url] = hash + case .override(let filename, _): + let entities = XMLHandler.getEntities(forTokenScript: xml) + indices.hashToEntitiesReferenced[hash] = entities + if let _ = indices.hashToOverridesFilename[hash] { + //TODO we didn't delete the file, but it's probably OK } + indices.hashToOverridesFilename[hash] = filename } - tokenScriptFileIndices.contractsToEntities.removeValue(forKey: fileName) - tokenScriptFileIndices.removeHash(forFile: fileName) - - let contractsAndServers: [AddressAndOptionalRPCServer] - if let contents = try? String(contentsOf: url) { - if let holdingContracts: [AddressAndOptionalRPCServer] = XMLHandler.functional.getHoldingContracts(forTokenScript: contents)?.map({ AddressAndOptionalRPCServer(address: $0.0, server: RPCServer(chainID: $0.1)) }) { - contractsAndServers = holdingContracts - let entities = XMLHandler.functional.getEntities(forTokenScript: contents) - for eachContractAndServer in contractsAndServers { - tokenScriptFileIndices.contractsToFileNames[eachContractAndServer.address, default: []] += [fileName] - } - tokenScriptFileIndices.contractsToEntities[fileName] = entities - tokenScriptFileIndices.trackHash(forFile: fileName, contents: contents) - tokenScriptFileIndices.removeBadTokenScriptFileName(fileName) - tokenScriptFileIndices.removeOldTokenScriptFileName(fileName) + } else { + hasHoldingContract = false + contractsAndServers = [] + } + + switch file { + case .official(_, _, let url, let attestation): + if let attestation { + let xmlHandler = resolver.xmlHandler(forAttestation: attestation, xmlString: xml) + if let collectionId = xmlHandler.attestationCollectionId, let schemaUid = xmlHandler.attestationSchemaUid { + hasAttestationSupport = true + schemaUidsAffected.append(schemaUid) + indices.urlToHash[url] = hash + indices.schemaUidToHashes[schemaUid] = prependHashToFront(hash: hash, list: indices.schemaUidToHashes[schemaUid, default: []]) } else { - contractsAndServers = [] - tokenScriptFileIndices.badTokenScriptFileNames += [fileName] + hasAttestationSupport = false } + } else if let collectionId = XMLHandler.getAttestationCollectionId(xmlString: xml), let schemaUid = XMLHandler.getAttestationSchemaUid(xmlString: xml) { + hasAttestationSupport = true + schemaUidsAffected.append(schemaUid) + indices.urlToHash[url] = hash + indices.schemaUidToHashes[schemaUid] = prependHashToFront(hash: hash, list: indices.schemaUidToHashes[schemaUid, default: []]) } else { - contractsAndServers = [] - tokenScriptFileIndices.removeHash(forFile: fileName) - tokenScriptFileIndices.removeBadTokenScriptFileName(fileName) - tokenScriptFileIndices.removeOldTokenScriptFileName(fileName) + hasAttestationSupport = false } + case .override(let filename, _): + if let collectionId = XMLHandler.getAttestationCollectionId(xmlString: xml), let schemaUid = XMLHandler.getAttestationSchemaUid(xmlString: xml) { + hasAttestationSupport = true + schemaUidsAffected.append(schemaUid) + indices.schemaUidToHashes[schemaUid] = prependHashToFront(hash: hash, list: indices.schemaUidToHashes[schemaUid, default: []]) + indices.hashToOverridesFilename[hash] = filename + } else { + hasAttestationSupport = false + } + } + + if !hasHoldingContract && !hasAttestationSupport { + //TODO bad TokenScript file? Do we book keep? + } - contractsAndServersAffected = contractsAndServers + contractsPreviouslyForThisXmlFile.map { AddressAndOptionalRPCServer(address: $0, server: nil) } + var contractsAndServersAffected: [AddressAndOptionalRPCServer] = contractsAndServers + contractsPreviouslyForThisXmlFile + return (contractsAndServersAffected: contractsAndServersAffected, tokenScriptFileIndices: indices, schemaUidsAffected: schemaUidsAffected) + } + + static func computeXmlFileChanged(file: FileChange, tokenScriptFileIndices: TokenScriptFileIndices, resolver: TokenScriptResolver) -> (contractsAndServersAffected: [AddressAndOptionalRPCServer], tokenScriptFileIndices: TokenScriptFileIndices, schemaUidsAffected: [Attestation.SchemaUid]) { + precondition(file.isXml) + if let xml = file.contents { + return computeNewOrUpdatedXmlFile(file: file, xml: xml, tokenScriptFileIndices: tokenScriptFileIndices, resolver: resolver) } else { - contractsAndServersAffected = [AddressAndOptionalRPCServer]() - for (xmlFileName, entities) in tokenScriptFileIndices.contractsToEntities where entities.contains(where: { $0.fileName == fileName }) { - let contracts = tokenScriptFileIndices.contracts(inFileName: xmlFileName) - contractsAndServersAffected.append(contentsOf: contracts.map { AddressAndOptionalRPCServer(address: $0, server: nil) }) - } + return computeXmlFileDeleted(file: file, tokenScriptFileIndices: tokenScriptFileIndices) } - purgeCacheFor(contractsAndServers: contractsAndServersAffected, changeHandler: changeHandler) - writeIndicesToDisk() - delegate?.badTokenScriptFilesChanged(in: self) } - private func purgeCacheFor(contractsAndServers: [AddressAndOptionalRPCServer], changeHandler: @escaping (AddressAndOptionalRPCServer) -> Void) { - //Import to clear the signature cache (which includes conflicts) because a file which was in conflict with another earlier might no longer be - //TODO clear the cache more intelligently rather than purge it entirely. It might be hard or impossible to know which other contracts are affected - tokenScriptFileIndices.signatureVerificationTypes = .init() - for each in Array(Set(contractsAndServers)) { - delegate?.invalidateAssetDefinition(forContractAndServer: .init(address: each.address, server: nil)) - changeHandler(each) + static func hash(contents: String) -> FileContentsHash { + //TODO if hashValue changes, doesn't it mean `TokenScriptFileIndices.fileHashes` is broken? + //String.hashValue is different with each app launch, so we can't use it + let inputData = Data(contents.utf8) + let hashedData = SHA256.hash(data: inputData) + let hash: String = hashedData.compactMap { String(format: "%02x", $0) }.joined() + return FileContentsHash(value: hash) + } +} + +fileprivate enum FileChange { + //Not having fromAttestation only means it's not triggered by an attestation. Doesn't mean the TokenScript can't be applied to one + case official(filename: Filename, directory: URL, fromUrl: URL, fromAttestation: Attestation?) + case override(filename: Filename, directory: URL) + + private var localUrl: URL { + switch self { + case .official(let filename, let directory, _, _): + return directory.appendingPathComponent(filename) + case .override(let filename, let directory): + return directory.appendingPathComponent(filename) + } + } + + var isOfficial: Bool { + switch self { + case .official: + return true + case .override: + return false } } + + var isOverride: Bool { + return !isOfficial + } + + var filename: Filename { + switch self { + case .official(let filename, _, _, _): + return filename + case .override(let filename, _): + return filename + } + } + + //Useful in debugger + var attestation: Attestation? { + switch self { + case .official(_, _, _, let attestation): + return attestation + case .override: + return nil + } + } + + var contents: String? { + return try? String(contentsOf: localUrl) + } + + var isXml: Bool { + return XMLHandler.hasValidTokenScriptFileExtension(url: localUrl) + } } diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionDiskBackingStoreWithOverrides.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionDiskBackingStoreWithOverrides.swift index 66cb18d92..c7c51ae19 100644 --- a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionDiskBackingStoreWithOverrides.swift +++ b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionDiskBackingStoreWithOverrides.swift @@ -2,61 +2,69 @@ import Foundation import AlphaWalletAddress +import AlphaWalletAttestation -public class AssetDefinitionDiskBackingStoreWithOverrides: AssetDefinitionBackingStore { - private let officialStore = AssetDefinitionDiskBackingStore() +public class AssetDefinitionDiskBackingStoreWithOverrides { + public static let overridesDirectoryName = "assetDefinitionsOverrides" + + private let officialStore: AssetDefinitionBackingStore //TODO make this be a `let` private var overridesStore: AssetDefinitionBackingStore public weak var delegate: AssetDefinitionBackingStoreDelegate? - public static let overridesDirectoryName = "assetDefinitionsOverrides" + public weak var resolver: TokenScriptResolver? { + didSet { + officialStore.resolver = resolver + overridesStore.resolver = resolver + } + } + + public init(overridesStore: AssetDefinitionBackingStore? = nil, resetFolders: Bool) { + self.officialStore = AssetDefinitionDiskBackingStore(resetFolders: resetFolders) + if let overridesStore = overridesStore { + self.overridesStore = overridesStore + } else { + let store = AssetDefinitionDiskBackingStore(directoryName: AssetDefinitionDiskBackingStoreWithOverrides.overridesDirectoryName, resetFolders: resetFolders) + self.overridesStore = store + store.watchOverridesDirectoryContentsForChanges() + } - public var badTokenScriptFileNames: [TokenScriptFileIndices.FileName] { + self.officialStore.delegate = self + self.overridesStore.delegate = self + } +} + +extension AssetDefinitionDiskBackingStoreWithOverrides: AssetDefinitionBackingStore { + public var badTokenScriptFileNames: [Filename] { return officialStore.badTokenScriptFileNames + overridesStore.badTokenScriptFileNames } - public var conflictingTokenScriptFileNames: (official: [TokenScriptFileIndices.FileName], overrides: [TokenScriptFileIndices.FileName], all: [TokenScriptFileIndices.FileName]) { + public var conflictingTokenScriptFileNames: (official: [Filename], overrides: [Filename], all: [Filename]) { let official = officialStore.conflictingTokenScriptFileNames.all let overrides = overridesStore.conflictingTokenScriptFileNames.all return (official: official, overrides: overrides, all: official + overrides) } - public var contractsWithTokenScriptFileFromOfficialRepo: [AlphaWallet.Address] { - return officialStore.contractsWithTokenScriptFileFromOfficialRepo + public func debugGetPathToScriptUriFile(url: URL) -> URL? { + return officialStore.debugGetPathToScriptUriFile(url: url) } - public init(overridesStore: AssetDefinitionBackingStore? = nil) { - if let overridesStore = overridesStore { - self.overridesStore = overridesStore - } else { - let store = AssetDefinitionDiskBackingStore(directoryName: AssetDefinitionDiskBackingStoreWithOverrides.overridesDirectoryName) - self.overridesStore = store - store.watchDirectoryContents { [weak self] contractAndServer in - self?.delegate?.invalidateAssetDefinition(forContractAndServer: contractAndServer) - } - } - - self.officialStore.delegate = self - self.overridesStore.delegate = self + public func getXml(byContract contract: AlphaWallet.Address) -> String? { + return overridesStore.getXml(byContract: contract) ?? officialStore.getXml(byContract: contract) } - public subscript(contract: AlphaWallet.Address) -> String? { - get { - return overridesStore[contract] ?? officialStore[contract] - } - set(xml) { - officialStore[contract] = xml - } + public func storeOfficialXmlForToken(_ contract: AlphaWallet.Address, xml: String, fromUrl url: URL) { + officialStore.storeOfficialXmlForToken(contract, xml: xml, fromUrl: url) } public func isOfficial(contract: AlphaWallet.Address) -> Bool { - if overridesStore[contract] != nil { + if overridesStore.getXml(byContract: contract) != nil { return false } return officialStore.isOfficial(contract: contract) } public func isCanonicalized(contract: AlphaWallet.Address) -> Bool { - if overridesStore[contract] != nil { + if overridesStore.getXml(byContract: contract) != nil { return overridesStore.isCanonicalized(contract: contract) } else { return officialStore.isCanonicalized(contract: contract) @@ -73,56 +81,51 @@ public class AssetDefinitionDiskBackingStoreWithOverrides: AssetDefinitionBackin } } - public func hasOutdatedTokenScript(forContract contract: AlphaWallet.Address) -> Bool { - if overridesStore[contract] != nil { - return overridesStore.hasOutdatedTokenScript(forContract: contract) - } else { - return officialStore.hasOutdatedTokenScript(forContract: contract) - } - } - public func lastModifiedDateOfCachedAssetDefinitionFile(forContract contract: AlphaWallet.Address) -> Date? { //Even with an override, we just want to fetch the latest official version. Doesn't imply we'll use the official version return officialStore.lastModifiedDateOfCachedAssetDefinitionFile(forContract: contract) } - public func forEachContractWithXML(_ body: (AlphaWallet.Address) -> Void) { - var overriddenContracts = [AlphaWallet.Address]() - overridesStore.forEachContractWithXML { contract in - overriddenContracts.append(contract) - body(contract) - } - officialStore.forEachContractWithXML { contract in - if !overriddenContracts.contains(contract) { - body(contract) - } - } - } - public func getCacheTokenScriptSignatureVerificationType(forXmlString xmlString: String) -> TokenScriptSignatureVerificationType? { return overridesStore.getCacheTokenScriptSignatureVerificationType(forXmlString: xmlString) ?? officialStore.getCacheTokenScriptSignatureVerificationType(forXmlString: xmlString) } ///The implementation assumes that we never verifies the signature files in the official store when there's an override available public func writeCacheTokenScriptSignatureVerificationType(_ verificationType: TokenScriptSignatureVerificationType, forContract contract: AlphaWallet.Address, forXmlString xmlString: String) { - if let xml = overridesStore[contract], xml == xmlString { + if let xml = overridesStore.getXml(byContract: contract), xml == xmlString { overridesStore.writeCacheTokenScriptSignatureVerificationType(verificationType, forContract: contract, forXmlString: xmlString) return } - if let xml = officialStore[contract], xml == xmlString { + if let xml = officialStore.getXml(byContract: contract), xml == xmlString { officialStore.writeCacheTokenScriptSignatureVerificationType(verificationType, forContract: contract, forXmlString: xmlString) return } } - public func deleteFileDownloadedFromOfficialRepoFor(contract: AlphaWallet.Address) { - officialStore.deleteFileDownloadedFromOfficialRepoFor(contract: contract) + public func deleteXmlFileDownloadedFromOfficialRepo(forContract contract: AlphaWallet.Address) { + officialStore.deleteXmlFileDownloadedFromOfficialRepo(forContract: contract) + } + + public func storeOfficialXmlForAttestation(_ attestation: Attestation, withURL url: URL, xml: String) { + officialStore.storeOfficialXmlForAttestation(attestation, withURL: url, xml: xml) + } + + public func getXml(byScriptUri url: URL) -> String? { + return officialStore.getXml(byScriptUri: url) + } + + public func getXmls(bySchemaId schemaUid: Attestation.SchemaUid) -> [String] { + return overridesStore.getXmls(bySchemaId: schemaUid) + officialStore.getXmls(bySchemaId: schemaUid) } } extension AssetDefinitionDiskBackingStoreWithOverrides: AssetDefinitionBackingStoreDelegate { - public func invalidateAssetDefinition(forContractAndServer contractAndServer: AddressAndOptionalRPCServer) { - delegate?.invalidateAssetDefinition(forContractAndServer: contractAndServer) + public func tokenScriptChanged(forContractAndServer contractAndServer: AddressAndOptionalRPCServer) { + delegate?.tokenScriptChanged(forContractAndServer: contractAndServer) + } + + public func tokenScriptChanged(forAttestationSchemaUid schemaUid: Attestation.SchemaUid) { + delegate?.tokenScriptChanged(forAttestationSchemaUid: schemaUid) } public func badTokenScriptFilesChanged(in: AssetDefinitionBackingStore) { diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionInMemoryBackingStore.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionInMemoryBackingStore.swift index fd33a6c0e..85fda8c36 100644 --- a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionInMemoryBackingStore.swift +++ b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionInMemoryBackingStore.swift @@ -2,40 +2,37 @@ import Foundation import AlphaWalletAddress +import AlphaWalletAttestation public class AssetDefinitionInMemoryBackingStore: AssetDefinitionBackingStore { private var xmls = [AlphaWallet.Address: String]() public weak var delegate: AssetDefinitionBackingStoreDelegate? - public var badTokenScriptFileNames: [TokenScriptFileIndices.FileName] { + public weak var resolver: TokenScriptResolver? + public var badTokenScriptFileNames: [Filename] { return .init() } - public var conflictingTokenScriptFileNames: (official: [TokenScriptFileIndices.FileName], overrides: [TokenScriptFileIndices.FileName], all: [TokenScriptFileIndices.FileName]) { + public var conflictingTokenScriptFileNames: (official: [Filename], overrides: [Filename], all: [Filename]) { return (official: [], overrides: [], all: []) } - public var contractsWithTokenScriptFileFromOfficialRepo: [AlphaWallet.Address] { - return .init() - } public init() { } - public subscript(contract: AlphaWallet.Address) -> String? { - get { - return xmls[contract] - } - set(xml) { - //TODO validate XML signature first - xmls[contract] = xml - } - } - public func lastModifiedDateOfCachedAssetDefinitionFile(forContract contract: AlphaWallet.Address) -> Date? { + public func debugGetPathToScriptUriFile(url: URL) -> URL? { return nil } - public func forEachContractWithXML(_ body: (AlphaWallet.Address) -> Void) { - xmls.forEach { contract, _ in - body(contract) - } + public func getXml(byContract contract: AlphaWallet.Address) -> String? { + return xmls[contract] + } + + public func storeOfficialXmlForToken(_ contract: AlphaWallet.Address, xml: String, fromUrl url: URL) { + //TODO validate XML signature first + xmls[contract] = xml + } + + public func lastModifiedDateOfCachedAssetDefinitionFile(forContract contract: AlphaWallet.Address) -> Date? { + return nil } public func isOfficial(contract: AlphaWallet.Address) -> Bool { @@ -50,10 +47,6 @@ public class AssetDefinitionInMemoryBackingStore: AssetDefinitionBackingStore { return false } - public func hasOutdatedTokenScript(forContract contract: AlphaWallet.Address) -> Bool { - return false - } - public func getCacheTokenScriptSignatureVerificationType(forXmlString xmlString: String) -> TokenScriptSignatureVerificationType? { return nil } @@ -62,7 +55,18 @@ public class AssetDefinitionInMemoryBackingStore: AssetDefinitionBackingStore { //do nothing } - public func deleteFileDownloadedFromOfficialRepoFor(contract: AlphaWallet.Address) { + public func deleteXmlFileDownloadedFromOfficialRepo(forContract contract: AlphaWallet.Address) { xmls[contract] = nil } + + public func storeOfficialXmlForAttestation(_ attestation: Attestation, withURL url: URL, xml: String) { + } + + public func getXml(byScriptUri url: URL) -> String? { + return nil + } + + public func getXmls(bySchemaId schemaUid: Attestation.SchemaUid) -> [String] { + return [] + } } diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionStore.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionStore.swift index 1a8b1b423..9744150ae 100644 --- a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionStore.swift +++ b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionStore.swift @@ -2,14 +2,15 @@ import Combine import AlphaWalletAddress +import AlphaWalletAttestation import AlphaWalletCore import AlphaWalletLogger import AlphaWalletWeb3 import PromiseKit -public protocol BaseTokenScriptFilesProvider: AnyObject { - func containsTokenScriptFile(for file: XMLFile) -> Bool - func baseTokenScriptFile(for tokenType: TokenType) -> XMLFile? +fileprivate enum AttestationOrToken { + case attestation(Attestation) + case token(AlphaWallet.Address) } /// Manage access to and cache asset definition XML files @@ -30,37 +31,30 @@ public class AssetDefinitionStore: NSObject { } } - public let features: TokenScriptFeatures - + let features: TokenScriptFeatures 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 xmlHandlersForTokens: 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 var attestationXmlChangeSubject: PassthroughSubject = .init() + private var listOfBadTokenScriptFilesSubject: CurrentValueSubject<[Filename], Never> = .init([]) private let networking: AssetDefinitionNetworking private let tokenScriptStatusResolver: TokenScriptStatusResolver - private let tokenScriptFilesProvider: BaseTokenScriptFilesProvider + private let baseTokenScriptFiles: BaseTokenScriptFiles private let blockchainsProvider: BlockchainsProvider public let assetAttributeResolver: AssetAttributeResolver - public var listOfBadTokenScriptFiles: AnyPublisher<[TokenScriptFileIndices.FileName], Never> { + public var listOfBadTokenScriptFiles: AnyPublisher<[Filename], Never> { listOfBadTokenScriptFilesSubject.eraseToAnyPublisher() } - public var conflictingTokenScriptFileNames: (official: [TokenScriptFileIndices.FileName], overrides: [TokenScriptFileIndices.FileName], all: [TokenScriptFileIndices.FileName]) { + public var conflictingTokenScriptFileNames: (official: [Filename], overrides: [Filename], all: [Filename]) { return backingStore.conflictingTokenScriptFileNames } - public var contractsWithTokenScriptFileFromOfficialRepo: [AlphaWallet.Address] { - return backingStore.contractsWithTokenScriptFileFromOfficialRepo - } - public var signatureChange: AnyPublisher { signatureChangeSubject.eraseToAnyPublisher() } @@ -69,7 +63,7 @@ public class AssetDefinitionStore: NSObject { bodyChangeSubject.eraseToAnyPublisher() } - public var attestationXMLChange: AnyPublisher { + public var attestationXMLChange: AnyPublisher { attestationXmlChangeSubject.eraseToAnyPublisher() } @@ -102,22 +96,24 @@ public class AssetDefinitionStore: NSObject { .eraseToAnyPublisher() } - convenience public init(backingStore: AssetDefinitionBackingStore = AssetDefinitionDiskBackingStoreWithOverrides(), baseTokenScriptFiles: [TokenType: String] = [:], networkService: NetworkService, reachability: ReachabilityManagerProtocol = ReachabilityManager(), blockchainsProvider: BlockchainsProvider, features: TokenScriptFeatures) { - let baseTokenScriptFilesProvider: BaseTokenScriptFilesProvider = InMemoryTokenScriptFilesProvider(baseTokenScriptFiles: baseTokenScriptFiles) - let signatureVerifier = TokenScriptSignatureVerifier(tokenScriptFilesProvider: baseTokenScriptFilesProvider, networkService: networkService, features: features, reachability: reachability) - self.init(backingStore: backingStore, tokenScriptFilesProvider: baseTokenScriptFilesProvider, signatureVerifier: signatureVerifier, networkService: networkService, blockchainsProvider: blockchainsProvider, features: features) + public convenience init(backingStore optionalBackingStore: AssetDefinitionBackingStore? = nil, baseTokenScriptFiles: [TokenType: String] = [:], networkService: NetworkService, reachability: ReachabilityManagerProtocol = ReachabilityManager(), blockchainsProvider: BlockchainsProvider, features: TokenScriptFeatures, resetFolders: Bool) { + let backingStore: AssetDefinitionBackingStore = optionalBackingStore ?? AssetDefinitionDiskBackingStoreWithOverrides(resetFolders: resetFolders) + let baseTokenScriptFiles: BaseTokenScriptFiles = BaseTokenScriptFiles(baseTokenScriptFiles: baseTokenScriptFiles) + let signatureVerifier = TokenScriptSignatureVerifier(baseTokenScriptFiles: baseTokenScriptFiles, networkService: networkService, features: features, reachability: reachability) + self.init(backingStore: backingStore, baseTokenScriptFiles: baseTokenScriptFiles, signatureVerifier: signatureVerifier, networkService: networkService, blockchainsProvider: blockchainsProvider, features: features) } - public init(backingStore: AssetDefinitionBackingStore, tokenScriptFilesProvider: BaseTokenScriptFilesProvider, signatureVerifier: TokenScriptSignatureVerifieble, networkService: NetworkService, blockchainsProvider: BlockchainsProvider, features: TokenScriptFeatures) { + init(backingStore: AssetDefinitionBackingStore, baseTokenScriptFiles: BaseTokenScriptFiles, signatureVerifier: TokenScriptSignatureVerifieble, networkService: NetworkService, blockchainsProvider: BlockchainsProvider, features: TokenScriptFeatures) { self.features = features self.blockchainsProvider = blockchainsProvider self.networking = AssetDefinitionNetworking(networkService: networkService) self.backingStore = backingStore - self.tokenScriptStatusResolver = BaseTokenScriptStatusResolver(backingStore: backingStore, signatureVerifier: signatureVerifier) - self.tokenScriptFilesProvider = tokenScriptFilesProvider + self.tokenScriptStatusResolver = TokenScriptStatusResolver(backingStore: backingStore, signatureVerifier: signatureVerifier) + self.baseTokenScriptFiles = baseTokenScriptFiles assetAttributeResolver = AssetAttributeResolver(blockchainsProvider: blockchainsProvider) super.init() self.backingStore.delegate = self + self.backingStore.resolver = self listOfBadTokenScriptFilesSubject.value = backingStore.badTokenScriptFileNames + backingStore.conflictingTokenScriptFileNames.all } @@ -137,7 +133,7 @@ public class AssetDefinitionStore: NSObject { /// /// IMPLEMENTATION NOTE: Current implementation will fetch the same XML multiple times if this function is called again before the previous attempt has completed. A check (which requires tracking completion handlers) hasn't been implemented because this doesn't usually happen in practice public func fetchXML(forContract contract: AlphaWallet.Address, server: RPCServer?, useCacheAndFetch: Bool = false, completionHandler: ((Result) -> Void)? = nil) { - if useCacheAndFetch && self[contract] != nil { + if useCacheAndFetch && backingStore.getXml(byContract: contract) != nil { completionHandler?(.cached) } @@ -181,7 +177,21 @@ public class AssetDefinitionStore: NSObject { } } - public func fetchXMLForAttestation(withScriptURL url: URL) async { + //Development/debug only + public func debugFilenameHoldingAttestationScriptUri(forAttestation attestation: Attestation) -> URL? { + guard let url = attestation.scriptUri else { return nil } + return backingStore.debugGetPathToScriptUriFile(url: url) + } + + public func fetchXMLForAttestationIfScriptURL(_ attestation: Attestation) async { + guard let url = attestation.scriptUri else { return } + //Cut down on unnecessary requests that would fail anyway + if url.absoluteString == "https://script.uri" { return } + if url.absoluteString == "ipfs://" { return } + + if url.isIpfs, attestationScriptUriTokenScriptHasDownloaded(url: url) { return } + + //TODO might have to improve downloading for intermittent failures. Currently, there are no-retries so even if the user re-gains connection, they have to restart app to download let request = AssetDefinitionNetworking.GetXmlFileRequest(url: url.rewrittenIfIpfs, lastModifiedDate: nil) return await withCheckedContinuation { continuation in @@ -203,19 +213,14 @@ public class AssetDefinitionStore: NSObject { 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] { + if xml == strongSelf.backingStore.getXml(byScriptUri: 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) - + strongSelf.handleDownloadedOfficialTokenScript(fromUrl: url, xml: xml, urlSource: AttestationOrToken.attestation(attestation)) continuation.resume(returning: ()) } } @@ -224,7 +229,7 @@ public class AssetDefinitionStore: NSObject { } private func fetchXML(contract: AlphaWallet.Address, server: RPCServer?, url: URL, useCacheAndFetch: Bool = false, completionHandler: ((Result) -> Void)? = nil) { - let lastModified = lastModifiedDateOfCachedAssetDefinitionFile(forContract: contract) + let lastModified = backingStore.lastModifiedDateOfCachedAssetDefinitionFile(forContract: contract) let request = AssetDefinitionNetworking.GetXmlFileRequest(url: url, lastModifiedDate: lastModified) networking.fetchXml(request: request) @@ -240,33 +245,43 @@ public class AssetDefinitionStore: NSObject { completionHandler?(.unmodified) 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 - if xml == strongSelf[contract] { + if xml == strongSelf.backingStore.getXml(byContract: contract) { completionHandler?(.unmodified) } else if functional.isTruncatedXML(xml: xml) { strongSelf.fetchXML(forContract: contract, server: server, useCacheAndFetch: false) { result in completionHandler?(result) } } else { - strongSelf[contract] = xml - strongSelf.invalidate(forContract: contract) completionHandler?(.updated) - strongSelf.triggerBodyChangedSubscribers(forContract: contract) - strongSelf.triggerSignatureChangedSubscribers(forContract: contract) + strongSelf.handleDownloadedOfficialTokenScript(fromUrl: url, xml: xml, urlSource: AttestationOrToken.token(contract)) } } }) } - private func triggerBodyChangedSubscribers(forContract contract: AlphaWallet.Address) { - bodyChangeSubject.send(contract) - } + private func handleDownloadedOfficialTokenScript(fromUrl url: URL, xml: String, urlSource: AttestationOrToken) { + let attestation: Attestation? + let contract: AlphaWallet.Address? + switch urlSource { + case .attestation(let source): + attestation = source + contract = nil + case .token(let source): + attestation = nil + contract = source + } - private func triggerAttestationXMLChangedSubscribers(forURL url: URL) { - attestationXmlChangeSubject.send(url) - } + //TODO Should create XMLHandler, to check if (attestation), if it affects a token. And if (token), if it affects an attestation. Remember this is official TS, not override - private func triggerSignatureChangedSubscribers(forContract contract: AlphaWallet.Address) { - signatureChangeSubject.send(contract) + if let attestation { + backingStore.storeOfficialXmlForAttestation(attestation, withURL: url, xml: xml) + _tokenScriptChanged(forAttestation: attestation) + } + + if let contract { + backingStore.storeOfficialXmlForToken(contract, xml: xml, fromUrl: url) + _tokenScriptChanged(forContract: contract) + } } @objc private func fetchXMLForContractInPasteboard() { @@ -299,106 +314,156 @@ public class AssetDefinitionStore: NSObject { } } - private func lastModifiedDateOfCachedAssetDefinitionFile(forContract contract: AlphaWallet.Address) -> Date? { - return backingStore.lastModifiedDateOfCachedAssetDefinitionFile(forContract: contract) - } - - public func forEachContractWithXML(_ body: (AlphaWallet.Address) -> Void) { - backingStore.forEachContractWithXML(body) + private func attestationScriptUriTokenScriptHasDownloaded(url: URL) -> Bool { + if let xml = backingStore.getXml(byScriptUri: url), !xml.isEmpty { + return true + } else { + return false + } } - public subscript(url: URL) -> String? { - return tokenScriptForAttestationStore[url] + //Test only + func getXml(byContract contract: AlphaWallet.Address) -> String? { + return backingStore.getXml(byContract: contract) } -} -extension AssetDefinitionStore: AssetDefinitionStoreProtocol { - public subscript(contract: AlphaWallet.Address) -> String? { - get { backingStore[contract] } - set { backingStore[contract] = newValue } + //Test only + func storeOfficialXmlForToken(_ contract: AlphaWallet.Address, xml: String, fromUrl url: URL) { + backingStore.storeOfficialXmlForToken(contract, xml: xml, fromUrl: url) } - public func isOfficial(contract: AlphaWallet.Address) -> Bool { - return backingStore.isOfficial(contract: contract) + private func privateXmlHandler(forContract contract: AlphaWallet.Address) -> PrivateXMLHandler { + let xmlString = backingStore.getXml(byContract: contract) + let isOfficial = backingStore.isOfficial(contract: contract) + let isCanonicalized = backingStore.isCanonicalized(contract: contract) + return PrivateXMLHandler(contract: contract, xmlString: xmlString, baseTokenType: nil, isOfficial: isOfficial, isCanonicalized: isCanonicalized, resolver: self, tokenScriptStatusResolver: tokenScriptStatusResolver, assetAttributeResolver: assetAttributeResolver, features: features) } - public func isCanonicalized(contract: AlphaWallet.Address) -> Bool { - return backingStore.isCanonicalized(contract: contract) + //Should keep this function so we don't expose `optionalTokenType` to other callers. It usually shouldn't be `nil` + public func tokenScriptStatus(forContract contract: AlphaWallet.Address) -> Promise { + return xmlHandler(forContract: contract, optionalTokenType: nil).tokenScriptStatus } - public func getXmlHandler(for key: AlphaWallet.Address) -> PrivateXMLHandler? { - return xmlHandlers[key] - } + //private because we don't want client code creating XMLHandler(s) to be able to accidentally pass in a nil TokenType + private func xmlHandler(forContract contract: AlphaWallet.Address, optionalTokenType tokenType: TokenType?) -> XMLHandler { + var privateXMLHandler: PrivateXMLHandler + var baseXMLHandler: PrivateXMLHandler? + if let handler = xmlHandlersForTokens[contract] { + privateXMLHandler = handler + } else { + privateXMLHandler = privateXmlHandler(forContract: contract) + xmlHandlersForTokens[contract] = privateXMLHandler + } - public func set(xmlHandler: PrivateXMLHandler?, for key: AlphaWallet.Address) { - xmlHandlers[key] = xmlHandler - } + if features.isActivityEnabled, let tokenType = tokenType { + let tokenTypeForBaseXml: TokenType + if privateXMLHandler.hasValidTokenScriptFile, let tokenType = privateXMLHandler.tokenType { + tokenTypeForBaseXml = TokenType(tokenInterfaceType: tokenType) + } else { + tokenTypeForBaseXml = tokenType + } - public func getXmlHandler(forAttestationAtURL url: URL) -> PrivateXMLHandler? { - return xmlHandlersForAttestations[url] - } + let key = functional.computeBasePrivateXMLHandlerKey(forContract: contract, tokenType: tokenTypeForBaseXml) + if let handler = baseXmlHandlers[key] { + baseXMLHandler = handler + } else { + if let xml = baseTokenScriptFiles.baseTokenScriptFile(for: tokenTypeForBaseXml) { + baseXMLHandler = PrivateXMLHandler(contract: contract, xmlString: xml, baseTokenType: tokenTypeForBaseXml, isOfficial: true, isCanonicalized: true, resolver: self, tokenScriptStatusResolver: tokenScriptStatusResolver, assetAttributeResolver: assetAttributeResolver, features: features) + baseXmlHandlers[key] = baseXMLHandler + } else { + baseXMLHandler = nil + } + } + } else { + baseXMLHandler = nil + } - public func set(xmlHandler: PrivateXMLHandler?, forAttestationAtURL url: URL) { - xmlHandlersForAttestations[url] = xmlHandler + return XMLHandler(baseXMLHandler: baseXMLHandler, privateXMLHandler: privateXMLHandler) } - public func getBaseXmlHandler(for key: String) -> PrivateXMLHandler? { - baseXmlHandlers[key] + fileprivate func create(forAttestation attestation: Attestation) -> PrivateXMLHandler? { + let xmls = backingStore.getXmls(bySchemaId: attestation.schemaUid) + for xmlString in xmls where !xmlString.isEmpty { + let xmlHandler = PrivateXMLHandler(forAttestation: attestation, xmlString: xmlString, tokenScriptStatusResolver: tokenScriptStatusResolver, assetAttributeResolver: assetAttributeResolver, features: features) + if xmlHandler.attestationCollectionId == xmlHandler.computeAttestationCollectionId(forAttestation: attestation) { + return xmlHandler + } + } + if let url = attestation.scriptUri, let xmlString = backingStore.getXml(byScriptUri: url), !xmlString.isEmpty { + let xmlHandler = PrivateXMLHandler(forAttestation: attestation, xmlString: xmlString, tokenScriptStatusResolver: tokenScriptStatusResolver, assetAttributeResolver: assetAttributeResolver, features: features) + if xmlHandler.attestationCollectionId == xmlHandler.computeAttestationCollectionId(forAttestation: attestation) { + return xmlHandler + } + } + return nil } - public func setBaseXmlHandler(for key: String, baseXmlHandler: PrivateXMLHandler?) { - baseXmlHandlers[key] = baseXmlHandler + public func deleteXmlFileDownloadedFromOfficialRepo(forContract contract: AlphaWallet.Address) { + backingStore.deleteXmlFileDownloadedFromOfficialRepo(forContract: contract) } - public func invalidateSignatureStatus(forContract contract: AlphaWallet.Address) { - triggerSignatureChangedSubscribers(forContract: contract) + private func _tokenScriptChanged(forContract contract: AlphaWallet.Address) { + xmlHandlersForTokens[contract] = nil + bodyChangeSubject.send(contract) + signatureChangeSubject.send(contract) } -} -extension AssetDefinitionStore: TokenScriptStatusResolver { - public func computeTokenScriptStatus(forContract contract: AlphaWallet.Address, xmlString: String, isOfficial: Bool) -> Promise { - tokenScriptStatusResolver.computeTokenScriptStatus(forContract: contract, xmlString: xmlString, isOfficial: isOfficial) + private func _tokenScriptChanged(forAttestations attestations: [Attestation]) { + guard !attestations.isEmpty else { return } + //TODO we only want to invalidate for those using scriptURIs only and not overridden with local TokenScript files, but this is easier and the performance hit should be low + for each in attestations { + xmlHandlersForAttestations[each.attestationId] = nil + } + attestationXmlChangeSubject.send() } - public func computeTokenScriptStatus(forAttestationURL url: URL, xmlString: String) -> Promise { - //TODO: attestations+TokenScript to implement computeTokenScriptStatus - return Promise { _ in } + private func _tokenScriptChanged(forAttestation attestation: Attestation) { + _tokenScriptChanged(forAttestations: [attestation]) } } -public final class InMemoryTokenScriptFilesProvider: BaseTokenScriptFilesProvider { - private let _baseTokenScriptFiles: AtomicDictionary = .init() - - public init(baseTokenScriptFiles: [TokenType: String] = [:]) { - _baseTokenScriptFiles.set(value: baseTokenScriptFiles) - } - - public func containsTokenScriptFile(for file: XMLFile) -> Bool { - return _baseTokenScriptFiles.contains(where: { $1 == file }) +extension AssetDefinitionStore: TokenScriptResolver { + public func xmlHandler(forContract contract: AlphaWallet.Address, tokenType: TokenType) -> XMLHandler { + return xmlHandler(forContract: contract, optionalTokenType: tokenType) } - public func baseTokenScriptFile(for tokenType: TokenType) -> XMLFile? { - return _baseTokenScriptFiles[tokenType] + public func xmlHandler(forAttestation attestation: Attestation) -> XMLHandler? { + let attestationSigner = attestation.signer + var privateXMLHandler: PrivateXMLHandler + if let handler = xmlHandlersForAttestations[attestation.attestationId] { + privateXMLHandler = handler + } else { + guard let handler = create(forAttestation: attestation) else { return nil } + let issuerAddressDerivedFromTokenScriptFile = handler.attestationIssuerKey.flatMap { deriveAddressFromPublicKey($0) } + privateXMLHandler = handler + guard let issuerAddressDerivedFromTokenScriptFile, issuerAddressDerivedFromTokenScriptFile == attestationSigner else { + infoLog("[TokenScript] Mismatch issuer public key in TokenScript file: \(handler.attestationIssuerKey) derived issuer address: \(String(describing: issuerAddressDerivedFromTokenScriptFile?.eip55String)) vs attestation's signer: \(attestationSigner.eip55String)") + return nil + } + xmlHandlersForAttestations[attestation.attestationId] = privateXMLHandler + } + return XMLHandler(baseXMLHandler: nil, privateXMLHandler: privateXMLHandler) } -} -extension AssetDefinitionStore: BaseTokenScriptFilesProvider { - public func containsTokenScriptFile(for file: XMLFile) -> Bool { - return tokenScriptFilesProvider.containsTokenScriptFile(for: file) + //Must not cache the privateXMLHandler in this initializer because we are using it to "test" a freshly downloaded XML file + public func xmlHandler(forAttestation attestation: Attestation, xmlString: String) -> XMLHandler { + let privateXMLHandler = PrivateXMLHandler(forAttestation: attestation, xmlString: xmlString, tokenScriptStatusResolver: tokenScriptStatusResolver, assetAttributeResolver: assetAttributeResolver, features: features) + return XMLHandler(baseXMLHandler: nil, privateXMLHandler: privateXMLHandler) } - public func baseTokenScriptFile(for tokenType: TokenType) -> XMLFile? { - return tokenScriptFilesProvider.baseTokenScriptFile(for: tokenType) + public func invalidateSignatureStatus(forContract contract: AlphaWallet.Address) { + signatureChangeSubject.send(contract) } } extension AssetDefinitionStore: AssetDefinitionBackingStoreDelegate { - public func invalidateAssetDefinition(forContractAndServer contractAndServer: AddressAndOptionalRPCServer) { - invalidate(forContract: contractAndServer.address) - triggerBodyChangedSubscribers(forContract: contractAndServer.address) - triggerSignatureChangedSubscribers(forContract: contractAndServer.address) - //TODO check why we are fetching here. Current func gets called when on-disk changed too? - fetchXML(forContract: contractAndServer.address, server: contractAndServer.server) + public func tokenScriptChanged(forContractAndServer contractAndServer: AddressAndOptionalRPCServer) { + _tokenScriptChanged(forContract: contractAndServer.address) + } + + public func tokenScriptChanged(forAttestationSchemaUid schemaUid: Attestation.SchemaUid) { + let attestations = AttestationsStore.allAttestations().filter { $0.schemaUid == schemaUid } + _tokenScriptChanged(forAttestations: attestations) } public func badTokenScriptFilesChanged(in: AssetDefinitionBackingStore) { @@ -409,27 +474,26 @@ extension AssetDefinitionStore: AssetDefinitionBackingStoreDelegate { } } -extension AssetDefinitionStore { - func invalidate(forContract contract: AlphaWallet.Address) { - xmlHandlers[contract] = nil - } -} - extension AssetDefinitionStore { enum functional {} } fileprivate extension AssetDefinitionStore.functional { + static func isTruncatedXML(xml: String) -> Bool { + //Safety check against a truncated file download + return !xml.trimmed.hasSuffix(">") + } + + static func computeBasePrivateXMLHandlerKey(forContract contract: AlphaWallet.Address, tokenType: TokenType) -> String { + //Key cannot be just `contract`, because the type can change (from the overriding TokenScript file) + return "\(contract.eip55String)-\(tokenType.rawValue)" + } + static func urlToFetchFromTokenScriptRepo(contract: AlphaWallet.Address) -> URL? { let name = contract.eip55String let url = URL(string: TokenScript.repoServer)?.appendingPathComponent(name) return url } - - static func isTruncatedXML(xml: String) -> Bool { - //Safety check against a truncated file download - return !xml.trimmed.hasSuffix(">") - } } enum SessionError: Error { diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetInternalValue.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetInternalValue.swift index 019da1662..e089323e0 100644 --- a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetInternalValue.swift +++ b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetInternalValue.swift @@ -176,7 +176,7 @@ extension Array where Element == Subscribable { each.publisher .first() .ignoreOutput() - .sink(receiveCompletion: { state in + .sink(receiveCompletion: { _ in count += 1 guard count == self.count else { return } seal.fulfill(Void()) diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/BaseTokenScriptFiles.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/BaseTokenScriptFiles.swift new file mode 100644 index 000000000..1bf8d5370 --- /dev/null +++ b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/BaseTokenScriptFiles.swift @@ -0,0 +1,20 @@ +// Copyright © 2023 Stormbird PTE. LTD. + +import Foundation +import AlphaWalletCore + +public final class BaseTokenScriptFiles { + private let _baseTokenScriptFiles: AtomicDictionary = .init() + + public init(baseTokenScriptFiles: [TokenType: String] = [:]) { + _baseTokenScriptFiles.set(value: baseTokenScriptFiles) + } + + public func containsBaseTokenScriptFile(for file: XMLFile) -> Bool { + return _baseTokenScriptFiles.contains(where: { $1 == file }) + } + + public func baseTokenScriptFile(for tokenType: TokenType) -> XMLFile? { + return _baseTokenScriptFiles[tokenType] + } +} diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/TokenScriptFileIndices.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/TokenScriptFileIndices.swift index ceec3f7ef..3df930444 100644 --- a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/TokenScriptFileIndices.swift +++ b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/TokenScriptFileIndices.swift @@ -2,100 +2,55 @@ import Foundation import AlphaWalletAddress +import AlphaWalletAttestation import AlphaWalletCore -//TODO reduce direct access to contractsToFileNames etc except for absolute simple reads public struct TokenScriptFileIndices: Codable { - public typealias FileContentsHash = Int - public typealias FileName = String - - public struct Entity: Codable { - let name: String - let fileName: FileName - } - - public var fileHashes = [FileName: FileContentsHash]() - public var signatureVerificationTypes = [FileContentsHash: TokenScriptSignatureVerificationType]() - public var contractsToFileNames = [AlphaWallet.Address: [FileName]]() - public var contractsToEntities = [FileName: [Entity]]() - public var badTokenScriptFileNames = [FileName]() - public var contractsToOldTokenScriptFileNames = [AlphaWallet.Address: [FileName]]() - - public var conflictingTokenScriptFileNames: [FileName] { - var result = [FileName]() - for (contract, fileNames) in contractsToFileNames where nonConflictingFileName(forContract: contract) == nil { - result.append(contentsOf: fileNames) - } - return Array(Set(result)) - } - - public mutating func trackHash(forFile fileName: FileName, contents: String) { - fileHashes[fileName] = hash(contents: contents) - } - - public mutating func removeHash(forFile fileName: FileName) { - fileHashes.removeValue(forKey: fileName) - } - - public mutating func removeOldTokenScriptFileName(_ fileName: FileName) { - //To be safe, we keep a copy of the keys of the dictionary (i.e. the contracts) to avoid modifying the dictionary while iterating through it - let contracts = Array(contractsToOldTokenScriptFileNames.keys) - for each in contracts { - guard let index = contractsToOldTokenScriptFileNames[each]?.firstIndex(of: fileName) else { continue } - contractsToOldTokenScriptFileNames[each]?.remove(at: index) - } - } - - public mutating func removeBadTokenScriptFileName(_ fileName: FileName) { - guard let index = badTokenScriptFileNames.firstIndex(of: fileName) else { return } - badTokenScriptFileNames.remove(at: index) - } - - ///Return the fileName if there are no other TokenScript files for that holding contract. There can be files with the exact same contents; those are fine because a TokenScript file downloaded from the official repo can support more than one holding contract, so those 2 contracts (0x1 and 0x2) will cause 0x1.tsml and 0x2.tsml to be downloaded with the same contents. This is not considered a conflict - public func nonConflictingFileName(forContract contract: AlphaWallet.Address) -> FileName? { - guard let fileNames = contractsToFileNames[contract] else { return nil } - let uniqueHashes = Set(fileNames.map { - fileHashes[$0] - }) - if uniqueHashes.count == 1 { - return fileNames.first - } else { - return nil - } - } - - public func hasConflictingFile(forContract contract: AlphaWallet.Address) -> Bool { - if contractsToFileNames[contract].isEmpty { - return false - } else { - return nonConflictingFileName(forContract: contract) == nil - } - } - - public func contracts(inFileName fileName: FileName) -> [AlphaWallet.Address] { - return Array(contractsToFileNames.filter { _, fileNames in fileNames.contains(fileName) }.keys) - } - - public func write(toUrl url: URL) { + var urlToHash: [URL: FileContentsHash] = [:] + var hashToOverridesFilename: [FileContentsHash: Filename] = [:] + //There could more than 1 hash (file) for each contract, but we'll always use the head + var contractToHashes = [AlphaWallet.Address: [FileContentsHash]]() + var schemaUidToHashes: [Attestation.SchemaUid: [FileContentsHash]] = [:] + //Only for overridden ones + var hashToEntitiesReferenced = [FileContentsHash: [XMLHandler.Entity]]() + + //TODO restore support bookkeeping signature of TokenScript files? + var signatureVerificationTypes = [FileContentsHash: TokenScriptSignatureVerificationType]() + //TODO restore support bookkeeping bad TokenScript files? + var badTokenScriptFileNames: [Filename] = [] + //TODO restore support bookkeeping bad TokenScript files? + var conflictingTokenScriptFileNames: [Filename] = [] + + //TODO restore support bookkeeping bad TokenScript files? + func hasConflictingFile(forContract contract: AlphaWallet.Address) -> Bool { + return false + } + + func write(toUrl url: URL) { let encoder = JSONEncoder() guard let data = try? encoder.encode(self) else { return } try? data.write(to: url) } - public func hash(contents: String) -> FileContentsHash { - //The value returned by `hashValue` might be subject to change and 2 strings that has the same `hasValue` *might* not be identical, but should be good enough for now. It is much faster than other commonly available hashes and we need it to be very fast because it is called once for each file upon startup - return contents.hashValue - } - - public static func load(fromUrl url: URL) -> TokenScriptFileIndices? { + static func load(fromUrl url: URL) -> TokenScriptFileIndices? { guard let data = try? Data(contentsOf: url) else { return nil } return try? JSONDecoder().decode(TokenScriptFileIndices.self, from: data) } - public mutating func copySignatureVerificationTypes(_ oldVerificationTypes: [FileContentsHash: TokenScriptSignatureVerificationType]) { + mutating func copySignatureVerificationTypes(_ oldVerificationTypes: [FileContentsHash: TokenScriptSignatureVerificationType]) { signatureVerificationTypes = .init() - for eachHash in fileHashes.values { + for eachHash in urlToHash.values { signatureVerificationTypes[eachHash] = oldVerificationTypes[eachHash] } } } + +extension TokenScriptFileIndices { + enum functional {} +} + +extension URL { + func appendingPathComponent(_ pathComponent: Filename) -> URL { + return appendingPathComponent(pathComponent.value) + } +} \ No newline at end of file diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/TokenScriptForAttestationStore.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/TokenScriptForAttestationStore.swift deleted file mode 100644 index 4365a20d0..000000000 --- a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/TokenScriptForAttestationStore.swift +++ /dev/null @@ -1,13 +0,0 @@ -// 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 4e090eb23..3570bd0c9 100644 --- a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/TokenScriptSignatureVerifier.swift +++ b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/TokenScriptSignatureVerifier.swift @@ -3,6 +3,7 @@ import Foundation import Combine import AlphaWalletAddress +import AlphaWalletAttestation import AlphaWalletCore import AlphaWalletLogger import PromiseKit @@ -14,78 +15,8 @@ public protocol TokenScriptSignatureVerifieble { func verifyXMLSignatureViaAPI(xml: String, completion: @escaping (TokenScriptSignatureVerifier.VerifierResult) -> Void) } -public class BaseTokenScriptStatusResolver: TokenScriptStatusResolver { - - private let backingStore: AssetDefinitionBackingStore - private let signatureVerifier: TokenScriptSignatureVerifieble - - public init(backingStore: AssetDefinitionBackingStore, signatureVerifier: TokenScriptSignatureVerifieble) { - self.backingStore = backingStore - self.signatureVerifier = signatureVerifier - } - - public func computeTokenScriptStatus(forContract contract: AlphaWallet.Address, xmlString: String, isOfficial: Bool) -> Promise { - if backingStore.hasConflictingFile(forContract: contract) { - return .value(.type2BadTokenScript(isDebugMode: !isOfficial, error: .tokenScriptType2ConflictingFiles, reason: .conflictWithAnotherFile)) - } - if backingStore.hasOutdatedTokenScript(forContract: contract) { - return .value(.type2BadTokenScript(isDebugMode: !isOfficial, error: .tokenScriptType2OldSchemaVersion, reason: .oldTokenScriptVersion)) - } - if xmlString.nilIfEmpty == nil { - return .value(.type0NoTokenScript) - } - - switch XMLHandler.functional.checkTokenScriptSchema(xmlString) { - case .supportedTokenScriptVersion: - return firstly { - verificationType(forXml: xmlString) - }.then { [backingStore] verificationStatus -> Promise in - return Promise { seal in - backingStore.writeCacheTokenScriptSignatureVerificationType(verificationStatus, forContract: contract, forXmlString: xmlString) - - switch verificationStatus { - case .verified(let domainName): - seal.fulfill(.type1GoodTokenScriptSignatureGoodOrOptional(isDebugMode: !isOfficial, isSigned: true, validatedDomain: domainName, error: .tokenScriptType1SupportedAndSigned)) - case .verificationFailed: - seal.fulfill(.type2BadTokenScript(isDebugMode: !isOfficial, error: .tokenScriptType2InvalidSignature, reason: .invalidSignature)) - case .notCanonicalizedAndNotSigned: - //But should always be debug mode because we can't have a non-canonicalized XML from the official repo - seal.fulfill(.type1GoodTokenScriptSignatureGoodOrOptional(isDebugMode: !isOfficial, isSigned: false, validatedDomain: nil, error: .tokenScriptType1SupportedNotCanonicalizedAndUnsigned)) - } - } - } - case .unsupportedTokenScriptVersion(let isOld): - if isOld { - return .value(.type2BadTokenScript(isDebugMode: !isOfficial, error: .custom("type 2 or bad? Mismatch version. Old version"), reason: .oldTokenScriptVersion)) - } else { - preconditionFailure("Not expecting an unsupported and new version of TokenScript schema here") - return .value(.type2BadTokenScript(isDebugMode: !isOfficial, error: .custom("type 2 or bad? Mismatch version. Unknown schema"), reason: nil)) - } - case .unknownXml: - preconditionFailure("Not expecting an unknown XML here when checking TokenScript schema") - return .value(.type2BadTokenScript(isDebugMode: !isOfficial, error: .custom("unknown. Maybe empty invalid? Doesn't even include something that might be our schema"), reason: nil)) - case .others: - preconditionFailure("Not expecting an unknown error when checking TokenScript schema") - return .value(.type2BadTokenScript(isDebugMode: !isOfficial, error: .custom("Not XML?"), reason: nil)) - } - } - - 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) - } else { - return signatureVerifier.verificationType(forXml: xmlString) - } - } -} - public final class TokenScriptSignatureVerifier: TokenScriptSignatureVerifieble { - private let tokenScriptFilesProvider: BaseTokenScriptFilesProvider + private let baseTokenScriptFiles: BaseTokenScriptFiles private let networking: TokenScriptSignatureNetworking private let queue = DispatchQueue(label: "org.alphawallet.swift.TokenScriptSignatureVerifier") private let reachability: ReachabilityManagerProtocol @@ -107,8 +38,8 @@ public final class TokenScriptSignatureVerifier: TokenScriptSignatureVerifieble } } - public init(tokenScriptFilesProvider: BaseTokenScriptFilesProvider, networkService: NetworkService, features: TokenScriptFeatures, reachability: ReachabilityManagerProtocol = ReachabilityManager()) { - self.tokenScriptFilesProvider = tokenScriptFilesProvider + public init(baseTokenScriptFiles: BaseTokenScriptFiles, networkService: NetworkService, features: TokenScriptFeatures, reachability: ReachabilityManagerProtocol = ReachabilityManager()) { + self.baseTokenScriptFiles = baseTokenScriptFiles self.reachability = reachability self.networking = TokenScriptSignatureNetworking(networkService: networkService) self.features = features @@ -117,7 +48,7 @@ public final class TokenScriptSignatureVerifier: TokenScriptSignatureVerifieble public func verificationType(forXml xmlString: String) -> Promise { return Promise { seal in if features.isActivityEnabled { - if tokenScriptFilesProvider.containsTokenScriptFile(for: xmlString) { + if baseTokenScriptFiles.containsBaseTokenScriptFile(for: xmlString) { seal.fulfill(.verified(domainName: "*.aw.app")) return } diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/AssetDefinitionStoreProtocol.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/AssetDefinitionStoreProtocol.swift deleted file mode 100644 index d55a05676..000000000 --- a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/AssetDefinitionStoreProtocol.swift +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright © 2023 Stormbird PTE. LTD. - -import AlphaWalletAddress - -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? - func invalidateSignatureStatus(forContract contract: AlphaWallet.Address) - - var assetAttributeResolver: AssetAttributeResolver { get } -} diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/FileContentsHash.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/FileContentsHash.swift new file mode 100644 index 000000000..c6a6d15c1 --- /dev/null +++ b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/FileContentsHash.swift @@ -0,0 +1,12 @@ +// Copyright © 2023 Stormbird PTE. LTD. + +import Foundation + +struct FileContentsHash: Hashable, Codable { + let value: String + + //For official TokenScript XML files sourced from URLs, we use the hash as the filename + static func convertFromOfficialXmlFilename(_ filename: Filename) -> Self { + return Self(value: filename.value) + } +} diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/Filename.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/Filename.swift new file mode 100644 index 000000000..1597444ff --- /dev/null +++ b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/Filename.swift @@ -0,0 +1,12 @@ +// Copyright © 2023 Stormbird PTE. LTD. + +import Foundation + +public struct Filename: Hashable, Codable { + let value: String + + //For official TokenScript XML files sourced from URLs, we use the hash as the filename + static func convertFromOfficialXmlHash(_ hash: FileContentsHash) -> Self { + return Self(value: hash.value) + } +} diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/GeneralisedTime.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/GeneralisedTime.swift index 73725baca..8808bf7de 100644 --- a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/GeneralisedTime.swift +++ b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/GeneralisedTime.swift @@ -47,6 +47,10 @@ public struct GeneralisedTime: Codable { self.date = date self.timeZone = timeZone } + public init(date: Date) { + self.date = date + self.timeZone = TimeZone.current + } private static let regex = try? NSRegularExpression(pattern: "([+-])(\\d\\d)(\\d\\d)$", options: []) /// Given "20180619210000+0300", extract "+0300" and convert to seconds private static func extractTimeZoneSecondsFromGMT(string: String) -> Int? { diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/TokenScriptResolver.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/TokenScriptResolver.swift new file mode 100644 index 000000000..fe6d34883 --- /dev/null +++ b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/TokenScriptResolver.swift @@ -0,0 +1,12 @@ +// Copyright © 2023 Stormbird PTE. LTD. + +import AlphaWalletAddress +import AlphaWalletAttestation + +public protocol TokenScriptResolver: AnyObject { + func xmlHandler(forContract contract: AlphaWallet.Address, tokenType: TokenType) -> XMLHandler + func xmlHandler(forAttestation attestation: Attestation) -> XMLHandler? + func xmlHandler(forAttestation attestation: Attestation, xmlString: String) -> XMLHandler + + func invalidateSignatureStatus(forContract contract: AlphaWallet.Address) +} diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/TokenScriptStatusResolver.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/TokenScriptStatusResolver.swift index 4a0c997f8..1cc8bf070 100644 --- a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/TokenScriptStatusResolver.swift +++ b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/TokenScriptStatusResolver.swift @@ -1,9 +1,75 @@ // Copyright © 2023 Stormbird PTE. LTD. import AlphaWalletAddress +import AlphaWalletAttestation import PromiseKit -public protocol TokenScriptStatusResolver { - func computeTokenScriptStatus(forContract contract: AlphaWallet.Address, xmlString: String, isOfficial: Bool) -> Promise - func computeTokenScriptStatus(forAttestationURL url: URL, xmlString: String) -> Promise +public class TokenScriptStatusResolver { + private let backingStore: AssetDefinitionBackingStore + private let signatureVerifier: TokenScriptSignatureVerifieble + + init(backingStore: AssetDefinitionBackingStore, signatureVerifier: TokenScriptSignatureVerifieble) { + self.backingStore = backingStore + self.signatureVerifier = signatureVerifier + } + + public func computeTokenScriptStatus(forContract contract: AlphaWallet.Address, xmlString: String, isOfficial: Bool) -> Promise { + if backingStore.hasConflictingFile(forContract: contract) { + return .value(.type2BadTokenScript(isDebugMode: !isOfficial, error: .tokenScriptType2ConflictingFiles, reason: .conflictWithAnotherFile)) + } + //TODO not support tracking outdated TokenScript files anymore? + //if backingStore.hasOutdatedTokenScript(forContract: contract) { + // return .value(.type2BadTokenScript(isDebugMode: !isOfficial, error: .tokenScriptType2OldSchemaVersion, reason: .oldTokenScriptVersion)) + //} + if xmlString.nilIfEmpty == nil { + return .value(.type0NoTokenScript) + } + + switch XMLHandler.checkTokenScriptSchema(xmlString) { + case .supportedTokenScriptVersion: + return firstly { + verificationType(forXml: xmlString) + }.then { [backingStore] verificationStatus -> Promise in + return Promise { seal in + backingStore.writeCacheTokenScriptSignatureVerificationType(verificationStatus, forContract: contract, forXmlString: xmlString) + + switch verificationStatus { + case .verified(let domainName): + seal.fulfill(.type1GoodTokenScriptSignatureGoodOrOptional(isDebugMode: !isOfficial, isSigned: true, validatedDomain: domainName, error: .tokenScriptType1SupportedAndSigned)) + case .verificationFailed: + seal.fulfill(.type2BadTokenScript(isDebugMode: !isOfficial, error: .tokenScriptType2InvalidSignature, reason: .invalidSignature)) + case .notCanonicalizedAndNotSigned: + //But should always be debug mode because we can't have a non-canonicalized XML from the official repo + seal.fulfill(.type1GoodTokenScriptSignatureGoodOrOptional(isDebugMode: !isOfficial, isSigned: false, validatedDomain: nil, error: .tokenScriptType1SupportedNotCanonicalizedAndUnsigned)) + } + } + } + case .unsupportedTokenScriptVersion(let isOld): + if isOld { + return .value(.type2BadTokenScript(isDebugMode: !isOfficial, error: .custom("type 2 or bad? Mismatch version. Old version"), reason: .oldTokenScriptVersion)) + } else { + preconditionFailure("Not expecting an unsupported and new version of TokenScript schema here") + return .value(.type2BadTokenScript(isDebugMode: !isOfficial, error: .custom("type 2 or bad? Mismatch version. Unknown schema"), reason: nil)) + } + case .unknownXml: + preconditionFailure("Not expecting an unknown XML here when checking TokenScript schema") + return .value(.type2BadTokenScript(isDebugMode: !isOfficial, error: .custom("unknown. Maybe empty invalid? Doesn't even include something that might be our schema"), reason: nil)) + case .others: + preconditionFailure("Not expecting an unknown error when checking TokenScript schema") + return .value(.type2BadTokenScript(isDebugMode: !isOfficial, error: .custom("Not XML?"), reason: nil)) + } + } + + public func computeTokenScriptStatus(forAttestation attestation: Attestation, xmlString: String) -> Promise { + //TODO attestations+TokenScript to implement computeTokenScriptStatus. Note that this is about the TokenScript file. Not the attestation issuer + return Promise { _ in } + } + + private func verificationType(forXml xmlString: String) -> PromiseKit.Promise { + if let cachedVerificationType = backingStore.getCacheTokenScriptSignatureVerificationType(forXmlString: xmlString) { + return .value(cachedVerificationType) + } else { + return signatureVerifier.verificationType(forXml: xmlString) + } + } } diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Web3.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Web3.swift new file mode 100644 index 000000000..b1f9eb260 --- /dev/null +++ b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Web3.swift @@ -0,0 +1,10 @@ +// Copyright © 2023 Stormbird PTE. LTD. + +import Foundation +import AlphaWalletAddress +import AlphaWalletWeb3 + +func deriveAddressFromPublicKey(_ key: String) -> AlphaWallet.Address? { + let recoveredEthereumAddress: EthereumAddress? = Web3.Utils.publicToAddress(Data(hex: key)) + return recoveredEthereumAddress.flatMap { AlphaWallet.Address(address: $0) } +} \ No newline at end of file diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/XMLHandler+XPaths.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/XMLHandler+XPaths.swift index 438367cbd..f8ed51e6f 100644 --- a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/XMLHandler+XPaths.swift +++ b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/XMLHandler+XPaths.swift @@ -2,6 +2,7 @@ import Foundation import AlphaWalletAddress +import AlphaWalletAttestation import AlphaWalletCore import BigInt import Kanna @@ -57,6 +58,26 @@ extension XMLHandler { return element.xpath("attribute".addToXPath(namespacePrefix: xmlContext.namespacePrefix), namespaces: xmlContext.namespaces) } + static func getAttestationAttributeElements(fromAttributeElement element: XMLElement, xmlContext: XmlContext) -> XPathObject { + return element.xpath("attestation/meta/attributeField".addToXPath(namespacePrefix: xmlContext.namespacePrefix), namespaces: xmlContext.namespaces) + } + + static func getAttestationCollectionFieldElements(fromAttributeElement element: XMLElement, xmlContext: XmlContext) -> XPathObject { + return element.xpath("attestation/collectionFields/collectionField".addToXPath(namespacePrefix: xmlContext.namespacePrefix), namespaces: xmlContext.namespaces) + } + + static func getAttestationIdFieldElements(fromAttributeElement element: XMLElement, xmlContext: XmlContext) -> XPathObject { + return element.xpath("attestation/idFields/idField".addToXPath(namespacePrefix: xmlContext.namespacePrefix), namespaces: xmlContext.namespaces) + } + + static func getAttestationIssuerKey(fromAttributeElement element: XMLElement, xmlContext: XmlContext) -> String? { + return element.at_xpath("attestation/key".addToXPath(namespacePrefix: xmlContext.namespacePrefix), namespaces: xmlContext.namespaces)?.text?.trimmed + } + + static func getAttestationSchemaUid(fromAttributeElement element: XMLElement, xmlContext: XmlContext) -> Attestation.SchemaUid? { + return element.at_xpath("attestation/eas".addToXPath(namespacePrefix: xmlContext.namespacePrefix), namespaces: xmlContext.namespaces)?["schemaUID"].flatMap { Attestation.SchemaUid(value: $0) } + } + static func getActionCardAttributeElements(fromRoot root: XMLDocument, xmlContext: XmlContext) -> XPathObject { root.xpath("/token/cards/card[@type='action']/attribute".addToXPath(namespacePrefix: xmlContext.namespacePrefix), namespaces: xmlContext.namespaces) } @@ -158,6 +179,20 @@ extension XMLHandler { } } + static func getAttestationElement(fromRoot root: XMLDocument, xmlContext: XmlContext) -> XMLElement? { + return root.at_xpath("/token/attestation".addToXPath(namespacePrefix: xmlContext.namespacePrefix), namespaces: xmlContext.namespaces) + } + + static func getAttestationNameElement(fromAttestationElement element: XMLElement?, xmlContext: XmlContext) -> XMLElement? { + guard let attestationElement = element else { return nil } + return attestationElement.at_xpath("meta/name".addToXPath(namespacePrefix: xmlContext.namespacePrefix), namespaces: xmlContext.namespaces) + } + + static func getAttestationDescriptionElement(fromAttestationElement element: XMLElement?, xmlContext: XmlContext) -> XMLElement? { + guard let attestationElement = element else { return nil } + return attestationElement.at_xpath("meta/description".addToXPath(namespacePrefix: xmlContext.namespacePrefix), namespaces: xmlContext.namespaces) + } + static func getDenialString(fromElement element: XMLElement?, xmlContext: XmlContext) -> XMLElement? { guard let element = element else { return nil } if let tag = element.at_xpath("denial/string[@xml:lang='\(xmlContext.lang)']".addToXPath(namespacePrefix: xmlContext.namespacePrefix), namespaces: xmlContext.namespaces) { diff --git a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/XMLHandler.swift b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/XMLHandler.swift index 42ac5b323..8c6d89c59 100644 --- a/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/XMLHandler.swift +++ b/modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/XMLHandler.swift @@ -7,6 +7,7 @@ import Foundation import AlphaWalletAddress +import AlphaWalletAttestation import AlphaWalletCore import Kanna import PromiseKit @@ -113,8 +114,7 @@ public enum TokenLevelTokenScriptDisplayStatus { public class PrivateXMLHandler { enum Target { case token(AlphaWallet.Address) - //TODO: attestations+TokenScript to implement. Is the key the script's URL? - case attestation(URL) + case attestation var isFifaTicketContract: Bool { switch self { @@ -139,6 +139,7 @@ public class PrivateXMLHandler { fileprivate static let tokenScriptNamespace = TokenScript.supportedTokenScriptNamespace private let features: TokenScriptFeatures + fileprivate let assetAttributeResolver: AssetAttributeResolver private var xml: XMLDocument private let signatureNamespacePrefix = "ds:" private let xhtmlNamespacePrefix = "xhtml:" @@ -167,7 +168,7 @@ public class PrivateXMLHandler { return holdingContractElement?["interface"].flatMap { TokenInterfaceType(rawValue: $0) } }() - fileprivate lazy var tokenType: TokenInterfaceType? = { + lazy var tokenType: TokenInterfaceType? = { var tokenType: TokenInterfaceType? threadSafe.performSync { tokenType = self._tokenType @@ -187,6 +188,9 @@ public class PrivateXMLHandler { return fields } + //See usage for why it has to be public + public lazy var _attestationFields: [AttestationAttribute] = extractFieldsForAttestation() + lazy var introductionHtmlString: String = { var introductionHtmlString: String = "" //TODO fallback to first if not found @@ -262,7 +266,7 @@ public class PrivateXMLHandler { 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 + //TODO attestations+TokenScript to implement support for `actions break } } @@ -310,7 +314,7 @@ public class PrivateXMLHandler { case .token(let contractAddress): optionalContract = contractAddress case .attestation: - //TODO: attestations+TokenScript to implement support for `activityCards` + //TODO attestations+TokenScript to implement support for `actions optionalContract = nil } } @@ -412,25 +416,36 @@ public class PrivateXMLHandler { return "en" } - //TODO maybe this should be removed. We should not use AssetDefinitionStore here because it's easy to create cyclical references and infinite loops since they refer to each other - convenience init(contract: AlphaWallet.Address, assetDefinitionStore: AssetDefinitionStoreProtocol) { - let xmlString = assetDefinitionStore[contract] - let isOfficial = assetDefinitionStore.isOfficial(contract: contract) - let isCanonicalized = assetDefinitionStore.isCanonicalized(contract: contract) - self.init(contract: contract, xmlString: xmlString, baseTokenType: nil, isOfficial: isOfficial, isCanonicalized: isCanonicalized, assetDefinitionStore: assetDefinitionStore) - } + private lazy var attestationName: String? = { + let attestationElement = XMLHandler.getAttestationElement(fromRoot: xml, xmlContext: xmlContext) + return XMLHandler.getAttestationNameElement(fromAttestationElement: attestationElement, xmlContext: xmlContext)?.text + }() - convenience init(contract: AlphaWallet.Address, baseXml: String, baseTokenType: TokenType, assetDefinitionStore: AssetDefinitionStoreProtocol) { - self.init(contract: contract, xmlString: baseXml, baseTokenType: baseTokenType, isOfficial: true, isCanonicalized: true, assetDefinitionStore: assetDefinitionStore) - } + private lazy var attestationDescription: String? = { + let attestationElement = XMLHandler.getAttestationElement(fromRoot: xml, xmlContext: xmlContext) + return XMLHandler.getAttestationDescriptionElement(fromAttestationElement: attestationElement, xmlContext: xmlContext)?.text + }() + + lazy var attestationIssuerKey: String? = { + return functional.getAttestationIssuerKey(xml: xml, xmlContext: xmlContext) + }() + + lazy var attestationCollectionId: String? = { + return functional.computeAttestationCollectionId(xml: xml, xmlContext: xmlContext) + }() + + lazy var attestationSchemaUid: Attestation.SchemaUid? = { + return functional.getAttestationSchemaUid(xml: xml, xmlContext: xmlContext) + }() - private init(contract: AlphaWallet.Address, xmlString: String?, baseTokenType: TokenType?, isOfficial: Bool, isCanonicalized: Bool, assetDefinitionStore: AssetDefinitionStoreProtocol) { + init(contract: AlphaWallet.Address, xmlString: String?, baseTokenType: TokenType?, isOfficial: Bool, isCanonicalized: Bool, resolver: TokenScriptResolver, tokenScriptStatusResolver: TokenScriptStatusResolver, assetAttributeResolver: AssetAttributeResolver, features: TokenScriptFeatures) { let xmlString = xmlString ?? "" self.target = Target.token(contract) self.isOfficial = isOfficial self.isCanonicalized = isCanonicalized self.baseTokenType = baseTokenType - self.features = assetDefinitionStore.features + self.features = features + self.assetAttributeResolver = assetAttributeResolver var _xml: XMLDocument! var _tokenScriptStatus: Promise! @@ -438,14 +453,14 @@ public class PrivateXMLHandler { var _server: RPCServerOrAny? let _xmlContext = xmlContext let _isBase = baseTokenType != nil - let features = self.features + let shouldLoadTokenScriptWithFailedSignatures = features.shouldLoadTokenScriptWithFailedSignatures 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(forContract: contract, xmlString: xmlString, isOfficial: isOfficial) + let tokenScriptStatusPromise = tokenScriptStatusResolver.computeTokenScriptStatus(forContract: contract, xmlString: xmlString, isOfficial: isOfficial) _tokenScriptStatus = tokenScriptStatusPromise if let tokenScriptStatus = tokenScriptStatusPromise.value { - let (xml, hasValidTokenScriptFile) = PrivateXMLHandler.storeXmlAccordingToTokenScriptStatus(xmlString: xmlString, tokenScriptStatus: tokenScriptStatus, features: features) + let (xml, hasValidTokenScriptFile) = PrivateXMLHandler.storeXmlAccordingToTokenScriptStatus(xmlString: xmlString, tokenScriptStatus: tokenScriptStatus, shouldLoadTokenScriptWithFailedSignatures: shouldLoadTokenScriptWithFailedSignatures) _xml = xml _hasValidTokenScriptFile = hasValidTokenScriptFile if _isBase { @@ -463,18 +478,18 @@ public class PrivateXMLHandler { _server = PrivateXMLHandler.extractServer(fromXML: _xml, xmlContext: _xmlContext, matchingContract: contract).flatMap { .server($0) } } tokenScriptStatusPromise.done { tokenScriptStatus in - let (xml, hasValidTokenScriptFile) = PrivateXMLHandler.storeXmlAccordingToTokenScriptStatus(xmlString: xmlString, tokenScriptStatus: tokenScriptStatus, features: features) - _xml = xml - _hasValidTokenScriptFile = hasValidTokenScriptFile - if isBase { - _server = .any - } else { - _server = PrivateXMLHandler.extractServer(fromXML: xml, xmlContext: _xmlContext, matchingContract: contract).flatMap { .server($0) } - } - if !isBase { - assetDefinitionStore.invalidateSignatureStatus(forContract: contract) - } - }.cauterize() + let (xml, hasValidTokenScriptFile) = PrivateXMLHandler.storeXmlAccordingToTokenScriptStatus(xmlString: xmlString, tokenScriptStatus: tokenScriptStatus, shouldLoadTokenScriptWithFailedSignatures: shouldLoadTokenScriptWithFailedSignatures) + _xml = xml + _hasValidTokenScriptFile = hasValidTokenScriptFile + if isBase { + _server = .any + } else { + _server = PrivateXMLHandler.extractServer(fromXML: xml, xmlContext: _xmlContext, matchingContract: contract).flatMap { .server($0) } + } + if !isBase { + resolver.invalidateSignatureStatus(forContract: contract) + } + }.cauterize() } } @@ -484,13 +499,14 @@ public class PrivateXMLHandler { self.server = _server } - private init(forAttestationURL url: URL, xmlString: String?, assetDefinitionStore: AssetDefinitionStoreProtocol) { - let xmlString = xmlString ?? "" - self.target = Target.attestation(url) + //While we pass in the attestation (we need it because we don't know the attestation's collectionId without passing it in for computation), we don't store the attestation + init(forAttestation attestation: Attestation, xmlString: String, tokenScriptStatusResolver: TokenScriptStatusResolver, assetAttributeResolver: AssetAttributeResolver, features: TokenScriptFeatures) { + self.target = Target.attestation self.isOfficial = false self.isCanonicalized = false self.baseTokenType = nil - self.features = assetDefinitionStore.features + self.features = features + self.assetAttributeResolver = assetAttributeResolver var _xml: XMLDocument! var _tokenScriptStatus: Promise! @@ -501,26 +517,23 @@ public class PrivateXMLHandler { 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) + let tokenScriptStatusPromise = tokenScriptStatusResolver.computeTokenScriptStatus(forAttestation: attestation, xmlString: xmlString) _tokenScriptStatus = tokenScriptStatusPromise if let tokenScriptStatus = tokenScriptStatusPromise.value { - let (xml, hasValidTokenScriptFile) = PrivateXMLHandler.storeXmlAccordingToTokenScriptStatus(xmlString: xmlString, tokenScriptStatus: tokenScriptStatus, features: features) + let (xml, hasValidTokenScriptFile) = PrivateXMLHandler.storeXmlAccordingToTokenScriptStatus(xmlString: xmlString, tokenScriptStatus: tokenScriptStatus, shouldLoadTokenScriptWithFailedSignatures: features.shouldLoadTokenScriptWithFailedSignatures) _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) + let (xml, hasValidTokenScriptFile) = PrivateXMLHandler.storeXmlAccordingToTokenScriptStatus(xmlString: xmlString, tokenScriptStatus: tokenScriptStatus, shouldLoadTokenScriptWithFailedSignatures: features.shouldLoadTokenScriptWithFailedSignatures) _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? + //TODO attestations+TokenScript to implement computeTokenScriptStatus. Note that this is about the TokenScript file. Not the attestation issuer is there a need to invalidate the signature status here? }.cauterize() } } @@ -531,9 +544,22 @@ public class PrivateXMLHandler { self.server = _server } - convenience init(forAttestationURL url: URL, assetDefinitionStore: AssetDefinitionStoreProtocol) { - let xmlString = assetDefinitionStore[url] - self.init(forAttestationURL: url, xmlString: xmlString, assetDefinitionStore: assetDefinitionStore) + func computeCollectionIdFieldNames(forAttestation attestation: Attestation) -> [String] { + guard let tokensElement = XMLHandler.getTokenElement(fromRoot: xml, xmlContext: xmlContext) else { return [] } + let collectionFieldElements = XMLHandler.getAttestationCollectionFieldElements(fromAttributeElement: tokensElement, xmlContext: xmlContext) + return collectionFieldElements.compactMap { $0["name"] } + } + + func computeAttestationCollectionId(forAttestation attestation: Attestation) -> String { + let collectionIdFieldNames = computeCollectionIdFieldNames(forAttestation: attestation) + let collectionIdFields: [AttestationAttribute] = collectionIdFieldNames.map { AttestationAttribute(label: $0, path: $0) } + return Attestation.computeAttestationCollectionId(forAttestation: attestation, collectionIdFields: collectionIdFields) + } + + func computeAttestationIdFieldNames(forAttestation attestation: Attestation) -> [String] { + guard let tokensElement = XMLHandler.getTokenElement(fromRoot: xml, xmlContext: xmlContext) else { return [] } + let fieldElements = XMLHandler.getAttestationIdFieldElements(fromAttributeElement: tokensElement, xmlContext: xmlContext) + return fieldElements.compactMap { $0["name"] } } private func extractHtml(fromViewElement element: XMLElement) -> (html: String, style: String) { @@ -557,7 +583,7 @@ public class PrivateXMLHandler { } } - private static func storeXmlAccordingToTokenScriptStatus(xmlString: String, tokenScriptStatus: TokenLevelTokenScriptDisplayStatus, features: TokenScriptFeatures) -> (xml: XMLDocument, hasValidTokenScriptFile: Bool) { + private static func storeXmlAccordingToTokenScriptStatus(xmlString: String, tokenScriptStatus: TokenLevelTokenScriptDisplayStatus, shouldLoadTokenScriptWithFailedSignatures: Bool) -> (xml: XMLDocument, hasValidTokenScriptFile: Bool) { let xml: XMLDocument let hasValidTokenScriptFile: Bool switch tokenScriptStatus { @@ -578,7 +604,7 @@ public class PrivateXMLHandler { hasValidTokenScriptFile = false } } else { - if features.shouldLoadTokenScriptWithFailedSignatures { + if shouldLoadTokenScriptWithFailedSignatures { xml = (try? Kanna.XML(xml: xmlString, encoding: .utf8)) ?? PrivateXMLHandler.emptyXML hasValidTokenScriptFile = true } else { @@ -590,18 +616,26 @@ public class PrivateXMLHandler { return (xml: xml, hasValidTokenScriptFile: hasValidTokenScriptFile) } - func getToken(name: String, symbol: String, fromTokenIdOrEvent tokenIdOrEvent: TokenIdOrEvent, index: UInt16, inWallet account: AlphaWallet.Address, server: RPCServer, tokenType: TokenType, assetDefinitionStore: AssetDefinitionStoreProtocol) -> TokenScript.Token { + func getToken(name: String, symbol: String, fromTokenIdOrEvent tokenIdOrEvent: TokenIdOrEvent, index: UInt16, inWallet account: AlphaWallet.Address, server: RPCServer, tokenType: TokenType) -> TokenScript.Token { guard tokenIdOrEvent.tokenId != 0 else { return .empty } let values: [AttributeId: AssetAttributeSyntaxValue] if areFieldsEmpty { values = .init() } else { //TODO read from cache again, perhaps based on a timeout/TTL for each attribute. There was a bug with reading from cache sometimes. e.g. cache a token with 8 token origin attributes and 1 function origin attribute and when displaying it and reading from the cache, sometimes it'll only return the 1 function origin attribute in the cache - values = resolveAttributesBypassingCache(withTokenIdOrEvent: tokenIdOrEvent, server: server, account: account, assetDefinitionStore: assetDefinitionStore) + values = resolveAttributesBypassingCache(withTokenIdOrEvent: tokenIdOrEvent, server: server, account: account, assetAttributeResolver: assetAttributeResolver) } return TokenScript.Token(tokenIdOrEvent: tokenIdOrEvent, tokenType: tokenType, index: index, name: name, symbol: symbol, status: .available, values: values) } + func getAttestationName() -> String? { + return attestationName + } + + func getAttestationDescription() -> String? { + return attestationDescription + } + private var areFieldsEmpty: Bool { var areFieldsEmpty: Bool = true threadSafe.performSync { @@ -611,18 +645,10 @@ public class PrivateXMLHandler { return areFieldsEmpty } - func resolveAttributesBypassingCache(withTokenIdOrEvent tokenIdOrEvent: TokenIdOrEvent, server: RPCServer, account: AlphaWallet.Address, assetDefinitionStore: AssetDefinitionStoreProtocol) -> [AttributeId: AssetAttributeSyntaxValue] { + fileprivate func resolveAttributesBypassingCache(withTokenIdOrEvent tokenIdOrEvent: TokenIdOrEvent, server: RPCServer, account: AlphaWallet.Address, assetAttributeResolver: AssetAttributeResolver) -> [AttributeId: AssetAttributeSyntaxValue] { var attributes: [AttributeId: AssetAttributeSyntaxValue] = [:] threadSafe.performSync { - attributes = assetDefinitionStore - .assetAttributeResolver - .resolve(withTokenIdOrEvent: tokenIdOrEvent, - userEntryValues: .init(), - server: server, - account: account, - additionalValues: .init(), - localRefs: .init(), - attributes: _fields) + attributes = assetAttributeResolver.resolve(withTokenIdOrEvent: tokenIdOrEvent, userEntryValues: .init(), server: server, account: account, additionalValues: .init(), localRefs: .init(), attributes: _fields) } return attributes } @@ -684,7 +710,7 @@ public class PrivateXMLHandler { private func createFunctionOriginFrom(ethereumFunctionElement: XMLElement) -> FunctionOrigin? { if let contract = ethereumFunctionElement["contract"].nilIfEmpty { guard let server = server else { return nil } - return XMLHandler.functional.getNonTokenHoldingContract(byName: contract, server: server, fromContractNamesAndAddresses: self.contractNamesAndAddresses) + return XMLHandler.getNonTokenHoldingContract(byName: contract, server: server, fromContractNamesAndAddresses: contractNamesAndAddresses) .flatMap { FunctionOrigin(forEthereumFunctionTransactionElement: ethereumFunctionElement, root: xml, originContract: $0, xmlContext: xmlContext, bitmask: nil, bitShift: 0) } } else { return XMLHandler.getRecipientAddress(fromEthereumFunctionElement: ethereumFunctionElement, xmlContext: xmlContext) @@ -713,6 +739,14 @@ public class PrivateXMLHandler { } } + private func extractFieldsForAttestation() -> [AttestationAttribute] { + if let tokensElement = XMLHandler.getTokenElement(fromRoot: xml, xmlContext: xmlContext) { + return extractFieldsForAttestation(fromElementContainingAttributes: tokensElement) + } else { + return .init() + } + } + private func extractSelectionsForToken() -> [TokenScriptSelection] { XMLHandler.getSelectionElements(fromRoot: xml, xmlContext: xmlContext).compactMap { each in guard let id = each["name"], let filter = each["filter"] else { return nil } @@ -741,11 +775,24 @@ public class PrivateXMLHandler { } 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 } - } + //TODO attributes for token and attestations are separate for now until it's necessary to combine them + return [:] + } + } + + private func extractFieldsForAttestation(fromElementContainingAttributes element: XMLElement) -> [AttestationAttribute] { + switch target { + case .token: + //TODO attributes for token and attestations are separate for now until it's necessary to combine them + return [] + case .attestation: + var fields: [AttestationAttribute] = XMLHandler + .getAttestationAttributeElements(fromAttributeElement: element, xmlContext: xmlContext) + .compactMap { + guard let path = $0["name"] else { return nil } + guard let label = $0.text else { return nil } + return AttestationAttribute(label: label, path: path) + } return fields } } @@ -787,8 +834,8 @@ public class PrivateXMLHandler { return .init(namespacePrefix: rootNamespacePrefix, namespaces: namespaces, lang: lang) } private static let regex = try? NSRegularExpression(pattern: "<\\!ENTITY\\s+(.*)\\s+SYSTEM\\s+\"(.*)\">", options: []) - fileprivate static func getEntities(inXml xml: String) -> [TokenScriptFileIndices.Entity] { - var entities = [TokenScriptFileIndices.Entity]() + fileprivate static func getEntities(inXml xml: String) -> [XMLHandler.Entity] { + var entities = [XMLHandler.Entity]() if let regex = Self.regex { regex.enumerateMatches(in: xml, options: [], range: .init(xml.startIndex.. Attestation.SchemaUid? { + guard let xml = try? Kanna.XML(xml: xmlString, encoding: .utf8) else { return nil } + let xmlContext = PrivateXMLHandler.createXmlContext(withLang: PrivateXMLHandler.lang) + return functional.getAttestationSchemaUid(xml: xml, xmlContext: xmlContext) + } + + static func getAttestationCollectionId(xmlString: String) -> String? { + guard let xml = try? Kanna.XML(xml: xmlString, encoding: .utf8) else { return nil } + let xmlContext = PrivateXMLHandler.createXmlContext(withLang: PrivateXMLHandler.lang) + return functional.computeAttestationCollectionId(xml: xml, xmlContext: xmlContext) + } + + fileprivate func resolveAttestationAttributes(forAttestation attestation: Attestation) -> [Attestation.TypeValuePair] { + return Attestation.resolveAttestationAttributes(forAttestation: attestation, withAttestationFields: _attestationFields) + } } // swiftlint:enable type_body_length +fileprivate extension PrivateXMLHandler { + enum functional {} +} + +fileprivate extension PrivateXMLHandler.functional { + static func getAttestationIssuerKey(xml: XMLDocument, xmlContext: XmlContext) -> String? { + if let tokensElement = XMLHandler.getTokenElement(fromRoot: xml, xmlContext: xmlContext) { + return XMLHandler.getAttestationIssuerKey(fromAttributeElement: tokensElement, xmlContext: xmlContext) + } else { + return nil + } + } + + static func computeAttestationCollectionId(xml: XMLDocument, xmlContext: XmlContext) -> String? { + guard let tokensElement = XMLHandler.getTokenElement(fromRoot: xml, xmlContext: xmlContext) else { return "" } + let attestationIssuerKey: String? = getAttestationIssuerKey(xml: xml, xmlContext: xmlContext) + var results: [String] = [ + Attestation.convertSignerAddressToFormatForComputingCollectionId(signer: attestationIssuerKey.flatMap { deriveAddressFromPublicKey($0) }) + ] + let collectionFieldElements = XMLHandler.getAttestationCollectionFieldElements(fromAttributeElement: tokensElement, xmlContext: xmlContext) + for each in collectionFieldElements { + if let eachText = each.text { + results.append(eachText) + } + } + let collectionId = results.joined() + if collectionId.isEmpty { + return nil + } else { + let hash = collectionId.sha3(.keccak256) + return hash + } + } + + static func getAttestationSchemaUid(xml: XMLDocument, xmlContext: XmlContext) -> Attestation.SchemaUid? { + guard let tokensElement = XMLHandler.getTokenElement(fromRoot: xml, xmlContext: xmlContext) else { return nil } + let schemaUID = XMLHandler.getAttestationSchemaUid(fromAttributeElement: tokensElement, xmlContext: xmlContext) + return schemaUID + } +} + final class ThreadSafe { private let queue: DispatchQueue @@ -826,9 +930,19 @@ final class ThreadSafe { /// This class delegates all the functionality to a singleton of the actual XML parser. 1 for each contract. So we just parse the XML file 1 time only for each contract public struct XMLHandler { + struct Entity: Codable { + let name: String + let fileName: Filename + } + + public var _attestationFields: [AttestationAttribute] { + privateXMLHandler._attestationFields + } + public static let fileExtension = "tsml" - private let privateXMLHandler: PrivateXMLHandler + //public because of cyclic dependency + public let privateXMLHandler: PrivateXMLHandler private let baseXMLHandler: PrivateXMLHandler? public var hasAssetDefinition: Bool { @@ -864,17 +978,11 @@ public struct XMLHandler { } public var tokenScriptStatus: Promise { - var tokenScriptStatus: Promise! - tokenScriptStatus = privateXMLHandler.tokenScriptStatus - - return tokenScriptStatus + return privateXMLHandler.tokenScriptStatus } public var introductionHtmlString: String { - var introductionHtmlString: String = "" - introductionHtmlString = privateXMLHandler.introductionHtmlString - - return introductionHtmlString + return privateXMLHandler.introductionHtmlString } public var tokenViewIconifiedHtml: (html: String, style: String) { @@ -1013,113 +1121,23 @@ public struct XMLHandler { """ } - public init(contract: AlphaWallet.Address, tokenType: TokenType, assetDefinitionStore: AssetDefinitionStoreProtocol) { - self.init(contract: contract, optionalTokenType: tokenType, assetDefinitionStore: assetDefinitionStore) + var attestationCollectionId: String? { + privateXMLHandler.attestationCollectionId } - 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 + var attestationSchemaUid: Attestation.SchemaUid? { + privateXMLHandler.attestationSchemaUid } - //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 - var privateXMLHandler: PrivateXMLHandler - var baseXMLHandler: PrivateXMLHandler? - if let handler = assetDefinitionStore.getXmlHandler(for: contract) { - privateXMLHandler = handler - } else { - privateXMLHandler = PrivateXMLHandler(contract: contract, assetDefinitionStore: assetDefinitionStore) - assetDefinitionStore.set(xmlHandler: privateXMLHandler, for: contract) - } - - 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 - } - + init(baseXMLHandler: PrivateXMLHandler?, privateXMLHandler: PrivateXMLHandler) { self.baseXMLHandler = baseXMLHandler self.privateXMLHandler = privateXMLHandler } - public func getToken(name: String, symbol: String, fromTokenIdOrEvent tokenIdOrEvent: TokenIdOrEvent, index: UInt16, inWallet account: AlphaWallet.Address, server: RPCServer, tokenType: TokenType, assetDefinitionStore: AssetDefinitionStoreProtocol) -> TokenScript.Token { - let overriden = privateXMLHandler.getToken( - name: name, - symbol: symbol, - fromTokenIdOrEvent: tokenIdOrEvent, - index: index, - inWallet: account, - server: server, - tokenType: tokenType, - assetDefinitionStore: assetDefinitionStore) - + public func getToken(name: String, symbol: String, fromTokenIdOrEvent tokenIdOrEvent: TokenIdOrEvent, index: UInt16, inWallet account: AlphaWallet.Address, server: RPCServer, tokenType: TokenType) -> TokenScript.Token { + let overriden = privateXMLHandler.getToken(name: name, symbol: symbol, fromTokenIdOrEvent: tokenIdOrEvent, index: index, inWallet: account, server: server, tokenType: tokenType) if let baseXMLHandler = baseXMLHandler { - let base = baseXMLHandler.getToken( - name: name, - symbol: symbol, - fromTokenIdOrEvent: tokenIdOrEvent, - index: index, - inWallet: account, - server: server, - tokenType: tokenType, - assetDefinitionStore: assetDefinitionStore) - + let base = baseXMLHandler.getToken(name: name, symbol: symbol, fromTokenIdOrEvent: tokenIdOrEvent, index: index, inWallet: account, server: server, tokenType: tokenType) let baseValues = base.values let overriddenValues = overriden.values @@ -1160,22 +1178,20 @@ public struct XMLHandler { return nameInPluralForm } - public func resolveAttributesBypassingCache(withTokenIdOrEvent tokenIdOrEvent: TokenIdOrEvent, server: RPCServer, account: AlphaWallet.Address, assetDefinitionStore: AssetDefinitionStoreProtocol) -> [AttributeId: AssetAttributeSyntaxValue] { - var attributes: [AttributeId: AssetAttributeSyntaxValue] = [:] - let overrides = privateXMLHandler.resolveAttributesBypassingCache( - withTokenIdOrEvent: tokenIdOrEvent, - server: server, - account: account, - assetDefinitionStore: assetDefinitionStore) + public func getAttestationName() -> String? { + privateXMLHandler.getAttestationName() + } + + public func getAttestationDescription() -> String? { + privateXMLHandler.getAttestationDescription() + } + public func resolveAttributesBypassingCache(withTokenIdOrEvent tokenIdOrEvent: TokenIdOrEvent, server: RPCServer, account: AlphaWallet.Address) -> [AttributeId: AssetAttributeSyntaxValue] { + var attributes: [AttributeId: AssetAttributeSyntaxValue] = [:] + let overrides = privateXMLHandler.resolveAttributesBypassingCache(withTokenIdOrEvent: tokenIdOrEvent, server: server, account: account, assetAttributeResolver: privateXMLHandler.assetAttributeResolver) if let baseXMLHandler = baseXMLHandler { //TODO This is inefficient because overridden attributes get resolved too - let base = baseXMLHandler.resolveAttributesBypassingCache( - withTokenIdOrEvent: tokenIdOrEvent, - server: server, - account: account, - assetDefinitionStore: assetDefinitionStore) - + let base = baseXMLHandler.resolveAttributesBypassingCache(withTokenIdOrEvent: tokenIdOrEvent, server: server, account: account, assetAttributeResolver: privateXMLHandler.assetAttributeResolver) attributes = base.merging(overrides) { _, new in new } } else { attributes = overrides @@ -1183,28 +1199,41 @@ public struct XMLHandler { return attributes } -} -extension XMLHandler { - public enum functional {} -} + public func computeAttestationIdentifyingFieldNames(forAttestation attestation: Attestation) -> [String] { + return privateXMLHandler.computeAttestationIdFieldNames(forAttestation: attestation) + } -extension XMLHandler.functional { + public func computeCollectionIdFieldNames(forAttestation attestation: Attestation) -> [String] { + return privateXMLHandler.computeCollectionIdFieldNames(forAttestation: attestation) + } - public static func tokenScriptStatus(forContract contract: AlphaWallet.Address, assetDefinitionStore: AssetDefinitionStoreProtocol) -> Promise { - XMLHandler(contract: contract, optionalTokenType: nil, assetDefinitionStore: assetDefinitionStore).tokenScriptStatus + public static func getAttestationSchemaUid(xmlString: String) -> Attestation.SchemaUid? { + return PrivateXMLHandler.getAttestationSchemaUid(xmlString: xmlString) } - public static func getNonTokenHoldingContract(byName name: String, server: RPCServerOrAny, fromContractNamesAndAddresses contractNamesAndAddresses: [String: [(AlphaWallet.Address, RPCServer)]]) -> AlphaWallet.Address? { - guard let addressesAndServers = contractNamesAndAddresses[name] else { return nil } - switch server { - case .any: - //TODO returning the first seems arbitrary, but I don't think TokenScript design has explored this area yet - guard let (contract, _) = addressesAndServers.first else { return nil } - return contract - case .server(let server): - guard let (contract, _) = addressesAndServers.first(where: { $0.1 == server }) else { return nil } - return contract + public static func getAttestationCollectionId(xmlString: String) -> String? { + return PrivateXMLHandler.getAttestationCollectionId(xmlString: xmlString) + } + + public func resolveAttestationAttributes(forAttestation attestation: Attestation) -> [Attestation.TypeValuePair] { + return privateXMLHandler.resolveAttestationAttributes(forAttestation: attestation) + } + + static func getEntities(forTokenScript xml: String) -> [Entity] { + return PrivateXMLHandler.getEntities(inXml: xml) + } + + static func isTokenScriptSupportedSchemaVersion(_ url: URL) -> Bool { + switch checkTokenScriptSchema(forPath: url) { + case .supportedTokenScriptVersion: + return true + case .unsupportedTokenScriptVersion: + return false + case .unknownXml: + return false + case .others: + return false } } @@ -1213,7 +1242,7 @@ extension XMLHandler.functional { //Lang doesn't matter let xmlContext = PrivateXMLHandler.createXmlContext(withLang: "en") - switch XMLHandler.functional.checkTokenScriptSchema(xmlString) { + switch checkTokenScriptSchema(xmlString) { case .supportedTokenScriptVersion: if let xml = try? Kanna.XML(xml: xmlString, encoding: .utf8) { return PrivateXMLHandler.getHoldingContracts(xml: xml, xmlContext: xmlContext) @@ -1223,11 +1252,19 @@ extension XMLHandler.functional { case .unsupportedTokenScriptVersion, .unknownXml, .others: return nil } - } - public static func getEntities(forTokenScript xml: String) -> [TokenScriptFileIndices.Entity] { - return PrivateXMLHandler.getEntities(inXml: xml) + public static func getNonTokenHoldingContract(byName name: String, server: RPCServerOrAny, fromContractNamesAndAddresses contractNamesAndAddresses: [String: [(AlphaWallet.Address, RPCServer)]]) -> AlphaWallet.Address? { + guard let addressesAndServers = contractNamesAndAddresses[name] else { return nil } + switch server { + case .any: + //TODO returning the first seems arbitrary, but I don't think TokenScript design has explored this area yet + guard let (contract, _) = addressesAndServers.first else { return nil } + return contract + case .server(let server): + guard let (contract, _) = addressesAndServers.first(where: { $0.1 == server }) else { return nil } + return contract + } } public static func checkTokenScriptSchema(forPath path: URL) -> TokenScriptSchema { @@ -1244,19 +1281,6 @@ extension XMLHandler.functional { } } - public static func isTokenScriptSupportedSchemaVersion(_ url: URL) -> Bool { - switch XMLHandler.functional.checkTokenScriptSchema(forPath: url) { - case .supportedTokenScriptVersion: - return true - case .unsupportedTokenScriptVersion: - return false - case .unknownXml: - return false - case .others: - return false - } - } - public static func checkTokenScriptSchema(_ contents: String) -> TokenScriptSchema { //It's fine to have a file that is empty. A CSS file for example. But we should expect the input to be XML if let xml = try? Kanna.XML(xml: contents, encoding: .utf8) { @@ -1282,5 +1306,16 @@ extension XMLHandler.functional { return .unknownXml } } + + static func hasValidTokenScriptFileExtension(url: URL) -> Bool { + return url.pathExtension == XMLHandler.fileExtension || url.pathExtension == "xml" + } +} + +extension XMLHandler { + public enum functional {} +} + +fileprivate extension XMLHandler.functional { } // swiftlint:enable file_length diff --git a/modules/AlphaWalletWeb3/AlphaWalletWeb3/Types/EIP712TypedData.swift b/modules/AlphaWalletWeb3/AlphaWalletWeb3/Types/EIP712TypedData.swift index b6652918e..ad359123c 100644 --- a/modules/AlphaWalletWeb3/AlphaWalletWeb3/Types/EIP712TypedData.swift +++ b/modules/AlphaWalletWeb3/AlphaWalletWeb3/Types/EIP712TypedData.swift @@ -100,10 +100,10 @@ extension EIP712TypedData { depSet.remove(primaryType) let sorted = [primaryType] + Array(depSet).sorted() let encoded = sorted.compactMap { type in - guard let values = types[type] else { return nil } - let param = values.map { "\($0.type) \($0.name)" }.joined(separator: ",") - return "\(type)(\(param))" - }.joined() + guard let values = types[type] else { return nil } + let param = values.map { "\($0.type) \($0.name)" }.joined(separator: ",") + return "\(type)(\(param))" + }.joined() return encoded.data(using: .utf8) ?? Data() }