Merge pull request #6999 from AlphaWallet/tokenscript-support-attestation

[TokenScript][Attestation] Attestation support in TokenScript
pull/7000/head
Hwee-Boon Yar 1 year ago committed by GitHub
commit 97caded5e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      AlphaWallet.xcodeproj/project.pbxproj
  2. 30
      AlphaWallet/ActiveWalletCoordinator.swift
  3. 2
      AlphaWallet/AppCoordinator.swift
  4. 5
      AlphaWallet/Common/Services/Application.swift
  5. 2
      AlphaWallet/Common/Views/ViewModels/TokenCardRowViewModel.swift
  6. 14
      AlphaWallet/Market/ImportMagicLinkController.swift
  7. 2
      AlphaWallet/Market/ViewModels/ImportMagicTokenCardRowViewModel.swift
  8. 4
      AlphaWallet/Redeem/ViewControllers/RedeemTokenCardQuantitySelectionViewController.swift
  9. 4
      AlphaWallet/Redeem/ViewModels/RedeemTokenCardQuantitySelectionViewModel.swift
  10. 3
      AlphaWallet/Resources/TokenScriptFiles/aETH.tsml
  11. 6
      AlphaWallet/Sell/ViewControllers/EnterSellTokensCardPriceQuantityViewController.swift
  12. 4
      AlphaWallet/Sell/ViewModels/EnterSellTokensCardPriceQuantityViewModel.swift
  13. 4
      AlphaWallet/Sell/ViewModels/GenerateTransferMagicLinkViewModel.swift
  14. 48
      AlphaWallet/TokenScript/AttestationTypeValuePairToJavaScriptConvertor.swift
  15. 21
      AlphaWallet/TokenScript/FetchTokenScriptFiles.swift
  16. 2
      AlphaWallet/Tokens/Collectibles/ViewModels/NFTCollectionViewModel.swift
  17. 10
      AlphaWallet/Tokens/Coordinators/TokensCoordinator.swift
  18. 2
      AlphaWallet/Tokens/NftAssetDisplayHelper.swift
  19. 156
      AlphaWallet/Tokens/ViewControllers/AttestationViewController.swift
  20. 6
      AlphaWallet/Tokens/ViewControllers/AttestationsViewController.swift
  21. 1
      AlphaWallet/Tokens/ViewControllers/TokenInstanceActionViewController.swift
  22. 4
      AlphaWallet/Tokens/ViewControllers/VerifiableStatusViewController.swift
  23. 57
      AlphaWallet/Tokens/ViewModels/AttestationViewCellViewModel.swift
  24. 2
      AlphaWallet/Tokens/ViewModels/FungibleTokenTabViewModel.swift
  25. 2
      AlphaWallet/Tokens/ViewModels/TokenCardWebViewModel.swift
  26. 16
      AlphaWallet/Tokens/ViewModels/TokensViewModel.swift
  27. 120
      AlphaWallet/Tokens/Views/TokenInstanceWebView.swift
  28. 4
      AlphaWallet/Transfer/ViewControllers/TransferTokensCardQuantitySelectionViewController.swift
  29. 2
      AlphaWallet/Transfer/ViewModels/SetTransferTokensCardExpiryDateViewControllerViewModel.swift
  30. 4
      AlphaWallet/Transfer/ViewModels/SetTransferTokensCardExpiryDateViewModel.swift
  31. 4
      AlphaWallet/Transfer/ViewModels/TransferTokensCardQuantitySelectionViewModel.swift
  32. 2
      AlphaWalletTests/Coordinators/ActiveWalletViewTests.swift
  33. 1
      AlphaWalletTests/Ens/EnsResolverTests.swift
  34. 12
      AlphaWalletTests/TokenScriptClient/AssetDefinitionDiskBackingStoreWithOverridesTests.swift
  35. 17
      AlphaWalletTests/TokenScriptClient/AssetDefinitionStoreTests.swift
  36. 4
      AlphaWalletTests/TokenScriptClient/TokenScriptSignatureVerifierTest.swift
  37. 21
      AlphaWalletTests/TokenScriptClient/XMLHandlerTest.swift
  38. 2
      AlphaWalletTokenScript.podspec
  39. 4
      Podfile.lock
  40. 1
      modules/AlphaWalletABI/AlphaWalletABI/Types/ABIValue.swift
  41. 238
      modules/AlphaWalletAttestation/AlphaWalletAttestation/Attestation.swift
  42. 13
      modules/AlphaWalletAttestation/AlphaWalletAttestation/AttestationAttribute.swift
  43. 56
      modules/AlphaWalletAttestation/AlphaWalletAttestation/AttestationsStore.swift
  44. 1
      modules/AlphaWalletAttestation/AlphaWalletAttestation/EasAttestation.swift
  45. 7
      modules/AlphaWalletCore/AlphaWalletCore/Extensions/String+Extensions.swift
  46. 10
      modules/AlphaWalletCore/AlphaWalletCore/Extensions/URL+Extensions.swift
  47. 2
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Alerts/PriceAlertDataStore.swift
  48. 2
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Initializers/MigrationInitializerForOneChainPerDatabase.swift
  49. 2
      modules/AlphaWalletFoundation/AlphaWalletFoundation/NFT/OpenSea/Types/OpenSeaNonFungibleTokenHandling.swift
  50. 2
      modules/AlphaWalletFoundation/AlphaWalletFoundation/RPC/CallSmartContractFunction.swift
  51. 13
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Settings/Types/Config.swift
  52. 2
      modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/EventSourceForActivities.swift
  53. 18
      modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/Models/TokenScriptSupportable.swift
  54. 10
      modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/TokenScript+UI.swift
  55. 10
      modules/AlphaWalletFoundation/AlphaWalletFoundation/TokenScriptClient/TokenScriptOverridesFileManager.swift
  56. 1
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/AlphaWalletTokensService.swift
  57. 38
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/Helpers/TokenAdaptor.swift
  58. 1
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/Logic/IsInterfaceSupported165.swift
  59. 13
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Tokens/TokensProcessingPipeline.swift
  60. 4
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Types/RealmConfiguration.swift
  61. 23
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/AttestationVerificationStatus.swift
  62. 2
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetAttribute.swift
  63. 23
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionBackingStore.swift
  64. 584
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionDiskBackingStore.swift
  65. 115
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionDiskBackingStoreWithOverrides.swift
  66. 52
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionInMemoryBackingStore.swift
  67. 312
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetDefinitionStore.swift
  68. 2
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/AssetInternalValue.swift
  69. 20
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/BaseTokenScriptFiles.swift
  70. 115
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/TokenScriptFileIndices.swift
  71. 13
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/TokenScriptForAttestationStore.swift
  72. 79
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/TokenScriptSignatureVerifier.swift
  73. 22
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/AssetDefinitionStoreProtocol.swift
  74. 12
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/FileContentsHash.swift
  75. 12
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/Filename.swift
  76. 4
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/GeneralisedTime.swift
  77. 12
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/TokenScriptResolver.swift
  78. 72
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Types/TokenScriptStatusResolver.swift
  79. 10
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/Web3.swift
  80. 35
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/XMLHandler+XPaths.swift
  81. 483
      modules/AlphaWalletTokenScript/AlphaWalletTokenScript/Models/XMLHandler.swift
  82. 8
      modules/AlphaWalletWeb3/AlphaWalletWeb3/Types/EIP712TypedData.swift

@ -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 = "<group>"; };
5E7C7228C9BEB801D4CD34DE /* EtherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EtherTests.swift; sourceTree = "<group>"; };
5E7C723C21F6376387AD1DCE /* PromptBackupWalletAfterWalletCreationViewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PromptBackupWalletAfterWalletCreationViewViewModel.swift; sourceTree = "<group>"; };
5E7C7240B2AC0FE9E752B516 /* AttestationTypeValuePairToJavaScriptConvertor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttestationTypeValuePairToJavaScriptConvertor.swift; sourceTree = "<group>"; };
5E7C72571AB0FECB26FEB1B1 /* ClearDappBrowserCacheCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClearDappBrowserCacheCoordinator.swift; sourceTree = "<group>"; };
5E7C727433F7B8E322B3C68A /* SetTransferTokensCardExpiryDateViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetTransferTokensCardExpiryDateViewController.swift; sourceTree = "<group>"; };
5E7C728B3CA6A429AB5EE5DF /* ContainerViewWithShadow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContainerViewWithShadow.swift; sourceTree = "<group>"; };
@ -3126,6 +3128,7 @@
5E7C7878E9E27F0CA05DFF03 /* Coordinators */,
5E7C78D36531AF80D3BAC20E /* ViewModels */,
5E7C7FD2D3F1340BD277AF86 /* FetchTokenScriptFiles.swift */,
5E7C7240B2AC0FE9E752B516 /* AttestationTypeValuePairToJavaScriptConvertor.swift */,
);
path = TokenScript;
sourceTree = "<group>";
@ -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;
};

@ -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)
}
}
}

@ -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)
}
}
}

@ -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)

@ -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 {

@ -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..<bytes32Tokens.count {
let token = bytes32Tokens[i]
if let tokenId = BigUInt(token.drop0x, radix: 16) {
let token = xmlHandler.getToken(
name: name,
symbol: symbol,
fromTokenIdOrEvent: .tokenId(tokenId: tokenId), index: UInt16(i),
inWallet: wallet.address,
server: server,
tokenType: tokenType,
assetDefinitionStore: assetDefinitionStore)
let token = xmlHandler.getToken(name: name, symbol: symbol, fromTokenIdOrEvent: .tokenId(tokenId: tokenId), index: UInt16(i), inWallet: wallet.address, server: server, tokenType: tokenType)
tokens.append(token)
}
}

@ -91,7 +91,7 @@ struct ImportMagicTokenCardRowViewModel: TokenCardRowViewModelProtocol {
var tokenScriptHtml: String {
guard let tokenHolder = viewModel.tokenHolder else { return "" }
let xmlHandler = XMLHandler(contract: tokenHolder.contractAddress, tokenType: tokenHolder.tokenType, assetDefinitionStore: assetDefinitionStore)
let xmlHandler = assetDefinitionStore.xmlHandler(forContract: tokenHolder.contractAddress, tokenType: tokenHolder.tokenType)
let (html: html, style: style) = xmlHandler.tokenViewIconifiedHtml
return wrapWithHtmlViewport(html: html, style: style, forTokenId: tokenHolder.tokenIds[0])

@ -54,7 +54,7 @@ class RedeemTokenCardQuantitySelectionViewController: UIViewController, TokenVer
init(viewModel: RedeemTokenCardQuantitySelectionViewModel,
assetDefinitionStore: AssetDefinitionStore) {
self.viewModel = viewModel
self.assetDefinitionStore = assetDefinitionStore
@ -121,7 +121,7 @@ class RedeemTokenCardQuantitySelectionViewController: UIViewController, TokenVer
@objc 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(
message: R.string.localizable.aWalletTokenRedeemSelectTokenQuantityAtLeastOneTitle(tokenTypeName),
alertButtonTitles: [R.string.localizable.oK()],

@ -18,7 +18,7 @@ struct RedeemTokenCardQuantitySelectionViewModel {
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.aWalletTokenRedeemSelectQuantityTitle(tokenTypeName)
}
@ -27,7 +27,7 @@ struct RedeemTokenCardQuantitySelectionViewModel {
}
var subtitleText: String {
let tokenTypeName = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore).getNameInPluralForm()
let tokenTypeName = assetDefinitionStore.xmlHandler(forTokenScriptSupportable: token).getNameInPluralForm()
return R.string.localizable.aWalletTokenRedeemQuantityTitle(tokenTypeName.localizedUppercase)
}
}

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><ts:token xmlns:ts="http://tokenscript.org/2020/06/tokenscript" xmlns:asnx="urn:ietf:params:xml:ns:asnx" xmlns:ethereum="urn:ethereum:constantinople" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" custodian="false" xsi:schemaLocation="http://tokenscript.org/2020/06/tokenscript http://tokenscript.org/2020/06/tokenscript.xsd">
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<ts:token xmlns:ts="http://tokenscript.org/2020/06/tokenscript" xmlns:asnx="urn:ietf:params:xml:ns:asnx" xmlns:ethereum="urn:ethereum:constantinople" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" custodian="false" xsi:schemaLocation="http://tokenscript.org/2020/06/tokenscript http://tokenscript.org/2020/06/tokenscript.xsd">
<asnx:module name="ERC20-Events">
<namedType name="MintOnDeposit">

@ -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)
}

@ -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)
}

@ -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)
}
}

@ -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"
}
}
}

@ -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<AnyCancellable>()
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)
}
}
}

@ -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

@ -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)
}
}
}

@ -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

@ -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<AnyCancellable>()
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 {

@ -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
}
}

@ -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,

@ -147,7 +147,7 @@ protocol TokenVerifiableStatusViewController: VerifiableStatusViewController {
extension TokenVerifiableStatusViewController {
var tokenScriptFileStatus: Promise<TokenLevelTokenScriptDisplayStatus> {
XMLHandler.functional.tokenScriptStatus(forContract: contract, assetDefinitionStore: assetDefinitionStore)
assetDefinitionStore.tokenScriptStatus(forContract: contract)
}
}
@ -160,6 +160,6 @@ protocol OptionalTokenVerifiableStatusViewController: VerifiableStatusViewContro
extension OptionalTokenVerifiableStatusViewController {
var tokenScriptFileStatus: Promise<TokenLevelTokenScriptDisplayStatus> {
guard let contract = contract else { return .value(.type0NoTokenScript) }
return XMLHandler.functional.tokenScriptStatus(forContract: contract, assetDefinitionStore: assetDefinitionStore)
return assetDefinitionStore.tokenScriptStatus(forContract: contract)
}
}

@ -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
])
}
}

@ -23,7 +23,7 @@ class FungibleTokenTabViewModel {
private let assetDefinitionStore: AssetDefinitionStore
private var cancelable = Set<AnyCancellable>()
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]

@ -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 {

@ -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)
}
}

@ -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()
}
}
}

@ -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()],

@ -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)
}

@ -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)
}

@ -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)
}
}

@ -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)
}
}

@ -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)
}

@ -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))
}
}

@ -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 }

File diff suppressed because one or more lines are too long

@ -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 {
</ts:token>
"""
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)
}

@ -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

@ -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

@ -7,6 +7,7 @@
import Foundation
import AlphaWalletAddress
import BigInt
//TODO remove AlphaWalletABI's dependency on TrustKeystore
import TrustKeystore
public indirect enum ABIValue: Equatable {

@ -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
}
}

@ -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
}
}

@ -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
}
}

@ -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

@ -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

@ -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
}

@ -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)"

@ -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"] = ""
}

@ -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:

@ -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)")

@ -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
}

@ -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:

@ -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)
}
}
}

@ -18,13 +18,8 @@ extension TokenScript {
public static func performTokenScriptAction(_ action: TokenInstanceAction, token: FoundationToken, tokenId: TokenId, tokenHolder: TokenHolder, userEntryIds: [String], fetchUserEntries: [Promise<Any?>], 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)

@ -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

@ -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)
}
}

@ -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

@ -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<Bool, SessionTaskError>] = [:]
private let blockchainProvider: BlockchainProvider

@ -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) }

@ -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)

@ -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
}
}
}

@ -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

@ -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)
}

@ -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)
}
}

@ -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) {

@ -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 []
}
}

@ -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<AlphaWallet.Address, PrivateXMLHandler> = .init()
private let xmlHandlersForAttestations: AtomicDictionary<URL, PrivateXMLHandler> = .init()
private let xmlHandlersForTokens: AtomicDictionary<AlphaWallet.Address, PrivateXMLHandler> = .init()
private let xmlHandlersForAttestations: AtomicDictionary<Attestation.AttestationId, 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 var attestationXmlChangeSubject: PassthroughSubject<Void, Never> = .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<AlphaWallet.Address, Never> {
signatureChangeSubject.eraseToAnyPublisher()
}
@ -69,7 +63,7 @@ public class AssetDefinitionStore: NSObject {
bodyChangeSubject.eraseToAnyPublisher()
}
public var attestationXMLChange: AnyPublisher<URL, Never> {
public var attestationXMLChange: AnyPublisher<Void, Never> {
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<TokenLevelTokenScriptDisplayStatus> {
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<TokenLevelTokenScriptDisplayStatus> {
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<TokenLevelTokenScriptDisplayStatus> {
//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<TokenType, String> = .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 {

@ -176,7 +176,7 @@ extension Array where Element == Subscribable<AssetInternalValue> {
each.publisher
.first()
.ignoreOutput()
.sink(receiveCompletion: { state in
.sink(receiveCompletion: { _ in
count += 1
guard count == self.count else { return }
seal.fulfill(Void())

@ -0,0 +1,20 @@
// Copyright © 2023 Stormbird PTE. LTD.
import Foundation
import AlphaWalletCore
public final class BaseTokenScriptFiles {
private let _baseTokenScriptFiles: AtomicDictionary<TokenType, String> = .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]
}
}

@ -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)
}
}

@ -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 }
}
}

@ -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<TokenLevelTokenScriptDisplayStatus> {
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<TokenLevelTokenScriptDisplayStatus> 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<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)
} 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<TokenScriptSignatureVerificationType> {
return Promise { seal in
if features.isActivityEnabled {
if tokenScriptFilesProvider.containsTokenScriptFile(for: xmlString) {
if baseTokenScriptFiles.containsBaseTokenScriptFile(for: xmlString) {
seal.fulfill(.verified(domainName: "*.aw.app"))
return
}

@ -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 }
}

@ -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)
}
}

@ -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)
}
}

@ -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? {

@ -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)
}

@ -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<TokenLevelTokenScriptDisplayStatus>
func computeTokenScriptStatus(forAttestationURL url: URL, xmlString: String) -> Promise<TokenLevelTokenScriptDisplayStatus>
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<TokenLevelTokenScriptDisplayStatus> {
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<TokenLevelTokenScriptDisplayStatus> 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<TokenLevelTokenScriptDisplayStatus> {
//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<TokenScriptSignatureVerificationType> {
if let cachedVerificationType = backingStore.getCacheTokenScriptSignatureVerificationType(forXmlString: xmlString) {
return .value(cachedVerificationType)
} else {
return signatureVerifier.verificationType(forXml: xmlString)
}
}
}

@ -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) }
}

@ -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) {

@ -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<TokenLevelTokenScriptDisplayStatus>!
@ -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<TokenLevelTokenScriptDisplayStatus>!
@ -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..<xml.endIndex, in: xml)) { match, _, _ in
@ -796,15 +843,72 @@ public class PrivateXMLHandler {
guard match.numberOfRanges == 3 else { return }
guard let entityRange = Range(match.range(at: 1), in: xml), let fileNameRange = Range(match.range(at: 2), in: xml) else { return }
let entityName = String(xml[entityRange])
let fileName = String(xml[fileNameRange])
entities.append(.init(name: entityName, fileName: fileName))
let fileName = Filename(value: String(xml[fileNameRange]))
entities.append(XMLHandler.Entity(name: entityName, fileName: fileName))
}
}
return entities
}
static func getAttestationSchemaUid(xmlString: String) -> 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<TokenLevelTokenScriptDisplayStatus> {
var tokenScriptStatus: Promise<TokenLevelTokenScriptDisplayStatus>!
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<TokenLevelTokenScriptDisplayStatus> {
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

@ -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()
}

Loading…
Cancel
Save