Merge pull request #3161 from AlphaWallet/erc1155-wip

WIP support for ERC1155 backend
pull/3130/head
Hwee-Boon Yar 3 years ago committed by GitHub
commit 9aa96d7284
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      AlphaWallet.xcodeproj/project.pbxproj
  2. 4
      AlphaWallet/Activities/ActivitiesService.swift
  3. 6
      AlphaWallet/Activities/ViewModels/ActivitiesViewModel.swift
  4. 2
      AlphaWallet/Browser/Coordinators/QRCodeResolutionCoordinator.swift
  5. 2
      AlphaWallet/Core/Coordinators/CustomUrlSchemeCoordinator.swift
  6. 6
      AlphaWallet/Core/Coordinators/FilterTokensCoordinator.swift
  7. 300
      AlphaWallet/Core/Coordinators/WalletBalance/PrivateBalanceFetcher.swift
  8. 37
      AlphaWallet/Core/Coordinators/WalletBalance/PrivateTokensDataStoreType.swift
  9. 2
      AlphaWallet/Core/Coordinators/WalletBalance/WalletBalanceFetcher.swift
  10. 12
      AlphaWallet/Core/Ethereum/ABI/ABI.swift
  11. 67
      AlphaWallet/Core/Ethereum/ABI/ERC1155.json
  12. 2
      AlphaWallet/Core/SwapToken/SwapTokenService.swift
  13. 36
      AlphaWallet/Core/Types/TokenProviderType.swift
  14. 6
      AlphaWallet/EtherClient/OpenSea.swift
  15. 5
      AlphaWallet/EtherClient/TrustClient/Models/OperationType.swift
  16. 2
      AlphaWallet/EtherClient/TrustClient/Models/RawTransaction.swift
  17. 3
      AlphaWallet/InCoordinator.swift
  18. 2
      AlphaWallet/Market/OrderHandler.swift
  19. 2
      AlphaWallet/Market/UniversalLinkHandler.swift
  20. 2
      AlphaWallet/Redeem/ViewControllers/TokenCardRedemptionViewController.swift
  21. 2
      AlphaWallet/TokenScriptClient/Coordinators/EventSourceCoordinatorForActivities.swift
  22. 2
      AlphaWallet/TokenScriptClient/Coordinators/FetchAssetDefinitionsCoordinator.swift
  23. 2
      AlphaWallet/TokenScriptClient/Models/Activity.swift
  24. 1
      AlphaWallet/TokenScriptClient/Models/TokenInterfaceType.swift
  25. 4
      AlphaWallet/TokenScriptClient/Models/XMLHandler.swift
  26. 39
      AlphaWallet/Tokens/Coordinators/GetIsERC1155ContractCoordinator.swift
  27. 13
      AlphaWallet/Tokens/Coordinators/SingleChainTokenCoordinator.swift
  28. 2
      AlphaWallet/Tokens/Coordinators/TokensCoordinator.swift
  29. 12
      AlphaWallet/Tokens/Helpers/TokenAdaptor.swift
  30. 6
      AlphaWallet/Tokens/Logic/ContractDataDetector.swift
  31. 242
      AlphaWallet/Tokens/Logic/Erc1155TokenIdsFetcher.swift
  32. 23
      AlphaWallet/Tokens/Models/Erc721Contract.swift
  33. 11
      AlphaWallet/Tokens/Types/NonFungibleFromJson.swift
  34. 25
      AlphaWallet/Tokens/Types/NonFungibleFromJsonTokenType.swift
  35. 32
      AlphaWallet/Tokens/Types/NonFungibleFromTokenUri.swift
  36. 30
      AlphaWallet/Tokens/Types/OpenSeaNonFungible.swift
  37. 12
      AlphaWallet/Tokens/Types/TokenObject.swift
  38. 13
      AlphaWallet/Tokens/Types/TokenType.swift
  39. 40
      AlphaWallet/Tokens/Types/TokensDataStore.swift
  40. 4
      AlphaWallet/Tokens/ViewControllers/NewTokenViewController.swift
  41. 2
      AlphaWallet/Tokens/ViewControllers/SelectAssetViewController.swift
  42. 2
      AlphaWallet/Tokens/ViewControllers/TokenInstanceActionViewController.swift
  43. 5
      AlphaWallet/Tokens/ViewControllers/TokenViewController.swift
  44. 4
      AlphaWallet/Tokens/ViewControllers/TokensViewController.swift
  45. 2
      AlphaWallet/Tokens/ViewModels/TokenInstanceViewModel.swift
  46. 8
      AlphaWallet/Tokens/ViewModels/TokenViewControllerViewModel.swift
  47. 2
      AlphaWallet/Tokens/ViewModels/TokensCardViewModel.swift
  48. 9
      AlphaWallet/Tokens/ViewModels/TokensViewModel.swift
  49. 4
      AlphaWallet/Transactions/Coordinators/ReplaceTransactionCoordinator.swift
  50. 4
      AlphaWallet/Transactions/Coordinators/TokensCardCoordinator.swift
  51. 2
      AlphaWallet/Transactions/Storage/TransactionsStorage.swift
  52. 2
      AlphaWallet/Transactions/ViewModels/TransactionRowCellViewModel.swift
  53. 5
      AlphaWallet/Transfer/Controllers/TransactionConfigurator.swift
  54. 2
      AlphaWallet/Transfer/Coordinators/SendCoordinator.swift
  55. 13
      AlphaWallet/Transfer/Types/TransactionType.swift
  56. 14
      AlphaWallet/Transfer/ViewControllers/SendViewController.swift
  57. 4
      AlphaWallet/Transfer/ViewControllers/TransactionConfirmationViewController.swift
  58. 2
      AlphaWallet/Transfer/ViewModels/ConfigureTransactionViewModel.swift
  59. 2
      AlphaWallet/Transfer/ViewModels/SendHeaderViewViewModel.swift
  60. 14
      AlphaWallet/Transfer/ViewModels/SendViewModel.swift
  61. 2
      AlphaWallet/Transfer/ViewModels/TransactionConfirmationViewModel.swift
  62. 4
      AlphaWallet/UI/TokenObject+UI.swift

@ -355,6 +355,7 @@
5E7C74BD08801CABF9695853 /* LocaleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C79778E4BFE1322711EA6 /* LocaleViewModel.swift */; };
5E7C74C1C2AB84F9AFAC630E /* TokenCardRowViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C12E88EB0B73AA1E562 /* TokenCardRowViewModelProtocol.swift */; };
5E7C74C6110D4E93C759D5DB /* ConfirmSignMessageTableViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7B080E387A79058430B9 /* ConfirmSignMessageTableViewCellViewModel.swift */; };
5E7C74D15D658E785D44CBB6 /* GetIsERC1155ContractCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C777C420B7E1C1E6FCF00 /* GetIsERC1155ContractCoordinator.swift */; };
5E7C74DBAE43954C185057B3 /* ChooseTokenCardTransferModeViewControllerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7BA578BE5FB0E613A6D6 /* ChooseTokenCardTransferModeViewControllerViewModel.swift */; };
5E7C74E1ECC13899FA369E1C /* ClearDappBrowserCacheCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C72571AB0FECB26FEB1B1 /* ClearDappBrowserCacheCoordinator.swift */; };
5E7C74E7DC2D79785240D757 /* GetERC875Balance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7251DB61EB9468910C81 /* GetERC875Balance.swift */; };
@ -455,6 +456,7 @@
5E7C785C39CC8243BEC1219C /* PromptViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7E50E9184C7F0FE3966C /* PromptViewModel.swift */; };
5E7C786AD8E4877C36D3B14A /* TokenAdaptorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C775FD95FE80B0F1CEA33 /* TokenAdaptorTest.swift */; };
5E7C786DB5B71302FF66CBAD /* SendTransactionErrorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7BB7DBFDF5A5E4F2DDC0 /* SendTransactionErrorViewModel.swift */; };
5E7C7883FB31565411F7C928 /* NonFungibleFromJsonTokenType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C78EBC7D0A09B8EAACBE3 /* NonFungibleFromJsonTokenType.swift */; };
5E7C788C8830A7CF05B9189A /* OpenSea.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C0CFD047ED7C488FB45 /* OpenSea.swift */; };
5E7C7896E99049F4B124FDF5 /* OpenSeaNonFungibleTokenDisplayHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7A9876B43B1D9D17A9A9 /* OpenSeaNonFungibleTokenDisplayHelper.swift */; };
5E7C78A31A16600FBA5C9956 /* ScanQRCodeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7721E0E4D4EFDD35E196 /* ScanQRCodeCoordinator.swift */; };
@ -581,6 +583,7 @@
5E7C7DFC309C7CC499202375 /* DecodedFunctionCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7A71851FA227231270BD /* DecodedFunctionCall.swift */; };
5E7C7E02785866606FF298F3 /* OpenSeaNonFungibleTokenViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C72FBC0D2787AAA804098 /* OpenSeaNonFungibleTokenViewCellViewModel.swift */; };
5E7C7E04D4DDD7D8881A2AB1 /* UniversalLinkCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C76AF81B8DFF605558499 /* UniversalLinkCoordinator.swift */; };
5E7C7E1C2976FA57D53BC2CE /* ERC1155.json in Resources */ = {isa = PBXBuildFile; fileRef = 5E7C77B85B39F8A7C1FB3B40 /* ERC1155.json */; };
5E7C7E2109BCEB05899A4F9A /* DefaultActivityItemViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C76B3FD690DC23263DE26 /* DefaultActivityItemViewCell.swift */; };
5E7C7E2BCAB70E73795B5B48 /* EtherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7228C9BEB801D4CD34DE /* EtherTests.swift */; };
5E7C7E2E47ED7EDD5C127D1D /* HistoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7AB627E023FFD95F2276 /* HistoryStore.swift */; };
@ -613,6 +616,7 @@
5E7C7EF1F2CDFA52BBF1C620 /* BrowserHistoryCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C712F42374C0B8DF8C64F /* BrowserHistoryCellViewModel.swift */; };
5E7C7F01A771565A1BCF7FFA /* SeedPhraseCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C77BCBD2C2BE682D384DB /* SeedPhraseCollectionView.swift */; };
5E7C7F1623D246AD32378D29 /* PromptBackupWalletViewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C796C7DEA3C2A70861828 /* PromptBackupWalletViewViewModel.swift */; };
5E7C7F173802073A6CB79813 /* Erc1155TokenIdsFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7CE6CBCB68739F466132 /* Erc1155TokenIdsFetcher.swift */; };
5E7C7F1B297CE042114EF095 /* LockEnterPasscodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C75CBBFF0273EF476F95B /* LockEnterPasscodeViewController.swift */; };
5E7C7F2284231870623C5605 /* TokenScriptCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C79C0C9631E4DFC4A40DF /* TokenScriptCard.swift */; };
5E7C7F3CB1F280E6795A479C /* EventOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C740BBC5AAF5C545CCC6A /* EventOrigin.swift */; };
@ -1351,6 +1355,7 @@
5E7C77685B78D5372F6C7CB0 /* PromptBackupWalletAfterExceedingThresholdViewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PromptBackupWalletAfterExceedingThresholdViewViewModel.swift; sourceTree = "<group>"; };
5E7C776B129861728FFB8CC8 /* EditMyDappViewControllerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditMyDappViewControllerViewModel.swift; sourceTree = "<group>"; };
5E7C7778166E01A0D483C58D /* SecureEnclave.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureEnclave.swift; sourceTree = "<group>"; };
5E7C777C420B7E1C1E6FCF00 /* GetIsERC1155ContractCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetIsERC1155ContractCoordinator.swift; sourceTree = "<group>"; };
5E7C778A54D7D3E196BC5542 /* DAppRequster.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DAppRequster.swift; sourceTree = "<group>"; };
5E7C778F20D32B70D7FF2135 /* TokenCardRedemptionInfoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenCardRedemptionInfoViewController.swift; sourceTree = "<group>"; };
5E7C77A1D9BAFAC05E2C96CC /* XMLHandler+XPaths.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "XMLHandler+XPaths.swift"; sourceTree = "<group>"; };
@ -1358,6 +1363,7 @@
5E7C77A8E9F8DF94ED53D452 /* WebImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebImageView.swift; sourceTree = "<group>"; };
5E7C77AED5BC29ED8D075D08 /* SeedPhraseBackupIntroductionViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeedPhraseBackupIntroductionViewModel.swift; sourceTree = "<group>"; };
5E7C77B790551456E111ED4F /* PeekOpenSeaNonFungibleTokenViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeekOpenSeaNonFungibleTokenViewController.swift; sourceTree = "<group>"; };
5E7C77B85B39F8A7C1FB3B40 /* ERC1155.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ERC1155.json; sourceTree = "<group>"; };
5E7C77BCBD2C2BE682D384DB /* SeedPhraseCollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeedPhraseCollectionView.swift; sourceTree = "<group>"; };
5E7C77C2844B3579A59C3F2F /* CallSmartContractFunction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallSmartContractFunction.swift; sourceTree = "<group>"; };
5E7C77DBECDF831CD6C8FF71 /* Environment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = "<group>"; };
@ -1394,6 +1400,7 @@
5E7C78CF45AA54EF8647C44B /* SeedPhraseBackupIntroductionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeedPhraseBackupIntroductionViewController.swift; sourceTree = "<group>"; };
5E7C78E0F20F882DD2136E29 /* GasNowPriceEstimates.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GasNowPriceEstimates.swift; sourceTree = "<group>"; };
5E7C78E5C8FAEA752B32626D /* UIActivityViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIActivityViewController.swift; sourceTree = "<group>"; };
5E7C78EBC7D0A09B8EAACBE3 /* NonFungibleFromJsonTokenType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NonFungibleFromJsonTokenType.swift; sourceTree = "<group>"; };
5E7C78EFAF641C41F06C46BF /* ServersCoordinatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServersCoordinatorTests.swift; sourceTree = "<group>"; };
5E7C78FAB9070B10A476DB29 /* AssetImplicitAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetImplicitAttributes.swift; sourceTree = "<group>"; };
5E7C78FF8A5682C27E15B488 /* UITableViewCell+TokenCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableViewCell+TokenCell.swift"; sourceTree = "<group>"; };
@ -1510,6 +1517,7 @@
5E7C7CD1FB7D353704EF3389 /* DateEntryField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DateEntryField.swift; path = Views/DateEntryField.swift; sourceTree = "<group>"; };
5E7C7CD7ABB18C1121D5776F /* LiveLocaleSwitcherBundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveLocaleSwitcherBundle.swift; sourceTree = "<group>"; };
5E7C7CDB0BAD5D27D2F24F57 /* ServerViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerViewCell.swift; sourceTree = "<group>"; };
5E7C7CE6CBCB68739F466132 /* Erc1155TokenIdsFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Erc1155TokenIdsFetcher.swift; sourceTree = "<group>"; };
5E7C7CE6E3560E773D2287E2 /* ElevateWalletSecurityCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ElevateWalletSecurityCoordinator.swift; sourceTree = "<group>"; };
5E7C7CF1465A1DCB44371BA9 /* ConsoleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConsoleViewController.swift; sourceTree = "<group>"; };
5E7C7CFDE7DEA8C06C4100AF /* TextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = "<group>"; };
@ -2601,6 +2609,7 @@
8703F66626135B330082EE25 /* ChartHistory.swift */,
5E7C73EA8FD43F18DDEB6965 /* NonFungibleFromTokenUri.swift */,
5E7C7B38E58247C6A3715BC4 /* NonFungibleFromJson.swift */,
5E7C78EBC7D0A09B8EAACBE3 /* NonFungibleFromJsonTokenType.swift */,
);
path = Types;
sourceTree = "<group>";
@ -3028,6 +3037,7 @@
87ED843A24C564B5001A3747 /* NewTokenCoordinator.swift */,
5E7C75BCE63C31EC8ED403A7 /* GetBlockTimestampCoordinator.swift */,
5E7C769E7A78811BDF9463A3 /* GetWalletNameCoordinator.swift */,
5E7C777C420B7E1C1E6FCF00 /* GetIsERC1155ContractCoordinator.swift */,
);
path = Coordinators;
sourceTree = "<group>";
@ -3712,6 +3722,7 @@
isa = PBXGroup;
children = (
5E7C7F9030DA27EF7C3FBDEB /* ContractDataDetector.swift */,
5E7C7CE6CBCB68739F466132 /* Erc1155TokenIdsFetcher.swift */,
);
path = Logic;
sourceTree = "<group>";
@ -4356,6 +4367,7 @@
87DB61C0264A94CD009FBDDE /* ABI.swift */,
87DB61C2264A9611009FBDDE /* ERC20.json */,
87713EAD264ABF2800B1B9CB /* DecodedFunctionCall+Decode.swift */,
5E7C77B85B39F8A7C1FB3B40 /* ERC1155.json */,
);
path = ABI;
sourceTree = "<group>";
@ -4686,6 +4698,7 @@
5E7C7F808D0B0B19ECEA6623 /* ETH.tsml in Resources */,
5E7C706322B52C0D0CED8018 /* aETH.tsml in Resources */,
5E7C7CE29C74DF7FDDDB6A0D /* ERC721-TokenScript.tsml in Resources */,
5E7C7E1C2976FA57D53BC2CE /* ERC1155.json in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -5758,6 +5771,9 @@
5E7C732AA43385E80681B24F /* EmailList.swift in Sources */,
5E7C776B88B6CDC18CA39D72 /* Environment.swift in Sources */,
5E7C7DE51E4829295CABDC21 /* ContractDataDetector.swift in Sources */,
5E7C7883FB31565411F7C928 /* NonFungibleFromJsonTokenType.swift in Sources */,
5E7C74D15D658E785D44CBB6 /* GetIsERC1155ContractCoordinator.swift in Sources */,
5E7C7F173802073A6CB79813 /* Erc1155TokenIdsFetcher.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

@ -51,7 +51,7 @@ extension TransactionType {
return .nativeCryptocurrency(primaryKey: tokenObject.primaryKey)
case .ERC20Token(let tokenObject, _, _):
return .erc20(contract: tokenObject.contractAddress)
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .dapp, .claimPaidErc875MagicLink, .tokenScript:
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .ERC1155Token, .dapp, .claimPaidErc875MagicLink, .tokenScript:
return .none
}
}
@ -632,7 +632,7 @@ fileprivate func == (activity: Activity, operation: LocalizedOperationObjectInst
return (activity.nativeViewType == .erc20Sent || activity.nativeViewType == .erc20Received) && isSameAmount() && isSameFrom() && isSameTo()
case .erc20TokenApprove:
return activity.nativeViewType == .erc20OwnerApproved || activity.nativeViewType == .erc20ApprovalObtained || activity.nativeViewType == .erc721OwnerApproved || activity.nativeViewType == .erc721ApprovalObtained
case .erc721TokenTransfer:
case .erc721TokenTransfer, .erc1155TokenTransfer:
return (activity.nativeViewType == .erc721Sent || activity.nativeViewType == .erc721Received) && isSameAmount() && isSameFrom() && isSameTo()
case .erc875TokenTransfer:
return false

@ -266,12 +266,12 @@ extension ActivitiesViewModel.functional {
if transactionRow.operation == nil {
erc20TokenOperation = .nativeCryptoTransfer(TokensDataStore.etherToken(forServer: transactionRow.server))
} else {
//Explicitly listing out combinations so future changes to enums will be caught by compiler
switch (transactionRow.state, transactionRow.operation?.operationType) {
case (.pending, .nativeCurrencyTokenTransfer), (.pending, .erc20TokenTransfer), (.pending, .erc721TokenTransfer), (.pending, .erc875TokenTransfer):
case (.pending, .nativeCurrencyTokenTransfer), (.pending, .erc20TokenTransfer), (.pending, .erc721TokenTransfer), (.pending, .erc875TokenTransfer), (.pending, .erc1155TokenTransfer):
erc20TokenOperation = transactionRow.operation?.contractAddress.flatMap { cache.tokenObject(address: $0, server: transactionRow.server) }.flatMap { TokenOperation.pendingTransfer($0) }
case (.completed, .nativeCurrencyTokenTransfer), (.completed, .erc20TokenTransfer), (.completed, .erc721TokenTransfer), (.completed, .erc875TokenTransfer):
case (.completed, .nativeCurrencyTokenTransfer), (.completed, .erc20TokenTransfer), (.completed, .erc721TokenTransfer), (.completed, .erc875TokenTransfer), (.completed, .erc1155TokenTransfer):
erc20TokenOperation = transactionRow.operation?.contractAddress.flatMap { cache.tokenObject(address: $0, server: transactionRow.server) }.flatMap { TokenOperation.completedTransfer($0) }
//Explicitly listing out combinations so future changes to enums will be caught by compiler
case (.pending, .erc20TokenApprove):
erc20TokenOperation = transactionRow.operation?.contractAddress.flatMap { cache.tokenObject(address: $0, server: transactionRow.server) }.flatMap { TokenOperation.pendingErc20Approval($0) }
case (.completed, .erc20TokenApprove):

@ -264,7 +264,7 @@ extension QRCodeResolutionCoordinator: ScanQRCodeCoordinatorDelegate {
decimals: Int(decimals),
type: .erc20,
balance: ["0"]
))
), shouldUpdateBalance: true)
let amount = maybeScientificAmountString.scientificAmountToBigInt.flatMap {
EtherNumberFormatter.full.string(from: $0, decimals: token.decimals)
}

@ -58,7 +58,7 @@ class CustomUrlSchemeCoordinator: Coordinator {
type: .erc20,
balance: ["0"]
)
tokensDatastore.addCustom(token: token)
tokensDatastore.addCustom(token: token, shouldUpdateBalance: true)
strongSelf.openSendPayFlowFor(server: server, contract: contract, recipient: recipient, amount: amount)
case .delegateTokenComplete:
break

@ -32,7 +32,7 @@ class FilterTokensCoordinator {
case .assetsOnly:
filteredTokens = tokens.filter { $0.type != .nativeCryptocurrency && $0.type != .erc20 }
case .collectiblesOnly:
filteredTokens = tokens.filter { $0.type == .erc721 && !$0.balance.isEmpty }
filteredTokens = tokens.filter { ($0.type == .erc721 || $0.type == .erc1155) && !$0.balance.isEmpty }
case .keyword(let keyword):
let lowercasedKeyword = keyword.trimmed.lowercased()
if lowercasedKeyword.isEmpty {
@ -45,6 +45,8 @@ class FilterTokensCoordinator {
return $0.type == .erc721
} else if lowercasedKeyword == "erc875" || lowercasedKeyword == "erc 875" {
return $0.type == .erc875
} else if lowercasedKeyword == "erc1155" || lowercasedKeyword == "erc 1155" {
return $0.type == .erc1155
} else if lowercasedKeyword == "tokenscript" {
let xmlHandler = XMLHandler(token: $0, assetDefinitionStore: assetDefinitionStore)
return xmlHandler.hasNoBaseAssetDefinition && (xmlHandler.server?.matches(server: $0.server) ?? false)
@ -64,7 +66,7 @@ class FilterTokensCoordinator {
return filteredTokens
}
func filterTokens(tokens: [PopularToken], walletTokens: [TokenObject], filter: WalletFilter) -> [PopularToken] {
var filteredTokens: [PopularToken] = tokens.filter { token in
!walletTokens.contains(where: { $0.contractAddress.sameContract(as: token.contractAddress) }) && !token.name.isEmpty

@ -25,6 +25,8 @@ protocol PrivateBalanceFetcherType: AnyObject {
}
class PrivateBalanceFetcher: PrivateBalanceFetcherType {
typealias TokenIdMetaData = (contract: AlphaWallet.Address, tokenId: BigUInt, json: String, value: BigInt)
static let fetchContractDataTimeout = TimeInterval(4)
//Unlike `SessionManager.default`, this doesn't add default HTTP headers. It looks like POAP token URLs (e.g. https://api.poap.xyz/metadata/2503/278569) don't like them and return `406` in the JSON. It's strangely not responsible when curling, but only when running in the app
private var sessionManagerWithDefaultHttpHeaders: SessionManager = {
@ -80,7 +82,7 @@ class PrivateBalanceFetcher: PrivateBalanceFetcherType {
}
deinit {
enabledObjectsObservation.flatMap{ $0.invalidate() }
enabledObjectsObservation.flatMap { $0.invalidate() }
}
private func getTokensFromOpenSea() -> OpenSea.PromiseResult {
@ -93,13 +95,13 @@ class PrivateBalanceFetcher: PrivateBalanceFetcherType {
refreshBalance(tokenObjects: Array(tokenObjects), updatePolicy: .all, force: false)
}
private func refreshBalanceForTokensThatAreNotNonTicket721(tokens: [Activity.AssignedToken], group: DispatchGroup) {
assert(!tokens.contains { $0.isERC721AndNotForTickets })
private func refreshBalanceForNonErc721Or1155Tokens(tokens: [Activity.AssignedToken], group: DispatchGroup) {
assert(!tokens.contains { $0.isERC721Or1155AndNotForTickets })
for tokenObject in tokens {
group.enter()
refreshBalance(forToken: tokenObject, tokensDatastore: tokensDatastore) { [weak self] balanceValueHasChange in
refreshBalanceForNonErc721Or1155Tokens(forToken: tokenObject, tokensDatastore: tokensDatastore) { [weak self] balanceValueHasChange in
guard let strongSelf = self, let delegate = strongSelf.delegate else {
group.leave()
return
@ -168,14 +170,14 @@ class PrivateBalanceFetcher: PrivateBalanceFetcherType {
private func refreshBalance(tokenObjects: [Activity.AssignedToken], group: DispatchGroup) {
let updateTokens = tokenObjects.filter { $0 != etherToken }
let nonERC721Tokens = updateTokens.filter { !$0.isERC721AndNotForTickets }
let erc721Tokens = updateTokens.filter { $0.isERC721AndNotForTickets }
let notErc721Or1155Tokens = updateTokens.filter { !$0.isERC721Or1155AndNotForTickets }
let erc721Or1155Tokens = updateTokens.filter { $0.isERC721Or1155AndNotForTickets }
refreshBalanceForTokensThatAreNotNonTicket721(tokens: nonERC721Tokens, group: group)
refreshBalanceForERC721Tokens(tokens: erc721Tokens, group: group)
refreshBalanceForNonErc721Or1155Tokens(tokens: notErc721Or1155Tokens, group: group)
refreshBalanceForErc721Or1155Tokens(tokens: erc721Or1155Tokens, group: group)
}
private func refreshBalance(forToken tokenObject: Activity.AssignedToken, tokensDatastore: PrivateTokensDatastoreType, completion: @escaping (Bool?) -> Void) {
private func refreshBalanceForNonErc721Or1155Tokens(forToken tokenObject: Activity.AssignedToken, tokensDatastore: PrivateTokensDatastoreType, completion: @escaping (Bool?) -> Void) {
switch tokenObject.type {
case .nativeCryptocurrency:
completion(nil)
@ -197,7 +199,7 @@ class PrivateBalanceFetcher: PrivateBalanceFetcherType {
completion(nil)
}
})
case .erc721:
case .erc721, .erc1155:
break
case .erc721ForTickets:
tokenProvider.getERC721ForTicketsBalance(for: tokenObject.contractAddress, completion: { result in
@ -211,21 +213,21 @@ class PrivateBalanceFetcher: PrivateBalanceFetcherType {
}
}
private func refreshBalanceForERC721Tokens(tokens: [Activity.AssignedToken], group: DispatchGroup) {
assert(!tokens.contains { !$0.isERC721AndNotForTickets })
private func refreshBalanceForErc721Or1155Tokens(tokens: [Activity.AssignedToken], group: DispatchGroup) {
assert(!tokens.contains { !$0.isERC721Or1155AndNotForTickets })
firstly {
getTokensFromOpenSea()
}.done { [weak self] contractToOpenSeaNonFungibles in
guard let strongSelf = self else { return }
let erc721ContractsFoundInOpenSea = Array(contractToOpenSeaNonFungibles.keys).map { $0 }
let erc721ContractsNotFoundInOpenSea = tokens.map { $0.contractAddress } - erc721ContractsFoundInOpenSea
strongSelf.updateNonOpenSeaNonFungiblesBalance(erc721ContractsNotFoundInOpenSea: erc721ContractsNotFoundInOpenSea, tokens: tokens, group: group)
let erc721Or1155ContractsFoundInOpenSea = Array(contractToOpenSeaNonFungibles.keys).map { $0 }
let erc721Or1155ContractsNotFoundInOpenSea = tokens.map { $0.contractAddress } - erc721Or1155ContractsFoundInOpenSea
strongSelf.updateNonOpenSeaNonFungiblesBalance(contracts: erc721Or1155ContractsNotFoundInOpenSea, tokens: tokens, group: group)
strongSelf.updateOpenSeaNonFungiblesBalanceAndAttributes(contractToOpenSeaNonFungibles: contractToOpenSeaNonFungibles, tokens: tokens, group: group)
}.cauterize()
}
private func updateNonOpenSeaNonFungiblesBalance(erc721ContractsNotFoundInOpenSea contracts: [AlphaWallet.Address], tokens: [Activity.AssignedToken], group: DispatchGroup) {
let promises = contracts.map { updateNonOpenSeaNonFungiblesBalance(contract: $0, tokens: tokens, tokensDatastore: tokensDatastore) }
private func updateNonOpenSeaNonFungiblesBalance(contracts: [AlphaWallet.Address], tokens: [Activity.AssignedToken], group: DispatchGroup) {
let promises = contracts.map { updateNonOpenSeaNonFungiblesBalance(erc721Or1115ContractNotFoundInOpenSea: $0, tokens: tokens, tokensDatastore: tokensDatastore) }
group.enter()
firstly {
@ -235,7 +237,17 @@ class PrivateBalanceFetcher: PrivateBalanceFetcherType {
}
}
private func updateNonOpenSeaNonFungiblesBalance(contract: AlphaWallet.Address, tokens: [Activity.AssignedToken], tokensDatastore: PrivateTokensDatastoreType) -> Promise<Void> {
private func updateNonOpenSeaNonFungiblesBalance(erc721Or1115ContractNotFoundInOpenSea contract: AlphaWallet.Address, tokens: [Activity.AssignedToken], tokensDatastore: PrivateTokensDatastoreType) -> Promise<Void> {
let erc721Promise = updateNonOpenSeaErc721Balance(contract: contract, tokens: tokens, tokensDatastore: tokensDatastore)
let erc1155Promise: Promise<Void> = Promise.value(())
return firstly {
when(fulfilled: erc721Promise, erc1155Promise)
}.map { _, _ in
//no-op
}.asVoid()
}
private func updateNonOpenSeaErc721Balance(contract: AlphaWallet.Address, tokens: [Activity.AssignedToken], tokensDatastore: PrivateTokensDatastoreType) -> Promise<Void> {
guard let erc721TokenIdsFetcher = erc721TokenIdsFetcher else { return Promise { _ in } }
return firstly {
erc721TokenIdsFetcher.tokenIdsForErc721Token(contract: contract, inAccount: account.address)
@ -255,26 +267,141 @@ class PrivateBalanceFetcher: PrivateBalanceFetcherType {
}.asVoid()
}
private func updateNonOpenSeaErc1155Balance(contract: AlphaWallet.Address, tokens: [Activity.AssignedToken], tokensDatastore: PrivateTokensDatastoreType) -> Promise<Void> {
guard let contractsTokenIdsAndValue = Erc1155TokenIdsFetcher(address: account.address, server: server).readJson() else {
return .value(())
}
return firstly {
addUnknownErc1155ContractsToDatabase(contractsTokenIdsAndValue: contractsTokenIdsAndValue.tokens, tokens: tokens)
}.then { (contractsTokenIdsAndValue: Erc1155TokenIds.ContractsTokenIdsAndValues) -> Promise<[TokenIdMetaData]> in
self.fetchErc1155NonFungibleJsons(contractsTokenIdsAndValue: contractsTokenIdsAndValue, tokens: tokens)
}.then { (results: [TokenIdMetaData]) -> Promise<Void> in
self.updateErc1155TokenIdBalancesInDatabase(tokenIdsData: results, tokens: tokens)
}
//TODO log error remotely
}
private func addUnknownErc1155ContractsToDatabase(contractsTokenIdsAndValue: Erc1155TokenIds.ContractsTokenIdsAndValues, tokens: [Activity.AssignedToken]) -> Promise<Erc1155TokenIds.ContractsTokenIdsAndValues> {
firstly {
functional.fetchUnknownErc1155ContractsDetails(contractsTokenIdsAndValue: contractsTokenIdsAndValue, tokens: tokens, server: server, account: account, assetDefinitionStore: assetDefinitionStore)
}.then { tokensToAdd -> Promise<Erc1155TokenIds.ContractsTokenIdsAndValues> in
let (promise, seal) = Promise<Erc1155TokenIds.ContractsTokenIdsAndValues>.pending()
self.tokensDatastore.addCustom(tokens: tokensToAdd, shouldUpdateBalance: false) {
seal.fulfill(contractsTokenIdsAndValue)
}
return promise
}
}
private func fetchErc1155NonFungibleJsons(contractsTokenIdsAndValue: Erc1155TokenIds.ContractsTokenIdsAndValues, tokens: [Activity.AssignedToken]) -> Promise<[TokenIdMetaData]> {
var allGuarantees: [Guarantee<TokenIdMetaData>] = .init()
for (contract, tokenIdsAndValues) in contractsTokenIdsAndValue {
let tokenIds = tokenIdsAndValues.keys
let guarantees: [Guarantee<TokenIdMetaData>] = tokenIds.map { tokenId -> Guarantee<TokenIdMetaData> in
fetchNonFungibleJson(forTokenId: String(tokenId), address: contract, tokens: tokens).map { jsonString -> TokenIdMetaData in
(contract: contract, tokenId: tokenId, json: jsonString, value: tokenIdsAndValues[tokenId]!)
}
}
allGuarantees.append(contentsOf: guarantees)
}
return when(fulfilled: allGuarantees)
}
private func updateErc1155TokenIdBalancesInDatabase(tokenIdsData: [TokenIdMetaData], tokens: [Activity.AssignedToken]) -> Promise<Void> {
let group: DispatchGroup = .init()
let (promise, seal) = Promise<Void>.pending()
var contractsTokenIdsAndValue: [AlphaWallet.Address: [BigUInt: String]] = .init()
for (contract, tokenId, json, value) in tokenIdsData {
var tokenIdsAndValue: [BigUInt: String] = contractsTokenIdsAndValue[contract] ?? .init()
tokenIdsAndValue[tokenId] = json
contractsTokenIdsAndValue[contract] = tokenIdsAndValue
}
for (contract, tokenIdsAndJsons) in contractsTokenIdsAndValue {
guard let tokenObject = tokens.first(where: { $0.contractAddress.sameContract(as: contract) }) else {
assertImpossibleCodePath(message: "ERC1155 contract: \(contract.eip55String) not found in database when setting balance for 1155")
return promise
}
let jsons: [String] = Array(tokenIdsAndJsons.values)
group.enter()
tokensDatastore.update(primaryKey: tokenObject.primaryKey, action: .nonFungibleBalance(jsons)) { _ in
group.leave()
}
group.enter()
tokensDatastore.update(primaryKey: tokenObject.primaryKey, action: .type(.erc1155)) { _ in
group.leave()
}
}
group.notify(queue: .main) {
seal.fulfill(())
}
return promise
}
//Misnomer, we call this "nonFungible", but this includes ERC1155 which can contain (semi-)fungibles, but there's no better name
private func fetchNonFungibleJson(forTokenId tokenId: String, address: AlphaWallet.Address, tokens: [Activity.AssignedToken]) -> Guarantee<String> {
firstly {
Erc721Contract(server: server).getErc721TokenUri(for: tokenId, contract: address)
}.then {
self.fetchTokenJson(forTokenId: tokenId, uri: $0, address: address, tokens: tokens)
}.recover { _ in
var jsonDictionary = JSON()
if let tokenObject = tokens.first(where: { $0.contractAddress.sameContract(as: address) }) {
jsonDictionary["tokenId"] = JSON(tokenId)
jsonDictionary["contractName"] = JSON(tokenObject.name)
jsonDictionary["symbol"] = JSON(tokenObject.symbol)
jsonDictionary["name"] = ""
jsonDictionary["imageUrl"] = ""
jsonDictionary["thumbnailUrl"] = ""
jsonDictionary["externalLink"] = ""
}.recover { e in
return self.generateTokenJsonFallback(forTokenId: tokenId, address: address, tokens: tokens)
}
}
private func fetchTokenJson(forTokenId tokenId: String, uri originalUri: URL, address: AlphaWallet.Address, tokens: [TokenObject]) -> Promise<String> {
struct Error: Swift.Error {
}
let uri = originalUri.rewrittenIfIpfs
return firstly {
//Must not use `SessionManager.default.request` or `Alamofire.request` which uses the former. See comment in var
sessionManagerWithDefaultHttpHeaders.request(uri, method: .get).responseData()
}.map { data, _ in
if let json = try? JSON(data: data) {
if json["error"] == "Internal Server Error" {
throw Error()
} else {
var jsonDictionary = json
if let tokenObject = tokens.first(where: { $0.contractAddress.sameContract(as: address) }) {
//We must make sure the value stored is at least an empty string, never nil because we need to deserialise/decode it
jsonDictionary["tokenId"] = JSON(tokenId)
jsonDictionary["tokenType"] = JSON(TokensDataStore.functional.nonFungibleTokenType(fromTokenType: tokenObject.type).rawValue)
jsonDictionary["contractName"] = JSON(tokenObject.name)
jsonDictionary["symbol"] = JSON(tokenObject.symbol)
jsonDictionary["name"] = JSON(jsonDictionary["name"].stringValue)
jsonDictionary["imageUrl"] = JSON(jsonDictionary["image"].string ?? jsonDictionary["image_url"].string ?? "")
jsonDictionary["thumbnailUrl"] = jsonDictionary["imageUrl"]
//POAP tokens (https://blockscout.com/xdai/mainnet/address/0x22C1f6050E56d2876009903609a2cC3fEf83B415/transactions), eg. https://api.poap.xyz/metadata/2503/278569, use `home_url` as the key for what they should use `external_url` for and they use `external_url` to point back to the token URI
jsonDictionary["externalLink"] = JSON(jsonDictionary["home_url"].string ?? jsonDictionary["external_url"].string ?? "")
}
if let jsonString = jsonDictionary.rawString() {
return jsonString
} else {
throw Error()
}
}
} else {
throw Error()
}
return .value(jsonDictionary.rawString()!)
}
}
private func generateTokenJsonFallback(forTokenId tokenId: String, address: AlphaWallet.Address, tokens: [Activity.AssignedToken]) -> Guarantee<String> {
var jsonDictionary = JSON()
if let tokenObject = tokens.first(where: { $0.contractAddress.sameContract(as: address) }) {
jsonDictionary["tokenId"] = JSON(tokenId)
jsonDictionary["tokenType"] = JSON(TokensDataStore.functional.nonFungibleTokenType(fromTokenType: tokenObject.type).rawValue)
jsonDictionary["contractName"] = JSON(tokenObject.name)
jsonDictionary["decimals"] = JSON(0)
jsonDictionary["symbol"] = JSON(tokenObject.symbol)
jsonDictionary["name"] = ""
jsonDictionary["imageUrl"] = ""
jsonDictionary["thumbnailUrl"] = ""
jsonDictionary["externalLink"] = ""
}
return .value(jsonDictionary.rawString()!)
}
private func fetchTokenJson(forTokenId tokenId: String, uri originalUri: URL, address: AlphaWallet.Address, tokens: [Activity.AssignedToken]) -> Promise<String> {
struct Error: Swift.Error {
}
@ -289,10 +416,12 @@ class PrivateBalanceFetcher: PrivateBalanceFetcherType {
} else {
var jsonDictionary = json
if let tokenObject = tokens.first(where: { $0.contractAddress.sameContract(as: address) }) {
jsonDictionary["tokenType"] = JSON(TokensDataStore.functional.nonFungibleTokenType(fromTokenType: tokenObject.type).rawValue)
//We must make sure the value stored is at least an empty string, never nil because we need to deserialise/decode it
jsonDictionary["tokenId"] = JSON(tokenId)
jsonDictionary["contractName"] = JSON(tokenObject.name)
jsonDictionary["symbol"] = JSON(tokenObject.symbol)
jsonDictionary["tokenId"] = JSON(tokenId)
jsonDictionary["decimals"] = JSON(jsonDictionary["decimals"].intValue ?? 0)
jsonDictionary["name"] = JSON(jsonDictionary["name"].stringValue)
jsonDictionary["imageUrl"] = JSON(jsonDictionary["image"].string ?? jsonDictionary["image_url"].string ?? "")
jsonDictionary["thumbnailUrl"] = jsonDictionary["imageUrl"]
@ -313,16 +442,113 @@ class PrivateBalanceFetcher: PrivateBalanceFetcherType {
private func updateOpenSeaNonFungiblesBalanceAndAttributes(contractToOpenSeaNonFungibles: [AlphaWallet.Address: [OpenSeaNonFungible]], tokens: [Activity.AssignedToken], group: DispatchGroup) {
for (contract, openSeaNonFungibles) in contractToOpenSeaNonFungibles {
group.enter()
tokensDatastore.addOrUpdateErc271(contract: contract, openSeaNonFungibles: openSeaNonFungibles, tokens: tokens) { [weak self] in
guard let strongSelf = self else {
group.leave()
return
var listOfJson = [String]()
var anyNonFungible: OpenSeaNonFungible?
for each in openSeaNonFungibles {
if let encodedJson = try? JSONEncoder().encode(each), let jsonString = String(data: encodedJson, encoding: .utf8) {
anyNonFungible = each
listOfJson.append(jsonString)
} else {
//no op
}
}
let tokenType: TokenType
if let anyNonFungible = anyNonFungible {
tokenType = anyNonFungible.tokenType.asTokenType
} else {
//Default to ERC721 because this is what we supported (from OpenSea) before adding ERC1155 support
tokenType = .erc721
}
group.leave()
strongSelf.delegate?.didUpdate(in: strongSelf)
if let tokenObject = tokens.first(where: { $0.contractAddress.sameContract(as: contract) }) {
switch tokenObject.type {
case .nativeCryptocurrency, .erc721, .erc875, .erc721ForTickets, .erc1155:
break
case .erc20:
group.enter()
tokensDatastore.update(primaryKey: tokenObject.primaryKey, action: .type(tokenType)) { _ in group.leave() }
}
group.enter()
tokensDatastore.update(primaryKey: tokenObject.primaryKey, action: .nonFungibleBalance(listOfJson)) { _ in group.leave() }
if let anyNonFungible = anyNonFungible {
group.enter()
tokensDatastore.update(primaryKey: tokenObject.primaryKey, action: .name(anyNonFungible.contractName)) { _ in group.leave() }
}
} else {
let token = ERCToken(
contract: contract,
server: server,
name: openSeaNonFungibles[0].contractName,
symbol: openSeaNonFungibles[0].symbol,
decimals: 0,
type: tokenType,
balance: listOfJson
)
group.enter()
tokensDatastore.addCustom(token: token, shouldUpdateBalance: tokenType.shouldUpdateBalanceWhenDetected) { group.leave() }
}
}
}
func writeJsonForTransactions(toUrl url: URL) {
guard let transactionStorage = erc721TokenIdsFetcher as? TransactionsStorage else { return }
transactionStorage.writeJsonForTransactions(toUrl: url)
}
}
extension PrivateBalanceFetcher {
class functional {}
}
fileprivate extension PrivateBalanceFetcher.functional {
static func fetchUnknownErc1155ContractsDetails(contractsTokenIdsAndValue: Erc1155TokenIds.ContractsTokenIdsAndValues, tokens: [Activity.AssignedToken], server: RPCServer, account: Wallet, assetDefinitionStore: AssetDefinitionStore) -> Promise<[ERCToken]> {
let contractsToAdd: [AlphaWallet.Address] = contractsTokenIdsAndValue.keys.filter { contract in
!tokens.contains(where: { $0.contractAddress.sameContract(as: contract)})
}
guard !contractsToAdd.isEmpty else {
return Promise<[ERCToken]>.value(.init())
}
let (promise, seal) = Promise<[ERCToken]>.pending()
//Can't use `DispatchGroup` because `ContractDataDetector.fetch()` doesn't call `completion` once and only once
var contractsProcessed: Set<AlphaWallet.Address> = .init()
var erc1155TokensToAdd: [ERCToken] = .init()
func markContractProcessed(_ contract: AlphaWallet.Address) {
contractsProcessed.insert(contract)
if contractsProcessed.count == contractsToAdd.count {
seal.fulfill(erc1155TokensToAdd)
}
}
for each in contractsToAdd {
ContractDataDetector(address: each, account: account, server: server, assetDefinitionStore: assetDefinitionStore).fetch { data in
switch data {
case .name, .symbol, .balance, .decimals:
break
case .nonFungibleTokenComplete(let name, let symbol, let balance, let tokenType):
let token = ERCToken(
contract: each,
server: server,
name: name,
symbol: symbol,
//Doesn't matter for ERC1155 since it's not used at the token level
decimals: 0,
type: tokenType,
balance: balance
)
erc1155TokensToAdd.append(token)
markContractProcessed(each)
case .fungibleTokenComplete:
markContractProcessed(each)
case .delegateTokenComplete:
markContractProcessed(each)
case .failed:
//TODO we are ignoring `.failed` here because it is called multiple times and we need to wait until `ContractDataDetector.fetch()`'s `completion` is called once and only once
break
}
}
}
return promise
}
}

@ -16,9 +16,9 @@ protocol PrivateTokensDatastoreType {
var enabledObjects: Results<TokenObject> { get }
var tokenObjects: [Activity.AssignedToken] { get }
func addOrUpdateErc271(contract: AlphaWallet.Address, openSeaNonFungibles: [OpenSeaNonFungible], tokens: [Activity.AssignedToken], completion: @escaping () -> Void)
func addCustom(token: ERCToken, completion: @escaping () -> Void)
func addCustom(tokens: [ERCToken], completion: @escaping () -> Void)
func addOrUpdateOpenSeaNonFungible(contract: AlphaWallet.Address, openSeaNonFungibles: [OpenSeaNonFungible], tokens: [Activity.AssignedToken], completion: @escaping () -> Void)
func addCustom(token: ERCToken, shouldUpdateBalance: Bool, completion: @escaping () -> Void)
func addCustom(tokens: [ERCToken], shouldUpdateBalance: Bool, completion: @escaping () -> Void)
func update(primaryKey: String, action: PrivateTokensDatastore.TokenUpdateAction, completion: @escaping (Bool?) -> Void)
func tokenObject(contract: AlphaWallet.Address) -> TokenObject?
}
@ -67,7 +67,7 @@ class PrivateTokensDatastore: PrivateTokensDatastoreType {
case type(TokenType)
}
func addOrUpdateErc271(contract: AlphaWallet.Address, openSeaNonFungibles: [OpenSeaNonFungible], tokens: [Activity.AssignedToken], completion: @escaping () -> Void) {
func addOrUpdateOpenSeaNonFungible(contract: AlphaWallet.Address, openSeaNonFungibles: [OpenSeaNonFungible], tokens: [Activity.AssignedToken], completion: @escaping () -> Void) {
var listOfJson = [String]()
var anyNonFungible: OpenSeaNonFungible?
for each in openSeaNonFungibles {
@ -78,16 +78,23 @@ class PrivateTokensDatastore: PrivateTokensDatastoreType {
//no op
}
}
let tokenType: TokenType
if let anyNonFungible = anyNonFungible {
tokenType = anyNonFungible.tokenType.asTokenType
} else {
//Default to ERC721 because this is what we supported (from OpenSea) before adding ERC1155 support
tokenType = .erc721
}
if let tokenObject = tokens.first(where: { $0.contractAddress.sameContract(as: contract) }) {
let group = DispatchGroup()
switch tokenObject.type {
case .nativeCryptocurrency, .erc721, .erc875, .erc721ForTickets:
case .nativeCryptocurrency, .erc721, .erc875, .erc721ForTickets, .erc1155:
break
case .erc20:
group.enter()
update(primaryKey: tokenObject.primaryKey, action: .type(.erc721)) { _ in
update(primaryKey: tokenObject.primaryKey, action: .type(tokenType)) { _ in
group.leave()
}
}
@ -114,14 +121,14 @@ class PrivateTokensDatastore: PrivateTokensDatastoreType {
name: openSeaNonFungibles[0].contractName,
symbol: openSeaNonFungibles[0].symbol,
decimals: 0,
type: .erc721,
type: tokenType,
balance: listOfJson
)
addCustom(token: token, completion: completion)
addCustom(token: token, shouldUpdateBalance: tokenType.shouldUpdateBalanceWhenDetected, completion: completion)
}
}
func addCustom(tokens: [ERCToken], completion: @escaping () -> Void) {
func addCustom(tokens: [ERCToken], shouldUpdateBalance: Bool, completion: @escaping () -> Void) {
backgroundQueue.async {
let backgroundRealm = self.realm.threadSafe
let newTokens: [TokenObject] = tokens.map { token in
@ -135,8 +142,10 @@ class PrivateTokensDatastore: PrivateTokensDatastoreType {
isCustom: true,
type: token.type
)
token.balance.forEach { balance in
newToken.balance.append(TokenBalance(balance: balance))
if shouldUpdateBalance {
token.balance.forEach { balance in
newToken.balance.append(TokenBalance(balance: balance))
}
}
if let object = backgroundRealm.object(ofType: TokenObject.self, forPrimaryKey: newToken.primaryKey) {
@ -157,8 +166,8 @@ class PrivateTokensDatastore: PrivateTokensDatastoreType {
}
}
func addCustom(token: ERCToken, completion: @escaping () -> Void) {
addCustom(tokens: [token], completion: completion)
func addCustom(token: ERCToken, shouldUpdateBalance: Bool, completion: @escaping () -> Void) {
addCustom(tokens: [token], shouldUpdateBalance: shouldUpdateBalance, completion: completion)
}
func update(primaryKey: String, action: TokenUpdateAction, completion: @escaping (Bool?) -> Void) {

@ -134,7 +134,7 @@ class WalletBalanceFetcher: NSObject, WalletBalanceFetcherType {
case .erc20:
let balance = ERC20Balance(tokenObject: tokenObject)
return ERC20BalanceViewModel(server: tokenObject.server, balance: balance, ticker: ticker)
case .erc875, .erc721, .erc721ForTickets:
case .erc875, .erc721, .erc721ForTickets, .erc1155:
return nil
}
}

@ -16,6 +16,14 @@ extension AlphaWallet.Ethereum.ABI {
static let ERC20: Data = {
let url = Bundle.main.url(forResource: "ERC20", withExtension: "json")!
return try! Data(contentsOf: url)
}()
}
}()
static let erc1155: Data = {
let url = Bundle.main.url(forResource: "ERC1155", withExtension: "json")!
return try! Data(contentsOf: url)
}()
static let erc1155String: String = {
String(data: erc1155, encoding: .utf8)!
}()
}

@ -0,0 +1,67 @@
[
{
"name" : "TransferSingle",
"inputs" : [
{
"type" : "address",
"indexed" : true,
"name" : "_operator"
},
{
"type" : "address",
"indexed" : true,
"name" : "_from"
},
{
"type" : "address",
"indexed" : true,
"name" : "_to"
},
{
"type" : "uint256",
"name" : "_id",
"indexed" : false
},
{
"type" : "uint256",
"name" : "_value",
"indexed" : false
}
],
"type" : "event",
"anonymous" : false
},
{
"name" : "TransferBatch",
"inputs" : [
{
"type" : "address",
"indexed" : true,
"name" : "_operator"
},
{
"type" : "address",
"indexed" : true,
"name" : "_from"
},
{
"type" : "address",
"indexed" : true,
"name" : "_to"
},
{
"type" : "uint256[]",
"name" : "_ids",
"indexed" : false
},
{
"type" : "uint256[]",
"name" : "_values",
"indexed" : false
}
],
"type" : "event",
"anonymous" : false
}
]

@ -65,7 +65,7 @@ extension TransactionType {
return TokenActionsServiceKey(tokenObject: token)
case .ERC20Token(let token, _, _):
return TokenActionsServiceKey(tokenObject: token)
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .dapp, .tokenScript, .claimPaidErc875MagicLink:
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .ERC1155Token, .dapp, .tokenScript, .claimPaidErc875MagicLink:
return nil
}
}

@ -74,6 +74,10 @@ class TokenProvider: TokenProviderType {
return GetIsERC721ContractCoordinator(forServer: server)
}()
private lazy var getIsERC1155ContractCoordinator: GetIsERC1155ContractCoordinator = {
return GetIsERC1155ContractCoordinator(forServer: server)
}()
private lazy var getDecimalsCoordinator: GetDecimalsCoordinator = {
return GetDecimalsCoordinator(forServer: server)
}()
@ -391,6 +395,22 @@ class TokenProvider: TokenProviderType {
}
}
}
let isErc1155Promise = Promise<Bool> { seal in
withRetry(times: numberOfTimesToRetryFetchContractData) { [weak self] triggerRetry in
guard let strongSelf = self else { return }
strongSelf.getIsERC1155ContractCoordinator.getIsERC1155Contract(for: address) { [weak self] result in
guard self != nil else { return }
switch result {
case .success(let isErc1155):
seal.fulfill(isErc1155)
case .failure:
if !triggerRetry() {
seal.fulfill(false)
}
}
}
}
}
firstly {
isErc721Promise
@ -416,9 +436,19 @@ class TokenProvider: TokenProviderType {
}.cauterize()
firstly {
when(fulfilled: isErc875Promise.asVoid(), isErc721Promise.asVoid())
}.done { _, _ in
if isErc875Promise.value == false && isErc721Promise.value == .notErc721 {
isErc1155Promise
}.done { isErc1155 in
if isErc1155 {
completion(.erc1155)
} else {
//no-op
}
}.cauterize()
firstly {
when(fulfilled: isErc875Promise.asVoid(), isErc721Promise.asVoid(), isErc1155Promise.asVoid())
}.done { _, _, _ in
if isErc875Promise.value == false && isErc721Promise.value == .notErc721 && isErc1155Promise.value == false {
completion(.erc20)
} else {
//no-op

@ -124,9 +124,11 @@ class OpenSea {
var results = sum
for (_, each): (String, JSON) in json["assets"] {
let type = each["asset_contract"]["schema_name"].stringValue
guard type == "ERC721" else { continue }
guard let tokenType = NonFungibleFromJsonTokenType(rawString: type) else { continue }
let tokenId = each["token_id"].stringValue
let contractName = each["asset_contract"]["name"].stringValue
//So if it's null in OpenSea, we get a 0, as expected. And 0 works for ERC721 too
let decimals = each["decimals"].intValue
let symbol = each["asset_contract"]["symbol"].stringValue
let name = each["name"].stringValue
let description = each["description"].stringValue
@ -148,7 +150,7 @@ class OpenSea {
traits.append(trait)
}
if let contract = AlphaWallet.Address(string: each["asset_contract"]["address"].stringValue) {
let cat = OpenSeaNonFungible(tokenId: tokenId, contractName: contractName, symbol: symbol, name: name, description: description, thumbnailUrl: thumbnailUrl, imageUrl: imageUrl, contractImageUrl: contractImageUrl, externalLink: externalLink, backgroundColor: backgroundColor, traits: traits)
let cat = OpenSeaNonFungible(tokenId: tokenId, tokenType: tokenType, contractName: contractName, decimals: decimals, symbol: symbol, name: name, description: description, thumbnailUrl: thumbnailUrl, imageUrl: imageUrl, contractImageUrl: contractImageUrl, externalLink: externalLink, backgroundColor: backgroundColor, traits: traits)
if var list = results[contract] {
list.append(cat)
results[contract] = list

@ -8,6 +8,7 @@ enum OperationType: String {
case erc20TokenApprove
case erc721TokenTransfer
case erc875TokenTransfer
case erc1155TokenTransfer
case unknown
init(string: String) {
@ -16,7 +17,7 @@ enum OperationType: String {
var isTransfer: Bool {
switch self {
case .nativeCurrencyTokenTransfer, .erc20TokenTransfer, .erc721TokenTransfer, .erc875TokenTransfer:
case .nativeCurrencyTokenTransfer, .erc20TokenTransfer, .erc721TokenTransfer, .erc875TokenTransfer, .erc1155TokenTransfer:
return true
case .erc20TokenApprove:
return false
@ -27,7 +28,7 @@ enum OperationType: String {
var isSend: Bool {
switch self {
case .nativeCurrencyTokenTransfer, .erc20TokenTransfer, .erc721TokenTransfer, .erc875TokenTransfer:
case .nativeCurrencyTokenTransfer, .erc20TokenTransfer, .erc721TokenTransfer, .erc875TokenTransfer, .erc1155TokenTransfer:
return true
case .erc20TokenApprove:
return false

@ -152,6 +152,8 @@ extension TransactionInstance {
return .erc721TokenTransfer
case (.erc875, _):
return .erc875TokenTransfer
case (.erc1155, _):
return .erc1155TokenTransfer
case (.erc20, .nativeCryptoTransfer), (.erc20, .others):
return .unknown
}

@ -839,6 +839,7 @@ extension InCoordinator: SettingsCoordinatorDelegate {
}
func delete(account: Wallet, in coordinator: SettingsCoordinator) {
Erc1155TokenIdsFetcher.deleteForWallet(account.address)
TransactionsStorage.deleteAllTransactions(realm: Wallet.functional.realm(forAccount: account))
}
@ -941,7 +942,7 @@ extension InCoordinator: TokensCoordinatorDelegate {
func shouldOpen(url: URL, shouldSwitchServer: Bool, forTransactionType transactionType: TransactionType, in coordinator: TokensCoordinator) {
switch transactionType {
case .nativeCryptocurrency(let token, _, _), .ERC20Token(let token, _, _), .ERC875Token(let token), .ERC721Token(let token):
case .nativeCryptocurrency(let token, _, _), .ERC20Token(let token, _, _), .ERC875Token(let token), .ERC721Token(let token), .ERC1155Token(let token):
if shouldSwitchServer {
open(url: url, onServer: token.server)
} else {

@ -83,7 +83,7 @@ public class OrderHandler {
)
messages.append(Data(bytes: message))
}
case .erc721, .nativeCryptocurrency, .erc20:
case .erc721, .erc1155, .nativeCryptocurrency, .erc20:
break
}
return messages

@ -58,7 +58,7 @@ public class UniversalLinkHandler {
message = formatMessageForLink721Ticket(signedOrder: signedOrder)
case .erc875:
message = formatMessageForLink(signedOrder: signedOrder)
case .nativeCryptocurrency, .erc20, .erc721:
case .nativeCryptocurrency, .erc20, .erc721, .erc1155:
// Should never happen
return ""
}

@ -111,7 +111,7 @@ class TokenCardRedemptionViewController: UIViewController, TokenVerifiableStatus
return
case .erc875:
redeemData = redeem.redeemMessage(indices: viewModel.tokenHolder.indices)
case .erc721, .erc721ForTickets:
case .erc721, .erc721ForTickets, .erc1155:
redeemData = redeem.redeemMessage(tokenIds: viewModel.tokenHolder.tokens.map({ $0.id }))
}
switch session.account.type {

@ -197,4 +197,4 @@ extension EventSourceCoordinatorForActivities.functional {
guard let filterValueTypedForEventFilters = filterValue.coerceToArgumentTypeForEventFilter(parameterType) else { return nil }
return (filter: [filterValueTypedForEventFilters], textEquivalent: textEquivalent)
}
}
}

@ -12,7 +12,7 @@ class FetchAssetDefinitionsCoordinator: Coordinator {
for each in tokensDataStores.values {
contracts.append(contentsOf: each.enabledObject.filter {
switch $0.type {
case .erc20, .erc721, .erc875, .erc721ForTickets:
case .erc20, .erc721, .erc875, .erc721ForTickets, .erc1155:
return true
case .nativeCryptocurrency:
return false

@ -100,7 +100,7 @@ struct Activity {
} else {
return .none
}
case .erc721, .erc721ForTickets:
case .erc721, .erc721ForTickets, .erc1155:
if isBaseCard {
switch name {
case "sent":

@ -6,4 +6,5 @@ enum TokenInterfaceType: String {
case erc20
case erc721
case erc875
case erc1155
}

@ -531,6 +531,8 @@ private class PrivateXMLHandler {
} else {
actions = [.nftSell, .nonFungibleTransfer]
}
case .erc1155:
actions = [.nonFungibleTransfer]
}
return actions.map { .init(type: $0) }
}
@ -552,6 +554,8 @@ private class PrivateXMLHandler {
} else {
actions = [.nftSell, .nonFungibleTransfer]
}
case .erc1155:
actions = [.nonFungibleTransfer]
}
return actions.map { .init(type: $0) }
}

@ -0,0 +1,39 @@
//
// Created by James Sangalli on 14/7/18.
// Copyright © 2018 Stormbird PTE. LTD.
//
import Foundation
import PromiseKit
import Result
import web3swift
class GetIsERC1155ContractCoordinator {
private let server: RPCServer
private struct ERC165Hash {
//https://eips.ethereum.org/EIPS/eip-1155
static let official = "0xd9b67a26"
}
init(forServer server: RPCServer) {
self.server = server
}
func getIsERC1155Contract(for contract: AlphaWallet.Address, completion: @escaping (ResultResult<Bool, AnyError>.t) -> Void) {
firstly {
GetInterfaceSupported165Coordinator(forServer: server).getInterfaceSupported165(hash: ERC165Hash.official, contract: contract)
}.done { result in
completion(.success(result))
}.catch { error in
completion(.failure(AnyError(error)))
}
}
private func adapt(_ value: Any) -> Bool {
if let value = value as? Bool {
return value
} else {
return false
}
}
}

@ -250,10 +250,13 @@ class SingleChainTokenCoordinator: Coordinator {
return .value(.none)
}
case .erc721:
//Handled in TokensDataStore.refreshBalanceForERC721Tokens()
//Handled in PrivateBalanceFetcher.refreshBalanceForErc721Or1155Tokens()
return .value(.none)
case .erc721ForTickets:
//Handled in TokensDataStore.refreshBalanceForNonERC721TicketTokens()
//Handled in PrivateBalanceFetcher.refreshBalanceForNonErc721Or1155Tokens()
return .value(.none)
case .erc1155:
//Handled in PrivateBalanceFetcher.refreshBalanceForErc721Or1155Tokens()
return .value(.none)
case .nativeCryptocurrency:
return .value(.none)
@ -446,7 +449,7 @@ class SingleChainTokenCoordinator: Coordinator {
switch token.type {
case .nativeCryptocurrency, .erc20, .erc875, .erc721ForTickets:
break
case .erc721:
case .erc721, .erc1155:
//TODO is this check still necessary?
switch OpenSeaBackedNonFungibleTokenHandling(token: token, assetDefinitionStore: assetDefinitionStore, tokenViewType: .viewIconified) {
case .backedByOpenSea:
@ -527,7 +530,7 @@ class SingleChainTokenCoordinator: Coordinator {
}
func add(token: ERCToken) -> TokenObject {
let tokenObject = storage.addCustom(token: token)
let tokenObject = storage.addCustom(token: token, shouldUpdateBalance: true)
notifyTokensDidChange()
return tokenObject
@ -669,7 +672,7 @@ extension SingleChainTokenCoordinator: TokenViewControllerDelegate {
switch transactionType {
case .ERC20Token(let erc20Token, _, _):
token = erc20Token
case .dapp, .ERC721Token, .ERC875Token, .ERC875TokenOrder, .ERC721ForTicketToken, .tokenScript, .claimPaidErc875MagicLink:
case .dapp, .ERC721Token, .ERC875Token, .ERC875TokenOrder, .ERC721ForTicketToken, .ERC1155Token, .tokenScript, .claimPaidErc875MagicLink:
return
case .nativeCryptocurrency:
token = TokensDataStore.etherToken(forServer: server)

@ -224,6 +224,8 @@ extension TokensCoordinator: TokensViewControllerDelegate {
coordinator.showTokenList(for: .send(type: .ERC721Token(token)), token: token, navigationController: navigationController)
case .erc875, .erc721ForTickets:
coordinator.showTokenList(for: .send(type: .ERC875Token(token)), token: token, navigationController: navigationController)
case .erc1155:
coordinator.showTokenList(for: .send(type: .ERC721Token(token)), token: token, navigationController: navigationController)
}
}

@ -25,7 +25,7 @@ class TokenAdaptor {
switch token.type {
case .nativeCryptocurrency, .erc20, .erc875, .erc721ForTickets:
return getNotSupportedByNonFungibleJsonTokenHolders(forWallet: account)
case .erc721:
case .erc721, .erc1155:
let tokenType = NonFungibleFromJsonSupportedTokenHandling(token: token)
switch tokenType {
case .supported:
@ -40,7 +40,7 @@ class TokenAdaptor {
let balance = token.balance
var tokens = [Token]()
switch token.type {
case .erc875, .erc721ForTickets, .erc721, .nativeCryptocurrency:
case .erc875, .erc721ForTickets, .erc721, .erc1155, .nativeCryptocurrency:
for (index, item) in balance.enumerated() {
//id is the value of the bytes32 token
let id = item.balance
@ -71,7 +71,7 @@ class TokenAdaptor {
var tokens = [Token]()
for item in balance {
let jsonString = item.balance
if let token = getTokenForNonFungible(forJSONString: jsonString, inWallet: account, server: self.token.server, isSourcedFromEvents: isSourcedFromEvents) {
if let token = getTokenForNonFungible(forJSONString: jsonString, inWallet: account, server: self.token.server, isSourcedFromEvents: isSourcedFromEvents, tokenType: self.token.type) {
tokens.append(token)
}
}
@ -86,7 +86,7 @@ class TokenAdaptor {
} else {
break
}
case .erc721, .erc721ForTickets:
case .erc721, .erc721ForTickets, .erc1155:
return tokens.map { getTokenHolder(for: [$0]) }
}
var tokenHolders: [TokenHolder] = []
@ -156,7 +156,7 @@ class TokenAdaptor {
XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore).getToken(name: name, symbol: symbol, fromTokenIdOrEvent: tokenIdOrEvent, index: index, inWallet: account, server: server, tokenType: token.type)
}
private func getTokenForNonFungible(forJSONString jsonString: String, inWallet account: Wallet, server: RPCServer, isSourcedFromEvents: Bool) -> Token? {
private func getTokenForNonFungible(forJSONString jsonString: String, inWallet account: Wallet, server: RPCServer, isSourcedFromEvents: Bool, tokenType: TokenType) -> Token? {
guard let data = jsonString.data(using: .utf8), let nonFungible = nonFungible(fromJsonData: data) else { return nil }
let xmlHandler = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore)
@ -208,7 +208,7 @@ class TokenAdaptor {
}
return Token(
tokenIdOrEvent: tokenIdOrEvent,
tokenType: TokenType.erc721,
tokenType: nonFungible.tokenType.asTokenType,
index: 0,
name: nonFungible.contractName,
symbol: "",

@ -100,6 +100,10 @@ class ContractDataDetector {
self.callCompletionFailed()
}
}
case .erc1155:
let balance: [String] = .init()
self.nonFungibleBalanceSeal.fulfill(balance)
self.completionOfPartialData(.balance(balance: balance, tokenType: .erc1155))
case .erc20:
self.tokenProvider.getDecimals(for: self.address) { result in
switch result {
@ -145,7 +149,7 @@ class ContractDataDetector {
private func callCompletionOnAllData() {
if namePromise.isResolved, symbolPromise.isResolved, let tokenType = tokenTypePromise.value {
switch tokenType {
case .erc875, .erc721, .erc721ForTickets:
case .erc875, .erc721, .erc721ForTickets, .erc1155:
if let nonFungibleBalance = nonFungibleBalancePromise.value {
let name = namePromise.value
let symbol = symbolPromise.value

@ -0,0 +1,242 @@
// Copyright © 2021 Stormbird PTE. LTD.
import Foundation
import BigInt
import PromiseKit
import web3swift
struct Erc1155TokenIds: Codable {
typealias ContractsTokenIdsAndValues = [AlphaWallet.Address: [BigUInt: BigInt]]
let tokens: ContractsTokenIdsAndValues
let lastBlockNumber: BigUInt
}
fileprivate struct Erc1155TransferEvent: Comparable {
enum TransferType {
case send
case receive
}
let contract: AlphaWallet.Address
let tokenId: BigUInt
let value: BigUInt
let from: AlphaWallet.Address
let to: AlphaWallet.Address
let transferType: TransferType
let blockNumber: BigUInt
let transactionIndex: BigUInt
let logIndex: BigUInt
static func <(lhs: Erc1155TransferEvent, rhs: Erc1155TransferEvent) -> Bool {
if lhs.blockNumber == rhs.blockNumber {
if lhs.transactionIndex == rhs.transactionIndex {
return lhs.logIndex < rhs.logIndex
} else {
return lhs.transactionIndex < rhs.transactionIndex
}
} else {
return lhs.blockNumber < rhs.blockNumber
}
}
}
class Erc1155TokenIdsFetcher {
static let documentDirectory = URL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]).appendingPathComponent("erc1155TokenIds")
private let address: AlphaWallet.Address
private let server: RPCServer
init(address: AlphaWallet.Address, server: RPCServer) {
self.address = address
self.server = server
try? FileManager.default.createDirectory(at: Self.documentDirectory, withIntermediateDirectories: true)
}
func refreshBalance() -> Promise<Void> {
let fromBlockNumber: BigUInt
let old: Erc1155TokenIds
if let lastFetched: Erc1155TokenIds = readJson() {
old = lastFetched
} else {
//Should really be -1 instead 0, but so we don't fight with the type system (negative) and doesn't matter in practice being off by 1 at the start
old = .init(tokens: .init(), lastBlockNumber: 0)
}
fromBlockNumber = old.lastBlockNumber + 1
let toBlock: EventFilter.Block
if server == .binance_smart_chain || server == .binance_smart_chain_testnet || server == .heco {
//NOTE: binance_smart_chain does not allow range more than 5000
toBlock = .blockNumber(UInt64(fromBlockNumber) + 4000)
} else {
toBlock = .latest
}
return firstly {
functional.fetchEvents(forAddress: address, server: server, fromBlock: .blockNumber(UInt64(fromBlockNumber)), toBlock: toBlock)
}.map { fetched -> Erc1155TokenIds in
let tokens = fetched.tokens
switch toBlock {
case .latest, .pending:
//TODO even better if we set the latest block number in the blockchain
return fetched
case .blockNumber(let num):
let lastBlockNumber = BigUInt(num)
return Erc1155TokenIds(tokens: tokens, lastBlockNumber: lastBlockNumber)
}
}.get {
let tokens = $0.tokens
}.map { delta in
let updatedTokens = self.computeUpdatedBalance(fromOld: old.tokens, delta: delta.tokens)
return Erc1155TokenIds(tokens: updatedTokens, lastBlockNumber: delta.lastBlockNumber)
}.then {
self.writeJson(contractsTokenIdsAndValue: $0)
}
}
private func computeUpdatedBalance(fromOld old: Erc1155TokenIds.ContractsTokenIdsAndValues, delta: Erc1155TokenIds.ContractsTokenIdsAndValues) -> Erc1155TokenIds.ContractsTokenIdsAndValues {
var updatedTokens: Erc1155TokenIds.ContractsTokenIdsAndValues = old
for (contract, deltaTokenIdsAndValue) in delta {
if var updateTokenIdsAndValue = updatedTokens[contract] {
for (tokenId, deltaValue) in deltaTokenIdsAndValue {
let oldValue = updateTokenIdsAndValue[tokenId] ?? 0
updateTokenIdsAndValue[tokenId] = oldValue + deltaValue
}
updatedTokens[contract] = updateTokenIdsAndValue
} else {
updatedTokens[contract] = deltaTokenIdsAndValue
}
}
return updatedTokens
}
//MARK: Serialization
static private func fileUrl(forWallet address: AlphaWallet.Address, server: RPCServer) -> URL {
return documentDirectory.appendingPathComponent("\(address.eip55String)-\(server.chainID).json")
}
func readJson() -> Erc1155TokenIds? {
guard let data = try? Data(contentsOf: Self.fileUrl(forWallet: address, server: server)) else { return nil }
return try? JSONDecoder().decode(Erc1155TokenIds.self, from: data)
}
private func writeJson(contractsTokenIdsAndValue: Erc1155TokenIds) -> Promise<Void> {
Promise { seal in
if let data = try? JSONEncoder().encode(contractsTokenIdsAndValue) {
do {
try data.write(to: Self.fileUrl(forWallet: address, server: server), options: .atomicWrite)
seal.fulfill(())
} catch {
seal.reject(error)
}
} else {
struct E: Error {}
seal.reject(E())
}
}
}
static func deleteForWallet(_ address: AlphaWallet.Address) {
for each in RPCServer.allCases {
let file = fileUrl(forWallet: address, server: each)
try? FileManager.default.removeItem(at: file)
}
}
}
extension Erc1155TokenIdsFetcher {
class functional {}
}
extension Erc1155TokenIdsFetcher.functional {
static func fetchEvents(forAddress address: AlphaWallet.Address, server: RPCServer, fromBlock: EventFilter.Block, toBlock: EventFilter.Block) -> Promise<Erc1155TokenIds> {
let recipientAddress = EthereumAddress(address.eip55String)!
let nullFilter: [EventFilterable]? = nil
let singleTransferEventName = "TransferSingle"
let batchTransferEventName = "TransferBatch"
let sendParameterFilters: [[EventFilterable]?] = [nullFilter, [recipientAddress], nullFilter]
let receiveParameterFilters: [[EventFilterable]?] = [nullFilter, nullFilter, [recipientAddress]]
let sendSinglePromise = firstly {
fetchEvents(server: server, transferType: .send, eventName: singleTransferEventName, parameterFilters: sendParameterFilters, fromBlock: fromBlock, toBlock: toBlock)
}
let receiveSinglePromise = firstly {
fetchEvents(server: server, transferType: .receive, eventName: singleTransferEventName, parameterFilters: receiveParameterFilters, fromBlock: fromBlock, toBlock: toBlock)
}
let sendBulkPromise = firstly {
fetchEvents(server: server, transferType: .send, eventName: batchTransferEventName, parameterFilters: sendParameterFilters, fromBlock: fromBlock, toBlock: toBlock)
}
let receiveBulkPromise = firstly {
fetchEvents(server: server, transferType: .receive, eventName: batchTransferEventName, parameterFilters: receiveParameterFilters, fromBlock: fromBlock, toBlock: toBlock)
}
return firstly {
when(fulfilled: sendSinglePromise, receiveSinglePromise, sendBulkPromise, receiveBulkPromise)
}.map { a, b, c, d -> Erc1155TokenIds in
let all: [Erc1155TransferEvent] = (a + b + c + d).sorted()
var contractsTokenIdsAndValue: Erc1155TokenIds.ContractsTokenIdsAndValues = .init()
for each in all {
let tokenId = each.tokenId
let value = BigInt(each.value)
var tokenIdsAndValue = contractsTokenIdsAndValue[each.contract] ?? .init()
let oldValue = tokenIdsAndValue[tokenId] ?? 0
switch each.transferType {
case .send:
//We need to track negatives even if old value is 0 because we are computing the deltas since the last fetch
tokenIdsAndValue[tokenId] = oldValue - value
case .receive:
tokenIdsAndValue[tokenId] = oldValue + value
}
contractsTokenIdsAndValue[each.contract] = tokenIdsAndValue
}
let biggestBlockNumber: BigUInt = all.last?.blockNumber ?? 0
return Erc1155TokenIds(tokens: contractsTokenIdsAndValue, lastBlockNumber: biggestBlockNumber)
}
}
fileprivate static func fetchEvents(server: RPCServer, transferType: Erc1155TransferEvent.TransferType, eventName: String, parameterFilters: [[EventFilterable]?], fromBlock: EventFilter.Block, toBlock: EventFilter.Block) -> Promise<[Erc1155TransferEvent]> {
Promise { seal in
//We just need any contract for the Swift API to get events, it's not actually used
let dummyContract = Constants.nullAddress
let queue: DispatchQueue = .main
let eventFilter = EventFilter(fromBlock: fromBlock, toBlock: toBlock, addresses: nil, parameterFilters: parameterFilters)
firstly {
getEventLogs(withServer: server, contract: dummyContract, eventName: eventName, abiString: AlphaWallet.Ethereum.ABI.erc1155String, filter: eventFilter, queue: queue)
}.done(on: queue, { events in
let events = events.filter { $0.eventLog != nil }
let sortedEvents = events.sorted(by: { a, b in
if a.eventLog!.blockNumber == b.eventLog!.blockNumber {
return a.eventLog!.transactionIndex == b.eventLog!.transactionIndex
} else {
return a.eventLog!.blockNumber < b.eventLog!.blockNumber
}
})
let results: [Erc1155TransferEvent] = sortedEvents.flatMap { each -> [Erc1155TransferEvent] in
let contract = AlphaWallet.Address(address: each.eventLog!.address)
guard let from = ((each.decodedResult["_from"] as? EthereumAddress).flatMap({ AlphaWallet.Address(address: $0) })) else { return [] }
guard let to = ((each.decodedResult["_to"] as? EthereumAddress).flatMap({ AlphaWallet.Address(address: $0) })) else { return [] }
let blockNumber = each.eventLog!.blockNumber
let transactionIndex = each.eventLog!.transactionIndex
let logIndex = each.eventLog!.logIndex
if eventName == "TransferSingle" {
guard let tokenId = each.decodedResult["_id"] as? BigUInt else { return [] }
guard let value = each.decodedResult["_value"] as? BigUInt else { return [] }
return [.init(contract: contract, tokenId: tokenId, value: value, from: from, to: to, transferType: transferType, blockNumber: blockNumber, transactionIndex: transactionIndex, logIndex: logIndex)]
} else {
guard let tokenIds = each.decodedResult["_ids"] as? [BigUInt] else { return [] }
guard let values = each.decodedResult["_values"] as? [BigUInt] else { return [] }
let results: [Erc1155TransferEvent] = zip(tokenIds, values).map { (tokenId, value) in
.init(contract: contract, tokenId: tokenId, value: value, from: from, to: to, transferType: transferType, blockNumber: blockNumber, transactionIndex: transactionIndex, logIndex: logIndex)
}
return results
}
}
seal.fulfill(results)
}).catch { error in
//TODO should log remotely
}
}
}
}

@ -1,6 +1,7 @@
// Copyright © 2021 Stormbird PTE. LTD.
import Foundation
import BigInt
import PromiseKit
class Erc721Contract {
@ -23,11 +24,11 @@ class Erc721Contract {
return firstly {
callSmartContract(withServer: server, contract: contract, functionName: function.name, abiString: function.abi, parameters: [tokenId] as [AnyObject], timeout: TokensDataStore.fetchContractDataTimeout)
}.map { uriResult -> URL in
let string = (uriResult["0"] as? String) ?? ""
let string = ((uriResult["0"] as? String) ?? "").stringWithTokenIdSubstituted(tokenId)
if let url = URL(string: string) {
return url
} else {
throw Web3Error(description: "Error extracting tokenUri for contract \(contract.eip55String) tokenId: \(tokenId)")
throw Web3Error(description: "Error extracting tokenUri uri for contract \(contract.eip55String) tokenId: \(tokenId) string: \(uriResult)")
}
}
}
@ -37,12 +38,26 @@ class Erc721Contract {
return firstly {
callSmartContract(withServer: server, contract: contract, functionName: function.name, abiString: function.abi, parameters: [tokenId] as [AnyObject], timeout: TokensDataStore.fetchContractDataTimeout)
}.map { uriResult -> URL in
let string = (uriResult["0"] as? String) ?? ""
let string = ((uriResult["0"] as? String) ?? "").stringWithTokenIdSubstituted(tokenId)
if let url = URL(string: string) {
return url
} else {
throw Web3Error(description: "Error extracting tokenUri uri for contract \(contract.eip55String) tokenId: \(tokenId)")
throw Web3Error(description: "Error extracting token uri for contract \(contract.eip55String) tokenId: \(tokenId) string: \(uriResult)")
}
}
}
}
extension String {
fileprivate func stringWithTokenIdSubstituted(_ tokenId: String) -> String {
//According to https://eips.ethereum.org/EIPS/eip-1155
//The string format of the substituted hexadecimal ID MUST be lowercase alphanumeric: [0-9a-f] with no 0x prefix.
//The string format of the substituted hexadecimal ID MUST be leading zero padded to 64 hex characters length if necessary.
if let tokenId = BigInt(tokenId) {
let hex = String(tokenId, radix: 16).padding(toLength: 64, withPad: "0", startingAt: 0)
return self.replacingOccurrences(of: "{id}", with: hex)
} else {
return self
}
}
}

@ -5,7 +5,9 @@ import Foundation
//Shape of this originally created to match OpenSea's API output
protocol NonFungibleFromJson: Codable {
var tokenId: String { get }
var tokenType: NonFungibleFromJsonTokenType { get }
var contractName: String { get }
var decimals: Int { get }
var symbol: String { get }
var name: String { get }
var description: String { get }
@ -25,5 +27,14 @@ func nonFungible(fromJsonData jsonData: Data) -> NonFungibleFromJson? {
if let nonFungible = try? JSONDecoder().decode(NonFungibleFromTokenUri.self, from: jsonData) {
return nonFungible
}
//Parse JSON strings which were saved before we added support for ERC1155. So they are all ERC721s with missing fields
if let nonFungible = try? JSONDecoder().decode(OpenSeaNonFungibleBeforeErc1155Support.self, from: jsonData) {
return nonFungible.asPostErc1155Support
}
if let nonFungible = try? JSONDecoder().decode(NonFungibleFromTokenUriBeforeErc1155Support.self, from: jsonData) {
return nonFungible.asPostErc1155Support
}
return nil
}

@ -0,0 +1,25 @@
// Copyright © 2021 Stormbird PTE. LTD.
import Foundation
enum NonFungibleFromJsonTokenType: String, Codable {
case erc721
case erc1155
init?(rawString: String) {
if let value = Self(rawValue: rawString.lowercased()) {
self = value
} else {
return nil
}
}
var asTokenType: TokenType {
switch self {
case .erc721:
return .erc721
case .erc1155:
return .erc1155
}
}
}

@ -5,7 +5,9 @@ import Foundation
//To store the output from ERC721's `tokenURI()`. The output has to be massaged to fit here as the properties was designed for OpenSea
struct NonFungibleFromTokenUri: Codable, NonFungibleFromJson {
let tokenId: String
let tokenType: NonFungibleFromJsonTokenType
let contractName: String
let decimals: Int
let symbol: String
let name: String
var description: String {
@ -26,4 +28,34 @@ struct NonFungibleFromTokenUri: Codable, NonFungibleFromJson {
var generationTrait: OpenSeaNonFungibleTrait? {
nil
}
}
struct NonFungibleFromTokenUriBeforeErc1155Support: Codable {
let tokenId: String
let contractName: String
let symbol: String
let name: String
var description: String {
""
}
let thumbnailUrl: String
let imageUrl: String
var contractImageUrl: String {
""
}
let externalLink: String
var backgroundColor: String? {
""
}
var traits: [OpenSeaNonFungibleTrait] {
.init()
}
var generationTrait: OpenSeaNonFungibleTrait? {
nil
}
var asPostErc1155Support: NonFungibleFromJson {
let result = NonFungibleFromTokenUri(tokenId: tokenId, tokenType: .erc721, contractName: contractName, decimals: 0, symbol: symbol, name: name, thumbnailUrl: thumbnailUrl, imageUrl: imageUrl, externalLink: externalLink)
return result
}
}

@ -9,7 +9,9 @@ struct OpenSeaNonFungible: Codable, NonFungibleFromJson {
public static let cooldownIndexTraitName = "cooldown_index"
let tokenId: String
let tokenType: NonFungibleFromJsonTokenType
let contractName: String
let decimals: Int
let symbol: String
let name: String
let description: String
@ -32,4 +34,30 @@ struct OpenSeaNonFungibleTrait: Codable {
struct OpenSeaError: Error {
var localizedDescription: String
}
}
struct OpenSeaNonFungibleBeforeErc1155Support: Codable {
//Not every token might used the same name. This is just common in OpenSea
public static let generationTraitName = "generation"
public static let cooldownIndexTraitName = "cooldown_index"
let tokenId: String
let contractName: String
let symbol: String
let name: String
let description: String
let thumbnailUrl: String
let imageUrl: String
let contractImageUrl: String
let externalLink: String
let backgroundColor: String?
let traits: [OpenSeaNonFungibleTrait]
var generationTrait: OpenSeaNonFungibleTrait? {
return traits.first { $0.type == OpenSeaNonFungible.generationTraitName }
}
var asPostErc1155Support: NonFungibleFromJson {
let result = OpenSeaNonFungible(tokenId: tokenId, tokenType: .erc721, contractName: contractName, decimals: 0, symbol: symbol, name: name, description: description, thumbnailUrl: thumbnailUrl, imageUrl: imageUrl, contractImageUrl: contractImageUrl, externalLink: externalLink, backgroundColor: backgroundColor, traits: traits)
return result
}
}

@ -69,7 +69,7 @@ extension Activity {
case .erc20, .nativeCryptocurrency:
let fullValue = EtherNumberFormatter.plain.string(from: tokenObject.valueBigInt, decimals: decimals)
balance = .value(fullValue.optionalDecimalValue)
case .erc721, .erc721ForTickets, .erc875:
case .erc721, .erc721ForTickets, .erc875, .erc1155:
balance = .nftBalance(.init(value: tokenObject.balance.count))
}
}
@ -123,9 +123,9 @@ extension Activity {
}
}
var isERC721AndNotForTickets: Bool {
var isERC721Or1155AndNotForTickets: Bool {
switch type {
case .erc721:
case .erc721, .erc1155:
return true
case .nativeCryptocurrency, .erc20, .erc875, .erc721ForTickets:
return false
@ -259,9 +259,9 @@ class TokenObject: Object {
}
}
var isERC721AndNotForTickets: Bool {
var isERC721Or1155AndNotForTickets: Bool {
switch type {
case .erc721:
case .erc721, .erc1155:
return true
case .nativeCryptocurrency, .erc20, .erc875, .erc721ForTickets:
return false
@ -285,7 +285,7 @@ func isZeroBalance(_ balance: String, tokenType: TokenType) -> Bool {
return true
}
return false
case .erc721, .erc721ForTickets:
case .erc721, .erc721ForTickets, .erc1155:
return balance.isEmpty
}
}

@ -8,6 +8,7 @@ enum TokenType: String {
case erc875 = "ERC875"
case erc721 = "ERC721"
case erc721ForTickets = "ERC721ForTickets"
case erc1155 = "ERC1155"
init(tokenInterfaceType: TokenInterfaceType) {
switch tokenInterfaceType {
@ -17,6 +18,18 @@ enum TokenType: String {
self = .erc721
case .erc875:
self = .erc875
case .erc1155:
self = .erc1155
}
}
//Leaky abstraction. We shouldn't update the balance if it is ERC1155, because we are just setting a dummy value to signal completion of token data detection
var shouldUpdateBalanceWhenDetected: Bool {
switch self {
case .nativeCryptocurrency, .erc20, .erc875, .erc721, .erc721ForTickets:
return true
case .erc1155:
return false
}
}
}

@ -7,6 +7,7 @@ import Result
import RealmSwift
import SwiftyJSON
// swiftlint:disable file_length
enum TokenError: Error {
case failedToFetch
}
@ -183,20 +184,20 @@ class TokensDataStore: NSObject {
func addCustomPromise(token: ERCToken) -> Promise<TokenObject> {
return Promise<TokenObject> { seal in
DispatchQueue.main.async {
let token = self.addCustom(token: token)
let token = self.addCustom(token: token, shouldUpdateBalance: true)
seal.fulfill(token)
}
}
}
@discardableResult func addCustom(token: ERCToken) -> TokenObject {
let newToken = TokensDataStore.tokenObject(ercToken: token)
@discardableResult func addCustom(token: ERCToken, shouldUpdateBalance: Bool) -> TokenObject {
let newToken = TokensDataStore.tokenObject(ercToken: token, shouldUpdateBalance: shouldUpdateBalance)
add(tokens: [newToken])
return newToken
}
private static func tokenObject(ercToken token: ERCToken) -> TokenObject {
private static func tokenObject(ercToken token: ERCToken, shouldUpdateBalance: Bool) -> TokenObject {
let newToken = TokenObject(
contract: token.contract,
server: token.server,
@ -207,8 +208,10 @@ class TokensDataStore: NSObject {
isCustom: true,
type: token.type
)
token.balance.forEach { balance in
newToken.balance.append(TokenBalance(balance: balance))
if shouldUpdateBalance {
token.balance.forEach { balance in
newToken.balance.append(TokenBalance(balance: balance))
}
}
return newToken
@ -236,8 +239,7 @@ class TokensDataStore: NSObject {
case .delegateContracts(let delegateContract):
realm.add(delegateContract, update: .all)
case .ercToken(let token):
let newToken = Self.tokenObject(ercToken: token)
let newToken = Self.tokenObject(ercToken: token, shouldUpdateBalance: token.type.shouldUpdateBalanceWhenDetected)
unsafeAddTokenOperation(tokenObject: newToken, realm: realm)
tokenObjects += [newToken]
case .tokenObject(let tokenObject):
@ -351,7 +353,7 @@ class TokensDataStore: NSObject {
var value: Bool?
switch tokenObject.type {
case .nativeCryptocurrency, .erc721, .erc875, .erc721ForTickets:
case .nativeCryptocurrency, .erc721, .erc875, .erc721ForTickets, .erc1155:
break
case .erc20:
value = strongSelf.updateTokenUnsafe(primaryKey: tokenObject.primaryKey, action: .type(.erc721))
@ -382,7 +384,7 @@ class TokensDataStore: NSObject {
balance: listOfJson
)
strongSelf.addCustom(token: token)
strongSelf.addCustom(token: token, shouldUpdateBalance: true)
seal.fulfill(true)
}
@ -507,3 +509,21 @@ extension TokenObject {
return .init(address: contractAddress, server: server)
}
}
extension TokensDataStore {
class functional {}
}
extension TokensDataStore.functional {
static func nonFungibleTokenType(fromTokenType tokenType: TokenType) -> NonFungibleFromJsonTokenType {
switch tokenType {
case .erc721, .erc721ForTickets:
return NonFungibleFromJsonTokenType.erc721
case .erc1155:
return NonFungibleFromJsonTokenType.erc1155
case .nativeCryptocurrency, .erc20, .erc875:
return NonFungibleFromJsonTokenType.erc721
}
}
}
// swiftlint:enable file_length

@ -272,7 +272,7 @@ class NewTokenViewController: UIViewController {
case .nativeCryptocurrency, .erc20:
decimalsViews.makeEach(isHidden: false)
balanceViews.makeEach(isHidden: true)
case .erc721, .erc875, .erc721ForTickets:
case .erc721, .erc875, .erc721ForTickets, .erc1155:
decimalsViews.makeEach(isHidden: true)
balanceViews.makeEach(isHidden: false)
}
@ -316,7 +316,7 @@ class NewTokenViewController: UIViewController {
decimalsTextField.status = .error(error.prettyError)
isValid = false
}
case .erc721, .erc875, .erc721ForTickets:
case .erc721, .erc875, .erc721ForTickets, .erc1155:
if balanceTextField.value.trimmed.isEmpty {
let error = ValidationError(msg: R.string.localizable.warningFieldRequired())
balanceTextField.status = .error(error.prettyError)

@ -167,7 +167,7 @@ extension SelectAssetViewController: UITableViewDataSource {
cell.accessoryType = viewModel.accessoryType(selectedToken, indexPath: indexPath)
return cell
case .erc721, .erc721ForTickets, .erc875:
case .erc721, .erc721ForTickets, .erc875, .erc1155:
let cell: NonFungibleTokenViewCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(viewModel: .init(token: token, server: server, assetDefinitionStore: assetDefinitionStore))
cell.accessoryType = viewModel.accessoryType(selectedToken, indexPath: indexPath)

@ -37,7 +37,7 @@ class TokenInstanceActionViewController: UIViewController, TokenVerifiableStatus
return true
case .erc20:
return true
case .erc721:
case .erc721, .erc1155:
return false
case .erc875:
return false

@ -47,6 +47,7 @@ class TokenViewController: UIViewController {
private let sessions: ServerDictionary<WalletSession>
private let activitiesService: ActivitiesServiceType
private var activitiesSubscriptionKey: Subscribable<ActivitiesViewModel>.SubscribableKey?
init(session: WalletSession, tokensDataStore: TokensDataStore, assetDefinition: AssetDefinitionStore, transactionType: TransactionType, analyticsCoordinator: AnalyticsCoordinator, token: TokenObject, viewModel: TokenViewControllerViewModel, activitiesService: ActivitiesServiceType, sessions: ServerDictionary<WalletSession>) {
self.tokenObject = token
self.viewModel = viewModel
@ -192,7 +193,7 @@ class TokenViewController: UIViewController {
tokenInfoPageView.viewModel.currencyAmount = session.balanceCoordinator.ethBalanceViewModel.currencyAmount
configure(viewModel: viewModel)
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .dapp, .tokenScript, .claimPaidErc875MagicLink:
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken,. ERC1155Token, .dapp, .tokenScript, .claimPaidErc875MagicLink:
break
}
}
@ -241,7 +242,7 @@ class TokenViewController: UIViewController {
tokenObject = TokenActionsServiceKey(tokenObject: token)
case .ERC20Token(let token, _, _):
tokenObject = TokenActionsServiceKey(tokenObject: token)
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .dapp, .tokenScript, .claimPaidErc875MagicLink:
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .ERC1155Token, .dapp, .tokenScript, .claimPaidErc875MagicLink:
tokenObject = .none
}

@ -31,7 +31,7 @@ class TokensViewController: UIViewController {
private let tokenCollection: TokenCollection
private let assetDefinitionStore: AssetDefinitionStore
private let eventsDataStore: EventsDataStoreProtocol
private var viewModel: TokensViewModel {
didSet {
viewModel.walletConnectSessions = oldValue.walletConnectSessions
@ -534,7 +534,7 @@ extension TokensViewController: UITableViewDataSource {
ticker: session.balanceCoordinator.coinTicker(token.addressAndRPCServer)
))
return cell
case .erc721, .erc721ForTickets:
case .erc721, .erc721ForTickets, .erc1155:
let cell: NonFungibleTokenViewCell = tableView.dequeueReusableCell(for: indexPath)
cell.configure(viewModel: .init(token: token, server: server, assetDefinitionStore: assetDefinitionStore))
return cell

@ -20,7 +20,7 @@ struct TokenInstanceViewModel {
.init(type: .nftSell),
.init(type: .nonFungibleTransfer)
]
case .erc721:
case .erc721, .erc1155:
return [
.init(type: .nonFungibleTransfer)
]

@ -20,7 +20,7 @@ struct TokenViewControllerViewModel {
return TokensDataStore.etherToken(forServer: session.server)
case .ERC20Token(let token, _, _):
return token
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .dapp, .tokenScript, .claimPaidErc875MagicLink:
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .ERC1155Token, .dapp, .tokenScript, .claimPaidErc875MagicLink:
return nil
}
}
@ -39,6 +39,8 @@ struct TokenViewControllerViewModel {
return []
case .erc721ForTickets:
return []
case .erc1155:
return []
case .erc20:
let actions: [TokenInstanceAction] = [
.init(type: .erc20Send),
@ -60,7 +62,7 @@ struct TokenViewControllerViewModel {
}
} else {
switch token.type {
case .erc875, .erc721, .erc721ForTickets:
case .erc875, .erc721, .erc721ForTickets, .erc1155:
return actionsFromTokenScript
case .erc20:
return actionsFromTokenScript + tokenActionsProvider.actions(token: key)
@ -106,7 +108,7 @@ struct TokenViewControllerViewModel {
return session.balanceCoordinator.ethBalanceViewModel.value
case .ERC20Token(let tokenObject, _, _):
return tokenObject.valueBigInt
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .dapp, .tokenScript, .claimPaidErc875MagicLink:
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .ERC1155Token, .dapp, .tokenScript, .claimPaidErc875MagicLink:
return nil
}
}

@ -26,7 +26,7 @@ struct TokensCardViewModel {
.init(type: .nftSell),
.init(type: .nonFungibleTransfer)
]
case .erc721:
case .erc721, .erc1155:
return [
.init(type: .nonFungibleTransfer)
]

@ -205,6 +205,13 @@ extension TokensViewModel.functional {
//Remove tokens that look unwanted in the Wallet tab
static func filterAwaySpuriousTokens(_ tokens: [TokenObject]) -> [TokenObject] {
tokens.filter { !($0.name.isEmpty && $0.symbol.isEmpty && $0.decimals == 0) }
tokens.filter {
switch $0.type {
case .nativeCryptocurrency, .erc20, .erc875, .erc721, .erc721ForTickets:
return !($0.name.isEmpty && $0.symbol.isEmpty && $0.decimals == 0)
case .erc1155:
return true
}
}
}
}

@ -30,7 +30,7 @@ class ReplaceTransactionCoordinator: Coordinator {
switch transactionType {
case .nativeCryptocurrency:
return AlphaWallet.Address(string: transaction.to)
case .dapp, .ERC20Token, .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .tokenScript, .claimPaidErc875MagicLink:
case .dapp, .ERC20Token, .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .ERC1155Token, .tokenScript, .claimPaidErc875MagicLink:
return nil
}
}
@ -38,7 +38,7 @@ class ReplaceTransactionCoordinator: Coordinator {
switch transactionType {
case .nativeCryptocurrency:
return nil
case .dapp, .ERC20Token, .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .tokenScript, .claimPaidErc875MagicLink:
case .dapp, .ERC20Token, .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .ERC1155Token, .tokenScript, .claimPaidErc875MagicLink:
return AlphaWallet.Address(string: transaction.to)
}
}

@ -427,7 +427,7 @@ extension TokensCardCoordinator: TokensCardViewControllerDelegate {
func didPressTransfer(token: TokenObject, tokenHolder: TokenHolder, for type: PaymentFlow, tokenHolders: [TokenHolder], in viewController: TokensCardViewController) {
switch token.type {
case .erc721:
case .erc721, .erc1155:
let vc = makeTransferTokensCardViaWalletAddressViewController(token: token, for: tokenHolder, paymentFlow: type)
transferTokensViewController = vc
vc.navigationItem.largeTitleDisplayMode = .never
@ -480,7 +480,7 @@ extension TokensCardCoordinator: TokenInstanceViewControllerDelegate {
func didPressTransfer(token: TokenObject, tokenHolder: TokenHolder, forPaymentFlow paymentFlow: PaymentFlow, in viewController: TokenInstanceViewController) {
switch token.type {
case .erc721:
case .erc721, .erc1155:
let vc = makeTransferTokensCardViaWalletAddressViewController(token: token, for: tokenHolder, paymentFlow: paymentFlow)
transferTokensViewController = vc
vc.navigationItem.largeTitleDisplayMode = .never

@ -268,6 +268,8 @@ class TransactionsStorage: Hashable {
tokenType = .erc721
case .erc875TokenTransfer:
tokenType = .erc875
case .erc1155TokenTransfer:
tokenType = .erc1155
case .unknown:
tokenType = .erc20
}

@ -40,7 +40,7 @@ struct TransactionRowCellViewModel {
}
if let operation = operation {
switch operation.operationType {
case .nativeCurrencyTokenTransfer, .erc20TokenTransfer, .erc721TokenTransfer, .erc875TokenTransfer:
case .nativeCurrencyTokenTransfer, .erc20TokenTransfer, .erc721TokenTransfer, .erc875TokenTransfer, .erc1155TokenTransfer:
return R.string.localizable.transactionCellTokenTransferTitle(operation.symbol ?? "")
case .erc20TokenApprove:
return R.string.localizable.transactionCellTokenApproveTitle(operation.symbol ?? "")

@ -99,7 +99,7 @@ class TransactionConfigurator {
switch transaction.transactionType {
case .nativeCryptocurrency:
return transaction.recipient
case .dapp, .ERC20Token, .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .tokenScript, .claimPaidErc875MagicLink:
case .dapp, .ERC20Token, .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .ERC1155Token, .tokenScript, .claimPaidErc875MagicLink:
return transaction.contract
}
}
@ -113,6 +113,7 @@ class TransactionConfigurator {
case .ERC875TokenOrder: return transaction.value
case .ERC721Token: return 0
case .ERC721ForTicketToken: return 0
case .ERC1155Token: return 0
case .tokenScript: return transaction.value
case .claimPaidErc875MagicLink: return transaction.value
}
@ -370,7 +371,7 @@ class TransactionConfigurator {
let encoder = ABIEncoder()
try encoder.encode(function: functionEncoder, arguments: parameters)
return createConfiguration(server: server, transaction: transaction, gasLimit: transaction.gasLimit ?? GasLimitConfiguration.maxGasLimit, data: encoder.data)
case .ERC721Token(let token), .ERC721ForTicketToken(let token):
case .ERC721Token(let token), .ERC721ForTicketToken(let token), .ERC1155Token(let token):
let function: Function
let parameters: [Any]

@ -75,7 +75,7 @@ class SendCoordinator: Coordinator {
case .ERC20Token(_, let destination, let amount):
controller.targetAddressTextField.value = destination?.stringValue ?? ""
controller.amountTextField.ethCost = amount ?? ""
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .dapp, .tokenScript, .claimPaidErc875MagicLink:
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .ERC1155Token, .dapp, .tokenScript, .claimPaidErc875MagicLink:
break
}
controller.delegate = self

@ -18,6 +18,8 @@ enum TransactionType {
return .ERC721Token(token)
case .erc721ForTickets:
return .ERC721ForTicketToken(token)
case .erc1155:
return .ERC1155Token(token)
}
}()
}
@ -28,6 +30,7 @@ enum TransactionType {
case ERC875TokenOrder(TokenObject)
case ERC721Token(TokenObject)
case ERC721ForTicketToken(TokenObject)
case ERC1155Token(TokenObject)
case dapp(TokenObject, DAppRequester)
case claimPaidErc875MagicLink(TokenObject)
case tokenScript(TokenObject)
@ -38,7 +41,7 @@ enum TransactionType {
return nil
case .ERC20Token(let token, _, _):
return token.contractAddress
case .dapp, .tokenScript, .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .claimPaidErc875MagicLink:
case .dapp, .tokenScript, .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .ERC1155Token, .claimPaidErc875MagicLink:
return nil
}
}
@ -66,6 +69,8 @@ extension TransactionType {
return token.symbol
case .ERC721ForTicketToken(let token):
return token.symbol
case .ERC1155Token(let token):
return token.symbol
case .claimPaidErc875MagicLink(let token):
return token.symbol
}
@ -87,6 +92,8 @@ extension TransactionType {
return token
case .ERC721ForTicketToken(let token):
return token
case .ERC1155Token(let token):
return token
case .claimPaidErc875MagicLink(let token):
return token
}
@ -108,6 +115,8 @@ extension TransactionType {
return token.server
case .ERC721ForTicketToken(let token):
return token.server
case .ERC1155Token(let token):
return token.server
case .claimPaidErc875MagicLink(let token):
return token.server
}
@ -127,6 +136,8 @@ extension TransactionType {
return token.contractAddress
case .ERC721ForTicketToken(let token):
return token.contractAddress
case .ERC1155Token(let token):
return token.contractAddress
case .dapp(let token, _), .tokenScript(let token), .claimPaidErc875MagicLink(let token):
return token.contractAddress
}

@ -243,7 +243,7 @@ class SendViewController: UIViewController {
if let amount = amount {
amountTextField.ethCost = amount
}
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .dapp, .tokenScript, .claimPaidErc875MagicLink:
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .ERC1155Token, .dapp, .tokenScript, .claimPaidErc875MagicLink:
currentSubscribableKeyForNativeCryptoCurrencyPrice.flatMap { ethPrice.unsubscribe($0) }
amountTextField.cryptoToDollarRate = nil
}
@ -273,7 +273,7 @@ class SendViewController: UIViewController {
isAllFunds = true
amountTextField.set(ethCost: ethCost.allFundsFullValue, shortEthCost: ethCost.allFundsShortValue, useFormatting: false)
case .dapp, .ERC721ForTicketToken, .ERC721Token, .ERC875Token, .ERC875TokenOrder, .tokenScript, .claimPaidErc875MagicLink:
case .dapp, .ERC721ForTicketToken, .ERC721Token, .ERC875Token, .ERC1155Token, .ERC875TokenOrder, .tokenScript, .claimPaidErc875MagicLink:
break
}
}
@ -291,7 +291,7 @@ class SendViewController: UIViewController {
let shortValue = EtherNumberFormatter.shortPlain.string(from: token.valueBigInt, decimals: token.decimals).droppedTrailingZeros
return (fullValue.optionalDecimalValue, shortValue)
case .dapp, .ERC721ForTicketToken, .ERC721Token, .ERC875Token, .ERC875TokenOrder, .tokenScript, .claimPaidErc875MagicLink:
case .dapp, .ERC721ForTicketToken, .ERC721Token, .ERC875Token, .ERC1155Token, .ERC875TokenOrder, .tokenScript, .claimPaidErc875MagicLink:
return nil
}
}
@ -304,7 +304,7 @@ class SendViewController: UIViewController {
switch transactionType {
case .nativeCryptocurrency, .dapp, .tokenScript, .claimPaidErc875MagicLink:
checkIfGreaterThanZero = false
case .ERC20Token, .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken:
case .ERC20Token, .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .ERC1155Token:
checkIfGreaterThanZero = true
}
@ -354,7 +354,7 @@ class SendViewController: UIViewController {
case .ERC20Token(let token, let recipient, let amount):
let amount = amount.flatMap { EtherNumberFormatter.plain.number(from: $0, decimals: token.decimals) }
configureFor(contract: viewModel.transactionType.contract, recipient: recipient, amount: amount, shouldConfigureBalance: false)
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .dapp, .tokenScript, .claimPaidErc875MagicLink:
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken,. ERC1155Token, .dapp, .tokenScript, .claimPaidErc875MagicLink:
break
}
}
@ -415,7 +415,7 @@ class SendViewController: UIViewController {
type: .erc20,
balance: ["0"]
)
self.storage.addCustom(token: token)
self.storage.addCustom(token: token, shouldUpdateBalance: true)
self.configureFor(contract: contract, recipient: recipient, amount: amount)
self.activateAmountView()
case .delegateTokenComplete:
@ -449,7 +449,7 @@ class SendViewController: UIViewController {
transactionType = TransactionType(token: tokenObject, recipient: recipient, amount: amount.flatMap { EtherNumberFormatter().string(from: $0, units: .ether) })
case .ERC20Token(_, _, let amount):
transactionType = TransactionType(token: tokenObject, recipient: recipient, amount: amount)
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .dapp, .tokenScript, .claimPaidErc875MagicLink:
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken,. ERC1155Token, .dapp, .tokenScript, .claimPaidErc875MagicLink:
transactionType = TransactionType(token: tokenObject, recipient: recipient, amount: nil)
}
}

@ -207,7 +207,7 @@ class TransactionConfirmationViewController: UIViewController {
sendFungiblesViewModel.cryptoToDollarRate = cryptoToDollarRate
strongSelf.generateSubviews()
}
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .dapp, .tokenScript, .claimPaidErc875MagicLink:
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .ERC1155Token, .dapp, .tokenScript, .claimPaidErc875MagicLink:
sendFungiblesViewModel.ethPrice.subscribe { [weak self] cryptoToDollarRate in
guard let strongSelf = self else { return }
sendFungiblesViewModel.cryptoToDollarRate = cryptoToDollarRate
@ -364,7 +364,7 @@ class TransactionConfirmationViewController: UIViewController {
let balanceBaseViewModel = sendFungiblesViewModel.session.balanceCoordinator.ethBalanceViewModel
sendFungiblesViewModel.updateBalance(.nativeCryptocurrency(balanceViewModel: balanceBaseViewModel))
case .ERC20Token, .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .dapp, .tokenScript, .claimPaidErc875MagicLink:
case .ERC20Token, .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .ERC1155Token, .dapp, .tokenScript, .claimPaidErc875MagicLink:
break
}
case .sendNftTransaction, .claimPaidErc875MagicLink:

@ -64,7 +64,7 @@ struct ConfigureTransactionViewModel {
switch transactionType {
case .nativeCryptocurrency, .dapp, .tokenScript, .claimPaidErc875MagicLink:
return false
case .ERC20Token, .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken:
case .ERC20Token, .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .ERC1155Token:
return true
}
}

@ -196,7 +196,7 @@ struct TokenInfoPageViewModel {
} else {
return marketPriceAttributedString
}
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .dapp, .tokenScript, .claimPaidErc875MagicLink:
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .ERC1155Token, .dapp, .tokenScript, .claimPaidErc875MagicLink:
return nil
}
}

@ -38,6 +38,8 @@ struct SendViewModel {
return token
case .ERC721ForTicketToken(let token):
return token
case .ERC1155Token(let token):
return token
case .dapp, .tokenScript, .claimPaidErc875MagicLink:
return nil
}
@ -69,7 +71,7 @@ struct SendViewModel {
return true
}
return false
case .ERC20Token, .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .dapp, .tokenScript, .claimPaidErc875MagicLink:
case .ERC20Token, .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .ERC1155Token, .dapp, .tokenScript, .claimPaidErc875MagicLink:
return true
}
}
@ -78,7 +80,7 @@ struct SendViewModel {
switch transactionType {
case .nativeCryptocurrency, .ERC20Token:
return false
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .dapp, .tokenScript, .claimPaidErc875MagicLink:
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .ERC1155Token, .dapp, .tokenScript, .claimPaidErc875MagicLink:
return true
}
}
@ -90,7 +92,7 @@ struct SendViewModel {
case .ERC20Token(let token, _, _):
let value = EtherNumberFormatter.short.string(from: token.valueBigInt, decimals: token.decimals)
return R.string.localizable.sendAvailable("\(value) \(transactionType.symbol)")
case .dapp, .ERC721ForTicketToken, .ERC721Token, .ERC875Token, .ERC875TokenOrder, .tokenScript, .claimPaidErc875MagicLink:
case .dapp, .ERC721ForTicketToken, .ERC721Token, .ERC875Token, .ERC875TokenOrder, .ERC1155Token, .tokenScript, .claimPaidErc875MagicLink:
break
}
@ -104,7 +106,7 @@ struct SendViewModel {
case .ERC20Token(let token, _, _):
let tokenBalance = storage.token(forContract: token.contractAddress)?.valueBigInt
return tokenBalance == nil
case .dapp, .ERC721ForTicketToken, .ERC721Token, .ERC875Token, .ERC875TokenOrder, .tokenScript, .claimPaidErc875MagicLink:
case .dapp, .ERC721ForTicketToken, .ERC721Token, .ERC875Token, .ERC1155Token, .ERC875TokenOrder, .tokenScript, .claimPaidErc875MagicLink:
break
}
return true
@ -125,6 +127,8 @@ struct SendViewModel {
return EtherNumberFormatter.full.number(from: amountString, decimals: token.decimals)
case .ERC721ForTicketToken(let token):
return EtherNumberFormatter.full.number(from: amountString, decimals: token.decimals)
case .ERC1155Token(let token):
return EtherNumberFormatter.full.number(from: amountString, decimals: token.decimals)
}
}()
@ -141,7 +145,7 @@ struct SendViewModel {
if let tokenBalance = storage.token(forContract: token.contractAddress)?.valueBigInt, tokenBalance < value {
return nil
}
case .dapp, .ERC721ForTicketToken, .ERC721Token, .ERC875Token, .ERC875TokenOrder, .tokenScript, .claimPaidErc875MagicLink:
case .dapp, .ERC721ForTicketToken, .ERC721Token, .ERC875Token, .ERC1155Token, .ERC875TokenOrder, .tokenScript, .claimPaidErc875MagicLink:
break
}

@ -218,7 +218,7 @@ extension TransactionConfirmationViewModel {
} else {
return "\(amount.value) \(token.symbol)"
}
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .dapp, .tokenScript, .claimPaidErc875MagicLink:
case .ERC875Token, .ERC875TokenOrder, .ERC721Token, .ERC721ForTicketToken, .ERC1155Token, .dapp, .tokenScript, .claimPaidErc875MagicLink:
return String()
}
}

@ -35,7 +35,7 @@ extension TokenObject {
if let img = server.iconImage {
return .init((image: img, symbol: "", isFinal: true))
}
case .erc20, .erc875, .erc721, .erc721ForTickets:
case .erc20, .erc875, .erc721, .erc721ForTickets, .erc1155:
if let img = contractAddress.tokenImage {
return .init((image: img, symbol: "", isFinal: true))
}
@ -116,7 +116,7 @@ class TokenImageFetcher {
Promise { seal in
queue.async {
switch type {
case .erc721:
case .erc721, .erc1155:
if let json = balance, let data = json.data(using: .utf8), let openSeaNonFungible = nonFungible(fromJsonData: data), !openSeaNonFungible.contractImageUrl.isEmpty {
let request = URLRequest(url: URL(string: openSeaNonFungible.contractImageUrl)!)
fetch(request: request, queue: queue).done(on: queue, { image in

Loading…
Cancel
Save