Merge pull request #6925 from AlphaWallet/wip-attestation-support-in-tokenscript

[TokenScript] WIP for attestation support
pull/6926/head
Hwee-Boon Yar 1 year ago committed by GitHub
commit 3fdd299c94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      AlphaWallet.xcodeproj/project.pbxproj
  2. 11
      AlphaWallet/Common/Services/Application.swift
  3. 67
      AlphaWallet/TokenScript/FetchTokenScriptFiles.swift
  4. 5
      AlphaWallet/Tokens/Coordinators/TokensCoordinator.swift
  5. 6
      AlphaWalletTests/Coordinators/ActiveWalletViewTests.swift
  6. 1
      AlphaWalletTests/ViewControllers/PaymentCoordinatorTests.swift
  7. 15
      modules/AlphaWalletAttestation/AlphaWalletAttestation/Attestation.swift
  8. 10
      modules/AlphaWalletAttestation/AlphaWalletAttestation/AttestationsStore.swift
  9. 49
      modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/FetchTokenScriptFiles.swift
  10. 13
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/AlphaWalletTokensService.swift
  11. 11
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/TokensProcessingPipeline.swift
  12. 67
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionStore.swift
  13. 13
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/TokenScriptForAttestationStore.swift
  14. 5
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/TokenScriptSignatureVerifier.swift
  15. 3
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/AssetDefinitionStoreProtocol.swift
  16. 1
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/TokenScriptStatusResolver.swift
  17. 162
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/XMLHandler.swift

@ -359,6 +359,7 @@
5E7C7C53FC6F46DF501E8AD1 /* CallSmartContractFunctionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C78E17FA9C8C892EEA355 /* CallSmartContractFunctionTests.swift */; };
5E7C7C60BAF11B0BD135FC1E /* GroupActivityViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7E58DD0CF4E2B35B6ED2 /* GroupActivityViewCell.swift */; };
5E7C7C6CCD56211F6F4F6362 /* AttestationViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C707BB91428781B60832A /* AttestationViewCell.swift */; };
5E7C7C7C7B62A23FA7C3669B /* FetchTokenScriptFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7FD2D3F1340BD277AF86 /* FetchTokenScriptFiles.swift */; };
5E7C7C869A09FD09DCE77EE6 /* ActivityCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C760CC5A9E144BDB5451D /* ActivityCellViewModel.swift */; };
5E7C7C8C689E972389F4A564 /* CrashReporterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C76A087FB8690364F8552 /* CrashReporterViewController.swift */; };
5E7C7C98EAF40E8110241DBD /* NonFungibleTokenViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C783E3ADA4CF9554A0E7D /* NonFungibleTokenViewCell.swift */; };
@ -1353,6 +1354,7 @@
5E7C7FB7C3FB2A9CC0CC51D7 /* TokensViewControllerTableViewSectionHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokensViewControllerTableViewSectionHeader.swift; sourceTree = "<group>"; };
5E7C7FB99843529061368DA1 /* LocalesViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalesViewModel.swift; sourceTree = "<group>"; };
5E7C7FCE2427A30ACD860DF8 /* ServerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerViewModel.swift; sourceTree = "<group>"; };
5E7C7FD2D3F1340BD277AF86 /* FetchTokenScriptFiles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchTokenScriptFiles.swift; sourceTree = "<group>"; };
5E7C7FE30D58E4022AF04E48 /* AssetDefinitionStoreTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetDefinitionStoreTests.swift; sourceTree = "<group>"; };
5E7C7FE5EEC96A7CDF62213F /* BrowserHomeHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserHomeHeaderView.swift; sourceTree = "<group>"; };
5E7C7FF2EF77D5004A600DDB /* SuccessOverlayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SuccessOverlayView.swift; sourceTree = "<group>"; };
@ -2046,6 +2048,7 @@
5E7C704885516A808B3B8866 /* ErrorLocalizations.swift */,
5E7C7D784300C463FF3C7514 /* HardwareWallet */,
5E7C7864DB8E82BACE2F5F1F /* ImportedTypes.swift */,
5E7C7CA7694367E5D9D8DE99 /* Attestation */,
);
path = AlphaWallet;
sourceTree = "<group>";
@ -3115,6 +3118,7 @@
5E7C71077F12A2CF6733081D /* Views */,
5E7C7878E9E27F0CA05DFF03 /* Coordinators */,
5E7C78D36531AF80D3BAC20E /* ViewModels */,
5E7C7FD2D3F1340BD277AF86 /* FetchTokenScriptFiles.swift */,
);
path = TokenScript;
sourceTree = "<group>";
@ -3373,6 +3377,13 @@
path = Types;
sourceTree = "<group>";
};
5E7C7CA7694367E5D9D8DE99 /* Attestation */ = {
isa = PBXGroup;
children = (
);
path = Attestation;
sourceTree = "<group>";
};
5E7C7CC82D5F600655CF9FA7 /* Logic */ = {
isa = PBXGroup;
children = (
@ -5884,6 +5895,7 @@
5E7C74D586547B32EF5CD91C /* AcceptProposalViewController.swift in Sources */,
5E7C7270C6A83CC1ADC106DC /* AcceptAuthRequestViewController.swift in Sources */,
5E7C7F776D272AC23213B3BA /* ImportedTypes.swift in Sources */,
5E7C7C7C7B62A23FA7C3669B /* FetchTokenScriptFiles.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

@ -432,13 +432,12 @@ class Application: WalletDependenciesProvidable {
sessionsProvider.start()
let tokensService = AlphaWalletTokensService(
sessionsProvider: sessionsProvider,
tokensDataStore: tokensDataStore,
analytics: analytics,
transactionsStorage: transactionsDataStore,
let fetchTokenScriptFiles = FetchTokenScriptFilesImpl(
assetDefinitionStore: assetDefinitionStore,
transporter: BaseApiTransporter())
tokensDataStore: tokensDataStore,
sessionsProvider: sessionsProvider)
let tokensService = AlphaWalletTokensService(sessionsProvider: sessionsProvider, tokensDataStore: tokensDataStore, analytics: analytics, transactionsStorage: transactionsDataStore, assetDefinitionStore: assetDefinitionStore, fetchTokenScriptFiles: fetchTokenScriptFiles, transporter: BaseApiTransporter())
let tokensPipeline: TokensProcessingPipeline = WalletDataProcessingPipeline(
wallet: wallet,

@ -0,0 +1,67 @@
// Copyright © 2018 Stormbird PTE. LTD.
import Foundation
import Combine
import AlphaWalletAttestation
import AlphaWalletFoundation
import AlphaWalletTokenScript
public class FetchTokenScriptFilesImpl: FetchTokenScriptFiles {
private let assetDefinitionStore: AssetDefinitionStore
private let tokensDataStore: TokensDataStore
private let sessionsProvider: SessionsProvider
private let queue = DispatchQueue(label: "com.FetchAssetDefinitions.UpdateQueue")
private var cancellable = Set<AnyCancellable>()
public init(assetDefinitionStore: AssetDefinitionStore,
tokensDataStore: TokensDataStore,
sessionsProvider: SessionsProvider) {
self.assetDefinitionStore = assetDefinitionStore
self.tokensDataStore = tokensDataStore
self.sessionsProvider = sessionsProvider
}
public func start() {
fetchForTokens()
fetchForAttestations()
}
private func fetchForTokens() {
sessionsProvider.sessions
.map { $0.keys }
.receive(on: queue)
.flatMap { [tokensDataStore] servers in
asFuture {
await tokensDataStore.tokens(for: Array(servers))
}
}
.map { tokens in
return tokens.filter {
switch $0.type {
case .erc20, .erc721, .erc875, .erc721ForTickets, .erc1155:
return true
case .nativeCryptocurrency:
return false
}
}.map { AddressAndOptionalRPCServer(address: $0.contractAddress, server: $0.server) }
}.sink { [assetDefinitionStore] contractsInDatabase in
let contractsWithTokenScriptFileFromOfficialRepo = assetDefinitionStore.contractsWithTokenScriptFileFromOfficialRepo.map { AddressAndOptionalRPCServer(address: $0, server: nil) }
let contractsAndServers = Array(Set(contractsInDatabase + contractsWithTokenScriptFileFromOfficialRepo))
assetDefinitionStore.fetchXMLs(forContractsAndServers: contractsAndServers)
}.store(in: &cancellable)
}
private func fetchForAttestations() {
let attestations = AttestationsStore.allAttestations()
for each in attestations {
if let url = each.scriptUri {
Task { @MainActor in
await assetDefinitionStore.fetchXMLForAttestation(withScriptURL: url)
//TODO: attestations+TokenScript to implement
}
}
}
}
}

@ -248,7 +248,10 @@ class TokensCoordinator: Coordinator {
}
private func importAttestation(_ attestation: Attestation, intoWallet address: AlphaWallet.Address) {
attestationsStore.addAttestation(attestation, forWallet: address)
let isSuccessful = attestationsStore.addAttestation(attestation, forWallet: address)
if isSuccessful {
//TODO: attestations+TokenScript to implement reload like when we download TokenScript files at launch
}
}
}

@ -549,6 +549,12 @@ class ActiveWalletViewTests: XCTestCase {
}
// swiftlint:enable type_body_length
class FakeFetchTokenScriptFiles: FetchTokenScriptFiles {
func start() {
//no-op
}
}
import AlphaWalletNotifications
extension BasePushNotificationsService {

@ -55,6 +55,7 @@ extension WalletDataProcessingPipeline {
analytics: fas,
transactionsStorage: transactionsDataStore,
assetDefinitionStore: .make(),
fetchTokenScriptFiles: FakeFetchTokenScriptFiles(),
transporter: FakeApiTransporter())
let pipeline: TokensProcessingPipeline = WalletDataProcessingPipeline(

@ -191,6 +191,21 @@ public struct Attestation: Codable, Hashable {
public var server: RPCServer { easAttestation.server }
//TODO not hardcode
public var name: String { "EAS Attestation" }
public var scriptUri: URL? {
let url: URL? = data.compactMap { each in
if each.type.name == "scriptURI" {
switch each.value {
case .string(let value):
return URL(string: value)
case .address, .bool, .bytes, .int, .uint:
return nil
}
} else {
return nil
}
}.first
return url
}
private init(data: [TypeValuePair], easAttestation: EasAttestation, isValidAttestationIssuer: Bool, source: String) {
self.data = data

@ -21,21 +21,27 @@ public class AttestationsStore {
self.attestations = functional.readAttestations(forWallet: wallet, from: Self.fileUrl)
}
public func addAttestation(_ attestation: Attestation, forWallet address: AlphaWallet.Address) {
public static func allAttestations() -> [Attestation] {
return functional.readAttestations(from: fileUrl).flatMap { $0.value }
}
public func addAttestation(_ attestation: Attestation, forWallet address: AlphaWallet.Address) -> Bool {
var allAttestations = functional.readAttestations(from: Self.fileUrl)
do {
var attestationsForWallet: [Attestation] = allAttestations[address] ?? []
guard !attestations.contains(attestation) else {
infoLog("[Attestation] Attestation already exist. Skipping")
return
return false
}
attestationsForWallet.append(attestation)
allAttestations[address] = attestationsForWallet
try saveAttestations(attestations: allAttestations)
attestations = attestationsForWallet
infoLog("[Attestation] Imported attestation")
return true
} catch {
errorLog("[Attestation] failed to encode attestations while adding attestation to: \(Self.fileUrl.absoluteString) error: \(error)")
return false
}
}

@ -1,48 +1,5 @@
// Copyright © 2018 Stormbird PTE. LTD.
// Copyright © 2023 Stormbird PTE. LTD.
import Foundation
import Combine
import AlphaWalletTokenScript
public class FetchTokenScriptFiles {
private let assetDefinitionStore: AssetDefinitionStore
private let tokensDataStore: TokensDataStore
private let sessionsProvider: SessionsProvider
private let queue = DispatchQueue(label: "com.FetchAssetDefinitions.UpdateQueue")
private var cancellable = Set<AnyCancellable>()
public init(assetDefinitionStore: AssetDefinitionStore,
tokensDataStore: TokensDataStore,
sessionsProvider: SessionsProvider) {
self.assetDefinitionStore = assetDefinitionStore
self.tokensDataStore = tokensDataStore
self.sessionsProvider = sessionsProvider
}
public func start() {
sessionsProvider.sessions
.map { $0.keys }
.receive(on: queue)
.flatMap { [tokensDataStore] servers in
asFuture {
await tokensDataStore.tokens(for: Array(servers))
}
}
.map { tokens in
return tokens.filter {
switch $0.type {
case .erc20, .erc721, .erc875, .erc721ForTickets, .erc1155:
return true
case .nativeCryptocurrency:
return false
}
}.map { AddressAndOptionalRPCServer(address: $0.contractAddress, server: $0.server) }
}.sink { [assetDefinitionStore] contractsInDatabase in
let contractsWithTokenScriptFileFromOfficialRepo = assetDefinitionStore.contractsWithTokenScriptFileFromOfficialRepo.map { AddressAndOptionalRPCServer(address: $0, server: nil) }
let contractsAndServers = Array(Set(contractsInDatabase + contractsWithTokenScriptFileFromOfficialRepo))
assetDefinitionStore.fetchXMLs(forContractsAndServers: contractsAndServers)
}.store(in: &cancellable)
}
public protocol FetchTokenScriptFiles {
func start()
}

@ -58,23 +58,14 @@ public class AlphaWalletTokensService: TokensService {
.eraseToAnyPublisher()
}()
public init(sessionsProvider: SessionsProvider,
tokensDataStore: TokensDataStore,
analytics: AnalyticsLogger,
transactionsStorage: TransactionDataStore,
assetDefinitionStore: AssetDefinitionStore,
transporter: ApiTransporter) {
public init(sessionsProvider: SessionsProvider, tokensDataStore: TokensDataStore, analytics: AnalyticsLogger, transactionsStorage: TransactionDataStore, assetDefinitionStore: AssetDefinitionStore, fetchTokenScriptFiles: FetchTokenScriptFiles, transporter: ApiTransporter) {
self.transporter = transporter
self.sessionsProvider = sessionsProvider
self.tokensDataStore = tokensDataStore
self.analytics = analytics
self.transactionsStorage = transactionsStorage
self.assetDefinitionStore = assetDefinitionStore
self.fetchTokenScriptFiles = FetchTokenScriptFiles(
assetDefinitionStore: assetDefinitionStore,
tokensDataStore: tokensDataStore,
sessionsProvider: sessionsProvider)
self.fetchTokenScriptFiles = fetchTokenScriptFiles
}
public func tokens(for servers: [RPCServer]) async -> [Token] {

@ -57,11 +57,20 @@ public final class WalletDataProcessingPipeline: TokensProcessingPipeline {
}
}
let whenAttestationXMLChanged = assetDefinitionStore.attestationXMLChange
.receive(on: queue)
.flatMap { [tokensService] _ in
asFuture {
await tokensService.tokens
}
}
let whenTokensHasChanged = tokensService.tokensPublisher
.dropFirst()
.receive(on: queue)
let whenCollectionHasChanged = Publishers.Merge4(whenTokensHasChanged, whenTickersChanged, whenSignatureOrBodyChanged, whenCurrencyChanged)
let whenCollectionHasChanged = Publishers.Merge5(whenTokensHasChanged, whenTickersChanged, whenSignatureOrBodyChanged, whenAttestationXMLChanged, whenCurrencyChanged)
//TODO: attestations+TokenScript to verify attestationXMLChange triggers reloading
.map { $0.map { TokenViewModel(token: $0) } }
.flatMapLatest { tokenViewModels in asFuture { await self.applyTickers(tokens: tokenViewModels) ?? [] } }
.flatMap { self.applyTokenScriptOverrides(tokens: $0) }

@ -34,10 +34,14 @@ public class AssetDefinitionStore: NSObject {
private var lastContractInPasteboard: String?
private var backingStore: AssetDefinitionBackingStore
private var tokenScriptForAttestationStore = TokenScriptForAttestationStore()
//TODO: attestations+TokenScript rename to be for tokens (only)?
private let xmlHandlers: AtomicDictionary<AlphaWallet.Address, PrivateXMLHandler> = .init()
private let xmlHandlersForAttestations: AtomicDictionary<URL, PrivateXMLHandler> = .init()
private let baseXmlHandlers: AtomicDictionary<String, PrivateXMLHandler> = .init()
private var signatureChangeSubject: PassthroughSubject<AlphaWallet.Address, Never> = .init()
private var bodyChangeSubject: PassthroughSubject<AlphaWallet.Address, Never> = .init()
private var attestationXmlChangeSubject: PassthroughSubject<URL, Never> = .init()
private var listOfBadTokenScriptFilesSubject: CurrentValueSubject<[TokenScriptFileIndices.FileName], Never> = .init([])
private let networking: AssetDefinitionNetworking
private let tokenScriptStatusResolver: TokenScriptStatusResolver
@ -65,6 +69,10 @@ public class AssetDefinitionStore: NSObject {
bodyChangeSubject.eraseToAnyPublisher()
}
public var attestationXMLChange: AnyPublisher<URL, Never> {
attestationXmlChangeSubject.eraseToAnyPublisher()
}
public var assetsSignatureOrBodyChange: AnyPublisher<AlphaWallet.Address, Never> {
return Publishers
.Merge(signatureChange, bodyChange)
@ -173,6 +181,48 @@ public class AssetDefinitionStore: NSObject {
}
}
public func fetchXMLForAttestation(withScriptURL url: URL) async {
let request = AssetDefinitionNetworking.GetXmlFileRequest(url: url.rewrittenIfIpfs, lastModifiedDate: nil)
return await withCheckedContinuation { continuation in
networking.fetchXml(request: request)
.sinkAsync(receiveCompletion: { _ in
//no-op
}, receiveValue: { [weak self] response in
guard let strongSelf = self else {
continuation.resume(returning: ())
return
}
switch response {
case .error:
continuation.resume(returning: ())
return
case .unmodified:
continuation.resume(returning: ())
return
case .xml(let xml):
//Note that Alamofire converts the 304 to a 200 if caching is enabled (which it is, by default). So we'll never get a 304 here. Checking against Charles proxy will show that a 304 is indeed returned by the server with an empty body. So we compare the contents instead. https://github.com/Alamofire/Alamofire/issues/615
//TODO: attestations+TokenScript to implement persistance for attestations' TokenScript files
if xml == strongSelf.tokenScriptForAttestationStore[url] {
continuation.resume(returning: ())
return
} else if functional.isTruncatedXML(xml: xml) {
continuation.resume(returning: ())
return
} else {
strongSelf.tokenScriptForAttestationStore[url] = xml
//TODO: attestations+TokenScript do we enforce they must be IPFS before downloading?
//We do not invalidate (by removing the downloaded XML for the URL) like we do for TokenScript for tokens because the contents are on iPFS and thus immutable
strongSelf.triggerAttestationXMLChangedSubscribers(forURL: url)
continuation.resume(returning: ())
}
}
})
}
}
private func fetchXML(contract: AlphaWallet.Address, server: RPCServer?, url: URL, useCacheAndFetch: Bool = false, completionHandler: ((Result) -> Void)? = nil) {
let lastModified = lastModifiedDateOfCachedAssetDefinitionFile(forContract: contract)
let request = AssetDefinitionNetworking.GetXmlFileRequest(url: url, lastModifiedDate: lastModified)
@ -211,6 +261,10 @@ public class AssetDefinitionStore: NSObject {
bodyChangeSubject.send(contract)
}
private func triggerAttestationXMLChangedSubscribers(forURL url: URL) {
attestationXmlChangeSubject.send(url)
}
private func triggerSignatureChangedSubscribers(forContract contract: AlphaWallet.Address) {
signatureChangeSubject.send(contract)
}
@ -280,6 +334,14 @@ extension AssetDefinitionStore: AssetDefinitionStoreProtocol {
xmlHandlers[key] = xmlHandler
}
public func getXmlHandler(forAttestationAtURL url: URL) -> PrivateXMLHandler? {
return xmlHandlersForAttestations[url]
}
public func set(xmlHandler: PrivateXMLHandler?, forAttestationAtURL url: URL) {
xmlHandlersForAttestations[url] = xmlHandler
}
public func getBaseXmlHandler(for key: String) -> PrivateXMLHandler? {
baseXmlHandlers[key]
}
@ -297,6 +359,11 @@ extension AssetDefinitionStore: TokenScriptStatusResolver {
public func computeTokenScriptStatus(forContract contract: AlphaWallet.Address, xmlString: String, isOfficial: Bool) -> Promise<TokenLevelTokenScriptDisplayStatus> {
tokenScriptStatusResolver.computeTokenScriptStatus(forContract: contract, xmlString: xmlString, isOfficial: isOfficial)
}
public func computeTokenScriptStatus(forAttestationURL url: URL, xmlString: String) -> Promise<TokenLevelTokenScriptDisplayStatus> {
//TODO: attestations+TokenScript to implement computeTokenScriptStatus
return Promise { _ in }
}
}
public final class InMemoryTokenScriptFilesProvider: BaseTokenScriptFilesProvider {

@ -0,0 +1,13 @@
// Copyright © 2023 Stormbird PTE. LTD.
import Foundation
class TokenScriptForAttestationStore {
//TODO improve storage when we know more about how the TokenScript store for attestations is used
private var storage: [URL: String] = [:]
subscript(url: URL) -> String? {
get { return storage[url] }
set { storage[url] = newValue }
}
}

@ -70,6 +70,11 @@ public class BaseTokenScriptStatusResolver: TokenScriptStatusResolver {
}
}
public func computeTokenScriptStatus(forAttestationURL url: URL, xmlString: String) -> Promise<TokenLevelTokenScriptDisplayStatus> {
//TODO: attestations+TokenScript to implement computeTokenScriptStatus
return Promise { _ in }
}
private func verificationType(forXml xmlString: String) -> PromiseKit.Promise<TokenScriptSignatureVerificationType> {
if let cachedVerificationType = backingStore.getCacheTokenScriptSignatureVerificationType(forXmlString: xmlString) {
return .value(cachedVerificationType)

@ -6,10 +6,13 @@ public protocol AssetDefinitionStoreProtocol: TokenScriptStatusResolver {
var features: TokenScriptFeatures { get }
subscript(contract: AlphaWallet.Address) -> String? { get }
subscript(url: URL) -> String? { get }
func isOfficial(contract: AlphaWallet.Address) -> Bool
func isCanonicalized(contract: AlphaWallet.Address) -> Bool
func getXmlHandler(for key: AlphaWallet.Address) -> PrivateXMLHandler?
func set(xmlHandler: PrivateXMLHandler?, for key: AlphaWallet.Address)
func getXmlHandler(forAttestationAtURL url: URL) -> PrivateXMLHandler?
func set(xmlHandler: PrivateXMLHandler?, forAttestationAtURL url: URL)
func getBaseXmlHandler(for key: String) -> PrivateXMLHandler?
func setBaseXmlHandler(for key: String, baseXmlHandler: PrivateXMLHandler?)
func baseTokenScriptFile(for tokenType: TokenType) -> XMLFile?

@ -5,4 +5,5 @@ import PromiseKit
public protocol TokenScriptStatusResolver {
func computeTokenScriptStatus(forContract contract: AlphaWallet.Address, xmlString: String, isOfficial: Bool) -> Promise<TokenLevelTokenScriptDisplayStatus>
func computeTokenScriptStatus(forAttestationURL url: URL, xmlString: String) -> Promise<TokenLevelTokenScriptDisplayStatus>
}

@ -111,6 +111,29 @@ public enum TokenLevelTokenScriptDisplayStatus {
// Interface to extract data from non fungible token
// swiftlint:disable type_body_length
public class PrivateXMLHandler {
enum Target {
case token(AlphaWallet.Address)
//TODO: attestations+TokenScript to implement. Is the key the script's URL?
case attestation(URL)
var isFifaTicketContract: Bool {
switch self {
case .token(let contractAddress):
return contractAddress.isFifaTicketContract
case .attestation:
return false
}
}
var isUEFATicketContract: Bool {
switch self {
case .token(let contractAddress):
return contractAddress.isUEFATicketContract
case .attestation:
return false
}
}
}
private static let emptyXMLString = "<tbml:token xmlns:tbml=\"http://attestation.id/ns/tbml\"></tbml:token>"
private static let emptyXML = try! Kanna.XML(xml: emptyXMLString, encoding: .utf8)
fileprivate static let tokenScriptNamespace = TokenScript.supportedTokenScriptNamespace
@ -120,7 +143,7 @@ public class PrivateXMLHandler {
private let signatureNamespacePrefix = "ds:"
private let xhtmlNamespacePrefix = "xhtml:"
private let xmlContext = PrivateXMLHandler.createXmlContext(withLang: PrivateXMLHandler.lang)
private let contractAddress: AlphaWallet.Address
private let target: Target
var server: RPCServerOrAny?
//Explicit type so that the variable autocompletes with AppCode
private lazy var selections = extractSelectionsForToken()
@ -235,7 +258,13 @@ public class PrivateXMLHandler {
let selection = XMLHandler.getExcludeSelectionId(fromActionElement: actionElement, xmlContext: xmlContext).flatMap { id in
self.selections.first { $0.id == id }
}
switch target {
case .token(let contractAddress):
results.append(.init(type: .tokenScript(contract: contractAddress, title: name, viewHtml: (html: html, style: style), attributes: attributes, transactionFunction: functionOrigin, selection: selection)))
case .attestation:
//TODO: attestations+TokenScript to implement support for `actions
break
}
}
}
if fromActionAsTopLevel.isEmpty {
@ -277,7 +306,13 @@ public class PrivateXMLHandler {
let addressElements = XMLHandler.getAddressElements(fromContractElement: eventSourceContractElement, xmlContext: xmlContext)
optionalContract = addressElements.first?.text.flatMap({ AlphaWallet.Address(string: $0.trimmed) })
} else {
switch target {
case .token(let contractAddress):
optionalContract = contractAddress
case .attestation:
//TODO: attestations+TokenScript to implement support for `activityCards`
optionalContract = nil
}
}
guard let contract = optionalContract, let origin = Origin(forEthereumEventElement: ethereumEventElement, asnModuleNamedTypeElement: asnModuleNamedElement, contract: contract, xmlContext: xmlContext) else { return nil }
switch origin {
@ -319,10 +354,14 @@ public class PrivateXMLHandler {
}()
private lazy var _labelInSingularForm: String? = {
switch target {
case .token(let contractAddress):
if contractAddress.sameContract(as: Constants.katContractAddress) {
return Constants.katNameFallback
}
case .attestation:
break
}
if let labelStringElement = XMLHandler.getLabelStringElement(fromElement: tokenElement, xmlContext: xmlContext), let label = labelStringElement.text {
return label
} else {
@ -341,11 +380,15 @@ public class PrivateXMLHandler {
lazy var labelInPluralForm: String? = {
var labelInPluralForm: String?
threadSafe.performSync {
switch target {
case .token(let contractAddress):
if contractAddress.sameContract(as: Constants.katContractAddress) {
labelInPluralForm = Constants.katNameFallback
return
}
case .attestation:
break
}
if let nameElement = XMLHandler.getLabelElementForPluralForm(fromElement: tokenElement, xmlContext: xmlContext), let name = nameElement.text {
labelInPluralForm = name
} else {
@ -383,7 +426,7 @@ public class PrivateXMLHandler {
private init(contract: AlphaWallet.Address, xmlString: String?, baseTokenType: TokenType?, isOfficial: Bool, isCanonicalized: Bool, assetDefinitionStore: AssetDefinitionStoreProtocol) {
let xmlString = xmlString ?? ""
self.contractAddress = contract
self.target = Target.token(contract)
self.isOfficial = isOfficial
self.isCanonicalized = isCanonicalized
self.baseTokenType = baseTokenType
@ -441,6 +484,58 @@ public class PrivateXMLHandler {
self.server = _server
}
private init(forAttestationURL url: URL, xmlString: String?, assetDefinitionStore: AssetDefinitionStoreProtocol) {
let xmlString = xmlString ?? ""
self.target = Target.attestation(url)
self.isOfficial = false
self.isCanonicalized = false
self.baseTokenType = nil
self.features = assetDefinitionStore.features
var _xml: XMLDocument!
var _tokenScriptStatus: Promise<TokenLevelTokenScriptDisplayStatus>!
var _hasValidTokenScriptFile: Bool!
var _server: RPCServerOrAny?
let _xmlContext = xmlContext
let features = self.features
threadSafe.performSync {
//We still compute the TokenScript status even if xmlString is empty because it might be considered empty because there's a conflict
let tokenScriptStatusPromise = assetDefinitionStore.computeTokenScriptStatus(forAttestationURL: url, xmlString: xmlString)
_tokenScriptStatus = tokenScriptStatusPromise
if let tokenScriptStatus = tokenScriptStatusPromise.value {
let (xml, hasValidTokenScriptFile) = PrivateXMLHandler.storeXmlAccordingToTokenScriptStatus(xmlString: xmlString, tokenScriptStatus: tokenScriptStatus, features: features)
_xml = xml
_hasValidTokenScriptFile = hasValidTokenScriptFile
//TODO: attestations+TokenScript no specific server in TokenScript for attestation right?
_server = .any
} else {
_xml = (try? Kanna.XML(xml: xmlString, encoding: .utf8)) ?? PrivateXMLHandler.emptyXML
_hasValidTokenScriptFile = true
//TODO: attestations+TokenScript is there a specific server for attestation's TokenScript?
_server = .any
tokenScriptStatusPromise.done { tokenScriptStatus in
let (xml, hasValidTokenScriptFile) = PrivateXMLHandler.storeXmlAccordingToTokenScriptStatus(xmlString: xmlString, tokenScriptStatus: tokenScriptStatus, features: features)
_xml = xml
_hasValidTokenScriptFile = hasValidTokenScriptFile
//TODO: attestations+TokenScript no specific server in TokenScript for attestation right?
_server = .any
//TODO: attestations+TokenScript is there a need to invalidate the signature status here?
}.cauterize()
}
}
self.xml = _xml
self.tokenScriptStatus = _tokenScriptStatus
self.hasValidTokenScriptFile = _hasValidTokenScriptFile!
self.server = _server
}
convenience init(forAttestationURL url: URL, assetDefinitionStore: AssetDefinitionStoreProtocol) {
let xmlString = assetDefinitionStore[url]
self.init(forAttestationURL: url, xmlString: xmlString, assetDefinitionStore: assetDefinitionStore)
}
private func extractHtml(fromViewElement element: XMLElement) -> (html: String, style: String) {
let (style: style, script: script, body: body) = XMLHandler.getTokenScriptTokenViewContents(fromViewElement: element, xmlContext: xmlContext, xhtmlNamespacePrefix: xhtmlNamespacePrefix)
let sanitizedHtml = sanitize(html: body)
@ -546,13 +641,13 @@ public class PrivateXMLHandler {
case .erc20:
actions = [.erc20Send, .erc20Receive]
case .erc721:
if contractAddress.isUEFATicketContract {
if target.isUEFATicketContract {
actions = [.nftRedeem, .nonFungibleTransfer]
} else {
actions = [.nonFungibleTransfer]
}
case .erc875:
if contractAddress.isFifaTicketContract {
if target.isFifaTicketContract {
actions = [.nftRedeem, .nftSell, .nonFungibleTransfer]
} else {
actions = [.nftSell, .nonFungibleTransfer]
@ -569,13 +664,13 @@ public class PrivateXMLHandler {
case .erc20, .nativeCryptocurrency:
actions = [.erc20Send, .erc20Receive]
case .erc721, .erc721ForTickets:
if contractAddress.isUEFATicketContract {
if target.isUEFATicketContract {
actions = [.nftRedeem, .nonFungibleTransfer]
} else {
actions = [.nonFungibleTransfer]
}
case .erc875:
if contractAddress.isFifaTicketContract {
if target.isFifaTicketContract {
actions = [.nftRedeem, .nftSell, .nonFungibleTransfer]
} else {
actions = [.nftSell, .nonFungibleTransfer]
@ -635,6 +730,8 @@ public class PrivateXMLHandler {
}
private func extractFields(fromElementContainingAttributes element: XMLElement) -> [AttributeId: AssetAttribute] {
switch target {
case .token(let contractAddress):
var fields = [AttributeId: AssetAttribute]()
for each in XMLHandler.getAttributeElements(fromAttributeElement: element, xmlContext: xmlContext) {
guard let name = each["name"] else { continue }
@ -643,6 +740,14 @@ public class PrivateXMLHandler {
fields[name] = attribute
}
return fields
case .attestation:
//TODO: attestations+TokenScript to implement support for extractFields
var fields = [AttributeId: AssetAttribute]()
for each in XMLHandler.getAttributeElements(fromAttributeElement: element, xmlContext: xmlContext) {
guard let name = each["name"] else { continue }
}
return fields
}
}
//TODOis it still necessary to sanitize? Maybe we still need to strip a, button, html?
@ -912,6 +1017,47 @@ public struct XMLHandler {
self.init(contract: contract, optionalTokenType: tokenType, assetDefinitionStore: assetDefinitionStore)
}
public init(forAttestationURL url: URL, assetDefinitionStore: AssetDefinitionStoreProtocol) {
let features = assetDefinitionStore.features
var privateXMLHandler: PrivateXMLHandler
var baseXMLHandler: PrivateXMLHandler?
if let handler = assetDefinitionStore.getXmlHandler(forAttestationAtURL: url) {
privateXMLHandler = handler
} else {
privateXMLHandler = PrivateXMLHandler(forAttestationURL: url, assetDefinitionStore: assetDefinitionStore)
assetDefinitionStore.set(xmlHandler: privateXMLHandler, forAttestationAtURL: url)
}
//TODO: attestations+TokenScript tokenType not used? Relevant?
let tokenType: TokenType? = nil
if features.isActivityEnabled, let tokenType = tokenType {
//let tokenTypeForBaseXml: TokenType
//if privateXMLHandler.hasValidTokenScriptFile, let tokenTypeInXml = privateXMLHandler.tokenType.flatMap({ TokenType(tokenInterfaceType: $0) }) {
// tokenTypeForBaseXml = tokenTypeInXml
//} else {
// tokenTypeForBaseXml = tokenType
//}
////Key cannot be just `contract`, because the type can change (from the overriding TokenScript file)
//let key = "\(contract.eip55String)-\(tokenTypeForBaseXml.rawValue)"
//if let handler = assetDefinitionStore.getBaseXmlHandler(for: key) {
// baseXMLHandler = handler
//} else {
// if let xml = assetDefinitionStore.baseTokenScriptFile(for: tokenTypeForBaseXml) {
// baseXMLHandler = PrivateXMLHandler(contract: contract, baseXml: xml, baseTokenType: tokenTypeForBaseXml, assetDefinitionStore: assetDefinitionStore)
// assetDefinitionStore.setBaseXmlHandler(for: key, baseXmlHandler: baseXMLHandler)
// } else {
// baseXMLHandler = nil
// }
//}
} else {
baseXMLHandler = nil
}
self.baseXMLHandler = baseXMLHandler
self.privateXMLHandler = privateXMLHandler
}
//private because we don't want client code creating XMLHandler(s) to be able to accidentally pass in a nil TokenType
private init(contract: AlphaWallet.Address, optionalTokenType tokenType: TokenType?, assetDefinitionStore: AssetDefinitionStoreProtocol) {
let features = assetDefinitionStore.features

Loading…
Cancel
Save