Fix: Auto-detect tokens and pick up transactions for Polygon, Binance, etc

Bigger PR than I had hoped.

* Detects ERC721 token (IDs) better by checking for ERC721 receive transactions for token IDs, then using `tokenURI()` to get each token ID's details. So this works even for chains that aren't supported by OpenSea. Thanks @JamesSmartCell!
* Fix: some app-generated token icons weren't appearing
* Some (tiny) logic created outside of coordinators (was getting out of hand stuff logic in coordinators). eg: `AlphaWallet/Tokens/Models/Erc721Contract.swift`. Probably good to start doing this for new code and gradually cleaning up old code @vladyslav-iosdev
* Fixed etherscan API hostnames. eg should include `api-` prefix in several hostnames eg. `api-kovan.etherscan.io`. Also distinguish between Etherscan-compatible and Blockscout compatible ones as they have slightly different routes and outputs. Had to include some refactoring. Didn't overhaul it completely to minimize change in this PR. Part of this include removing some of the URLs specified in `Constants.swift` that are only used one (and only in `RPCServer.swift`. They have been moved (partially) into `RPCServer.swift`.
* Hence, several chains like Polygon mainnet, and Binance mainnet and testnet which was not picking up transactions (and hence auto-detecting tokens) properly should do so now)
pull/2819/head
Hwee-Boon Yar 3 years ago
parent b6c0eb7b2e
commit eb60bede53
  1. 28
      AlphaWallet.xcodeproj/project.pbxproj
  2. 6
      AlphaWallet/Core/Initializers/MigrationInitializer.swift
  3. 2
      AlphaWallet/EtherClient/TrustClient/AlphaWalletService.swift
  4. 1
      AlphaWallet/EtherClient/TrustClient/Models/LocalizedOperation.swift
  5. 4
      AlphaWallet/EtherClient/TrustClient/Models/RawTransaction.swift
  6. 12
      AlphaWallet/InCoordinator.swift
  7. 2
      AlphaWallet/RPC/Commands/web3swift-pod/GetERC721BalanceEncode.swift
  8. 13
      AlphaWallet/RPC/Commands/web3swift-pod/GetERC721TokenUri.swift
  9. 16
      AlphaWallet/Settings/Types/Config.swift
  10. 19
      AlphaWallet/Settings/Types/Constants.swift
  11. 119
      AlphaWallet/Settings/Types/RPCServers.swift
  12. 85
      AlphaWallet/Tokens/Coordinators/GetContractInteractions.swift
  13. 5
      AlphaWallet/Tokens/Coordinators/GetERC721BalanceCoordinator.swift
  14. 23
      AlphaWallet/Tokens/Helpers/TokenAdaptor.swift
  15. 48
      AlphaWallet/Tokens/Models/Erc721Contract.swift
  16. 29
      AlphaWallet/Tokens/Types/NonFungibleFromJson.swift
  17. 19
      AlphaWallet/Tokens/Types/NonFungibleFromTokenUri.swift
  18. 4
      AlphaWallet/Tokens/Types/OpenSeaNonFungible.swift
  19. 16
      AlphaWallet/Tokens/Types/OpenSeaNonFungibleTokenHandling.swift
  20. 107
      AlphaWallet/Tokens/Types/TokensDataStore.swift
  21. 6
      AlphaWallet/Tokens/ViewControllers/TokenInstanceActionViewController.swift
  22. 6
      AlphaWallet/Tokens/ViewControllers/TokenInstanceViewController.swift
  23. 6
      AlphaWallet/Tokens/ViewControllers/TokensCardViewController.swift
  24. 1
      AlphaWallet/Tokens/ViewControllers/TokensViewController.swift
  25. 1
      AlphaWallet/Tokens/Views/NonFungibleTokenViewCell.swift
  26. 65
      AlphaWallet/Transactions/Coordinators/SingleChainTransactionEtherscanDataCoordinator.swift
  27. 4
      AlphaWallet/Transactions/Storage/Transaction.swift
  28. 46
      AlphaWallet/Transactions/Storage/TransactionsStorage.swift
  29. 8
      AlphaWallet/Transactions/Types/Erc721TokenIdsFetcher.swift
  30. 8
      AlphaWallet/Transactions/Types/LocalizedOperationObject.swift
  31. 4
      AlphaWallet/UI/TokenObject+UI.swift

@ -263,6 +263,7 @@
5E7C71A2EAA5124E07AA54B6 /* Favicon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7BAC4E511FE8446D212F /* Favicon.swift */; };
5E7C71A6B0BDF301747A49AE /* ScreenChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C77E1E6194F5A1DC8D645 /* ScreenChecker.swift */; };
5E7C71A7D2BD6FCE3980CC51 /* ImportWalletHelpBubbleViewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7A16ABC8BD5D508AA641 /* ImportWalletHelpBubbleViewViewModel.swift */; };
5E7C71AE26F2201CFAC81C99 /* Erc721Contract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C70360398F1FF5493FB5A /* Erc721Contract.swift */; };
5E7C71AE5E97BDA5BB685A9F /* Dapp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7D8D618A8A8D55479CDF /* Dapp.swift */; };
5E7C71B412DAE4BD7920436B /* ABIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7558286761EF1ADD2988 /* ABIError.swift */; };
5E7C71B52A77008694BFA5D1 /* TokensDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C74B9EB81C51E956566E7 /* TokensDataStore.swift */; };
@ -304,6 +305,7 @@
5E7C7307F0253556A6BC57A9 /* AlphaWalletAddressExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7EE6BFC8BB79CD1C5565 /* AlphaWalletAddressExtension.swift */; };
5E7C73092E428F9519787284 /* Core.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C799E4784815CB0202820 /* Core.swift */; };
5E7C730C6AEF556AFB9A4B2C /* LocalesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7BF09AD68C113D58344C /* LocalesViewController.swift */; };
5E7C730CA247D13ECBCB94ED /* NonFungibleFromJson.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7B38E58247C6A3715BC4 /* NonFungibleFromJson.swift */; };
5E7C7317533D24B6A292F88D /* UIStackView+Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C73ED9226646D562B5A3C /* UIStackView+Array.swift */; };
5E7C731D0F6128BE8885A2D3 /* ServersCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7B8FD1E2BCC325DF4EE4 /* ServersCoordinator.swift */; };
5E7C732BD09AABEEE6096BF4 /* ServersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C74C0C1803DD17FE9EBA7 /* ServersViewController.swift */; };
@ -313,12 +315,14 @@
5E7C7350C5F9ADE212A0F1CA /* CallForAssetAttributeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C74F4900AB6D34CDD3674 /* CallForAssetAttributeCoordinator.swift */; };
5E7C7376B566E5A59CC8F463 /* ImportMagicTokenViewControllerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C72D0E7CA03ADE5CFAE7A /* ImportMagicTokenViewControllerViewModel.swift */; };
5E7C738BCA59B1DE116ECC96 /* WhereIsWalletAddressFoundOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C76052512831B707659CA /* WhereIsWalletAddressFoundOverlayView.swift */; };
5E7C738FFC4292F6B0D6BC4B /* Erc721TokenIdsFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C75E3F2155466F46BB139 /* Erc721TokenIdsFetcher.swift */; };
5E7C739447F6BADDBBBF7278 /* AssetDefinitionsOverridesViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7695F7C45A31C7EAF97F /* AssetDefinitionsOverridesViewCellViewModel.swift */; };
5E7C739611741D5E61900992 /* Eip681Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7AE608A39793F8BA3911 /* Eip681Parser.swift */; };
5E7C73AB79C2C5FC6C76CDFF /* ERC20-TokenScript.tsml in Resources */ = {isa = PBXBuildFile; fileRef = 5E7C78795F6336DDBE2EB4C5 /* ERC20-TokenScript.tsml */; };
5E7C73CE76679D1E1D6714F7 /* SolidityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C43F1B371836552CC18 /* SolidityType.swift */; };
5E7C73E7A68C56162FA2E845 /* SingleChainTransactionEtherscanDataCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C74259A3F3B0277B0E8C5 /* SingleChainTransactionEtherscanDataCoordinator.swift */; };
5E7C73F6762376F5B2B4214D /* EtherKeystore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7BA8A301CEEDE36D76A3 /* EtherKeystore.swift */; };
5E7C73F723684B295A8DEEB3 /* GetERC721TokenUri.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7870764B15D5E213604B /* GetERC721TokenUri.swift */; };
5E7C73FB906BF267F28B0205 /* ActivityRowType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C762AA80515C4E0A87C44 /* ActivityRowType.swift */; };
5E7C7402B29A987B0AF7061D /* VerifiableStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7CC48CA7A1EA7D539C87 /* VerifiableStatusViewController.swift */; };
5E7C740398B56DAED1D0C75A /* XmlContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7FBADEDE47DC37B9197A /* XmlContext.swift */; };
@ -554,6 +558,7 @@
5E7C7D6AC52076681FE8C43E /* AssetDefinitionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7716E500124CA910FB2C /* AssetDefinitionStore.swift */; };
5E7C7D71D3184F44C397FFE7 /* HelpViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C715F395B973FB61056CF /* HelpViewController.swift */; };
5E7C7D7A68C7780C293301F7 /* HDWalletTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C75DE215F0AAEF284948F /* HDWalletTest.swift */; };
5E7C7D7D1531464B489871EA /* NonFungibleFromTokenUri.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C73EA8FD43F18DDEB6965 /* NonFungibleFromTokenUri.swift */; };
5E7C7D8173CB1089D622DA38 /* HelpViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7646352F10C96B5FC6F6 /* HelpViewCell.swift */; };
5E7C7D918B514B4C2F8DF296 /* EventSourceCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7D4B3B2FEA6D4FDAE01C /* EventSourceCoordinator.swift */; };
5E7C7DC485AD4401A3F6D071 /* TokenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7EC53B2B5DFAAC7965EC /* TokenViewController.swift */; };
@ -1118,6 +1123,7 @@
5E7C702300BB7DB0FD7788EF /* XMLHandlerTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLHandlerTest.swift; sourceTree = "<group>"; };
5E7C702A684DF27DC8ED4E42 /* TokenObjectTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenObjectTest.swift; sourceTree = "<group>"; };
5E7C703366F010BFEF6B06C6 /* OpenSeaNonFungibleTokenViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenSeaNonFungibleTokenViewCell.swift; sourceTree = "<group>"; };
5E7C70360398F1FF5493FB5A /* Erc721Contract.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Erc721Contract.swift; sourceTree = "<group>"; };
5E7C7037994332AE52798488 /* AssetAttributeSyntax.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetAttributeSyntax.swift; sourceTree = "<group>"; };
5E7C703BA1D0E9ACB7399155 /* TransferTokensCardQuantitySelectionViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransferTokensCardQuantitySelectionViewModel.swift; sourceTree = "<group>"; };
5E7C704499C81ACA3B08A752 /* TokenInstanceWebView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenInstanceWebView.swift; sourceTree = "<group>"; };
@ -1192,6 +1198,7 @@
5E7C73DF5FBFE756097D32B1 /* EthCurrencyHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EthCurrencyHelper.swift; sourceTree = "<group>"; };
5E7C73E57ADDF29E0A5FB87E /* OpenSeaNonFungibleTokenCardRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenSeaNonFungibleTokenCardRowView.swift; sourceTree = "<group>"; };
5E7C73E8500C2573331D800D /* Function.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Function.swift; sourceTree = "<group>"; };
5E7C73EA8FD43F18DDEB6965 /* NonFungibleFromTokenUri.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NonFungibleFromTokenUri.swift; sourceTree = "<group>"; };
5E7C73ED9226646D562B5A3C /* UIStackView+Array.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIStackView+Array.swift"; sourceTree = "<group>"; };
5E7C73EFA9494B31C683A287 /* TimeEntryField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TimeEntryField.swift; path = Views/TimeEntryField.swift; sourceTree = "<group>"; };
5E7C740BBC5AAF5C545CCC6A /* EventOrigin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventOrigin.swift; sourceTree = "<group>"; };
@ -1244,6 +1251,7 @@
5E7C75D384C0D727BB43305E /* SettingsHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsHeaderView.swift; sourceTree = "<group>"; };
5E7C75D9C3EA9FF978ECF8E5 /* DAI.tsml */ = {isa = PBXFileReference; lastKnownFileType = file.tsml; path = DAI.tsml; sourceTree = "<group>"; };
5E7C75DE215F0AAEF284948F /* HDWalletTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HDWalletTest.swift; sourceTree = "<group>"; };
5E7C75E3F2155466F46BB139 /* Erc721TokenIdsFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Erc721TokenIdsFetcher.swift; sourceTree = "<group>"; };
5E7C75E7D995ABE0E6B7AD55 /* DappButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DappButton.swift; sourceTree = "<group>"; };
5E7C75F0DB84DB0F5449D6C1 /* WalletConnectSessionsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectSessionsViewController.swift; sourceTree = "<group>"; };
5E7C75F65E8C1E20EBA6A5F4 /* Scrollable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Scrollable.swift; sourceTree = "<group>"; };
@ -1306,6 +1314,7 @@
5E7C78454B11E39CF8B5E695 /* EnterPasswordCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnterPasswordCoordinator.swift; sourceTree = "<group>"; };
5E7C785114A3266813AC92A6 /* AssetDefinitionInMemoryBackingStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetDefinitionInMemoryBackingStore.swift; sourceTree = "<group>"; };
5E7C786486937661D0DCD4E2 /* Origin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Origin.swift; sourceTree = "<group>"; };
5E7C7870764B15D5E213604B /* GetERC721TokenUri.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetERC721TokenUri.swift; sourceTree = "<group>"; };
5E7C78795F6336DDBE2EB4C5 /* ERC20-TokenScript.tsml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.tsml; path = "ERC20-TokenScript.tsml"; sourceTree = "<group>"; };
5E7C787E1D2A6529C07709DB /* AssetDefinitionDiskBackingStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetDefinitionDiskBackingStore.swift; sourceTree = "<group>"; };
5E7C788ADDEA0609433B1FDF /* VerifySeedPhraseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerifySeedPhraseViewController.swift; sourceTree = "<group>"; };
@ -1381,6 +1390,7 @@
5E7C7B211FF0FE5BE98BB7D0 /* aETH.tsml */ = {isa = PBXFileReference; lastKnownFileType = file.tsml; path = aETH.tsml; sourceTree = "<group>"; };
5E7C7B29A9E728402D144C05 /* AppLocale.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppLocale.swift; sourceTree = "<group>"; };
5E7C7B3302309706CA0F972A /* TokensViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokensViewController.swift; sourceTree = "<group>"; };
5E7C7B38E58247C6A3715BC4 /* NonFungibleFromJson.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NonFungibleFromJson.swift; sourceTree = "<group>"; };
5E7C7B43816C35C3FE2EFFBE /* TokenInstanceAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenInstanceAction.swift; sourceTree = "<group>"; };
5E7C7B54826BFDD53DF3E5BF /* DiscoverDappCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscoverDappCell.swift; sourceTree = "<group>"; };
5E7C7B5838E12930000D5029 /* TokenViewControllerTransactionCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenViewControllerTransactionCellViewModel.swift; sourceTree = "<group>"; };
@ -2159,6 +2169,7 @@
2977CAF01F7E0C2E009682A0 /* Views */,
442FCB1A0F74A68425907AC9 /* Helpers */,
5E7C7E2DCCE0D775ECF83088 /* WalletFilter.swift */,
5E7C72C3D9C939508F8EBFAC /* Models */,
);
path = Tokens;
sourceTree = "<group>";
@ -2446,6 +2457,8 @@
87A0C93125AEF1E400E73F60 /* EventInstanceValue.swift */,
8703F663261319B80082EE25 /* AddressAndRPCServer.swift */,
8703F66626135B330082EE25 /* ChartHistory.swift */,
5E7C73EA8FD43F18DDEB6965 /* NonFungibleFromTokenUri.swift */,
5E7C7B38E58247C6A3715BC4 /* NonFungibleFromJson.swift */,
);
path = Types;
sourceTree = "<group>";
@ -2698,6 +2711,7 @@
2963A2891FC402940095447D /* LocalizedOperationObject.swift */,
294DFBA21FE0E2EA004CEB56 /* TransactionValue.swift */,
77872D222023F43B0032D687 /* TransactionsTracker.swift */,
5E7C75E3F2155466F46BB139 /* Erc721TokenIdsFetcher.swift */,
);
path = Types;
sourceTree = "<group>";
@ -3189,6 +3203,14 @@
path = OpenSea;
sourceTree = "<group>";
};
5E7C72C3D9C939508F8EBFAC /* Models */ = {
isa = PBXGroup;
children = (
5E7C70360398F1FF5493FB5A /* Erc721Contract.swift */,
);
path = Models;
sourceTree = "<group>";
};
5E7C7372BDD012864CBFE456 /* ViewModels */ = {
isa = PBXGroup;
children = (
@ -3608,6 +3630,7 @@
8499DA6A661E357C9D69BA18 /* GetERC721ForTicketsBalance.swift */,
8499D5AD5EA8967FDB7FA6E2 /* GetInterfaceSupported165Encode.swift */,
5E7C7C097681EC7E022B6C44 /* ENSReverseLookupEncode.swift */,
5E7C7870764B15D5E213604B /* GetERC721TokenUri.swift */,
);
path = "web3swift-pod";
sourceTree = "<group>";
@ -5450,6 +5473,11 @@
5E7C7C60BAF11B0BD135FC1E /* GroupActivityViewCell.swift in Sources */,
5E7C7BFE70D7C2CC0C84B72A /* GroupActivityCellViewModel.swift in Sources */,
5E7C7DEE91E5A1B4A41826EB /* ActivityRowModel.swift in Sources */,
5E7C7D7D1531464B489871EA /* NonFungibleFromTokenUri.swift in Sources */,
5E7C730CA247D13ECBCB94ED /* NonFungibleFromJson.swift in Sources */,
5E7C71AE26F2201CFAC81C99 /* Erc721Contract.swift in Sources */,
5E7C73F723684B295A8DEEB3 /* GetERC721TokenUri.swift in Sources */,
5E7C738FFC4292F6B0D6BC4B /* Erc721TokenIdsFetcher.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

@ -13,7 +13,7 @@ class MigrationInitializer: Initializer {
}
func perform() {
config.schemaVersion = 8
config.schemaVersion = 9
//NOTE: use [weak self] to avoid memory leak
config.migrationBlock = { [weak self] migration, oldSchemaVersion in
guard let strongSelf = self else { return }
@ -80,6 +80,10 @@ class MigrationInitializer: Initializer {
}
migration.deleteData(forType: EventActivity.className())
}
if oldSchemaVersion < 9 {
//no-op
}
}
}
}

@ -53,7 +53,7 @@ extension AlphaWalletService: TargetType {
case .getTransactions(_, let server, _, _, _, _):
switch server {
case .main, .classic, .callisto, .kovan, .ropsten, .custom, .rinkeby, .poa, .sokol, .goerli, .xDai, .artis_sigma1, .artis_tau1, .binance_smart_chain, .binance_smart_chain_testnet, .heco, .heco_testnet, .fantom, .fantom_testnet, .avalanche, .avalanche_testnet, .polygon, .mumbai_testnet:
return "/api"
return ""
}
case .register:
return "/push/register"

@ -27,6 +27,7 @@ struct LocalizedOperation: Decodable {
case to
case type
case value
//case tokenID
case contract
}

@ -100,7 +100,7 @@ extension TransactionInstance {
func generateLocalizedOperation(value: BigUInt, contract: AlphaWallet.Address, to recipient: AlphaWallet.Address, functionCall: DecodedFunctionCall) -> Promise<[LocalizedOperationObjectInstance]> {
if let token = tokensStorage.tokenThreadSafe(forContract: contract) {
let operationType = mapTokenTypeToTransferOperationType(token.type, functionCall: functionCall)
let result = LocalizedOperationObjectInstance(from: transaction.from, to: recipient.eip55String, contract: contract, type: operationType.rawValue, value: String(value), symbol: token.symbol, name: token.name, decimals: token.decimals)
let result = LocalizedOperationObjectInstance(from: transaction.from, to: recipient.eip55String, contract: contract, type: operationType.rawValue, value: String(value), tokenId: "", symbol: token.symbol, name: token.name, decimals: token.decimals)
return .value([result])
} else {
let getContractName = tokensStorage.getContractName(for: contract)
@ -112,7 +112,7 @@ extension TransactionInstance {
when(fulfilled: getContractName, getContractSymbol, getDecimals, getTokenType)
}.then { name, symbol, decimals, tokenType -> Promise<[LocalizedOperationObjectInstance]> in
let operationType = mapTokenTypeToTransferOperationType(tokenType, functionCall: functionCall)
let result = LocalizedOperationObjectInstance(from: transaction.from, to: recipient.eip55String, contract: contract, type: operationType.rawValue, value: String(value), symbol: symbol, name: name, decimals: Int(decimals))
let result = LocalizedOperationObjectInstance(from: transaction.from, to: recipient.eip55String, contract: contract, type: operationType.rawValue, value: String(value), tokenId: "", symbol: symbol, name: name, decimals: Int(decimals))
return .value([result])
}.recover { _ -> Promise<[LocalizedOperationObjectInstance]> in
//NOTE: Return an empty array when failure to fetch contracts data, instead of failing whole TransactionInstance creating

@ -318,6 +318,7 @@ class InCoordinator: NSObject, Coordinator {
transactionsStorages = .init()
for each in config.enabledServers {
let transactionsStorage = createTransactionsStorage(server: each)
//TODO why do we remove such transactions? especially `.failed` and `.unknown`?
transactionsStorage.removeTransactions(for: [.failed, .pending, .unknown])
transactionsStorages[each] = transactionsStorage
}
@ -377,6 +378,15 @@ class InCoordinator: NSObject, Coordinator {
//TODO rename this generic name to reflect that it's for event instances, not for event activity. A few other related ones too
setUpEventSourceCoordinator()
setUpEventSourceCoordinatorForActivities()
setUpErc721TokenIdsFetcher()
}
private func setUpErc721TokenIdsFetcher() {
for each in config.enabledServers {
let tokenStorage = tokensStorages[each]
let transactionStorage = transactionsStorages[each]
tokenStorage.erc721TokenIdsFetcher = transactionStorage
}
}
private func setupEventsStorages() {
@ -423,7 +433,7 @@ class InCoordinator: NSObject, Coordinator {
func showTabBar(animated: Bool) {
navigationController.setViewControllers([accountsCoordinator.accountsViewController], animated: false)
navigationController.pushViewController(tabBarController, animated: animated)
navigationController.setNavigationBarHidden(true, animated: true)
let inCoordinatorViewModel = InCoordinatorViewModel()

@ -8,4 +8,4 @@ import Foundation
struct GetERC721Balance {
let abi = "[{\"constant\":true,\"inputs\":[{\"name\":\"_owner\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"payable\":false,\"stateMutability\":\"view\",\"type\":\"function\"}]"
let name = "balanceOf"
}
}

@ -0,0 +1,13 @@
// Copyright © 2021 Stormbird PTE. LTD.
import Foundation
struct GetERC721TokenUri {
let abi = "[ { \"constant\": true, \"inputs\": [ {\"name\": \"\",\"type\": \"uint256\"}, ], \"name\": \"tokenURI\", \"outputs\": [{\"name\": \"\", \"type\": \"string\"}], \"type\": \"function\" } ]\n"
let name = "tokenURI"
}
struct GetERC721Uri {
let abi = "[ { \"constant\": true, \"inputs\": [ {\"name\": \"\",\"type\": \"uint256\"}, ], \"name\": \"uri\", \"outputs\": [{\"name\": \"\", \"type\": \"string\"}], \"type\": \"function\" } ]\n"
let name = "uri"
}

@ -82,6 +82,10 @@ struct Config {
"\(Keys.lastFetchedAutoDetectedTransactedTokenErc20BlockNumber)-\(wallet.eip55String)"
}
private static func generateLastFetchedErc721InteractionBlockNumberKey(_ wallet: AlphaWallet.Address) -> String {
"\(Keys.lastFetchedAutoDetectedTransactedTokenErc721BlockNumber)-\(wallet.eip55String)"
}
private static func generateLastFetchedAutoDetectedTransactedTokenErc20BlockNumberKey(_ wallet: AlphaWallet.Address) -> String {
"\(Keys.lastFetchedAutoDetectedTransactedTokenErc20BlockNumber)-\(wallet.eip55String)"
}
@ -101,6 +105,17 @@ struct Config {
return dictionary["\(server.chainID)"]?.intValue
}
static func setLastFetchedErc721InteractionBlockNumber(_ blockNumber: Int, server: RPCServer, wallet: AlphaWallet.Address, defaults: UserDefaults = UserDefaults.standard) {
var dictionary: [String: NSNumber] = (defaults.value(forKey: generateLastFetchedErc721InteractionBlockNumberKey(wallet)) as? [String: NSNumber]) ?? .init()
dictionary["\(server.chainID)"] = NSNumber(value: blockNumber)
defaults.set(dictionary, forKey: generateLastFetchedErc721InteractionBlockNumberKey(wallet))
}
static func getLastFetchedErc721InteractionBlockNumber(_ server: RPCServer, wallet: AlphaWallet.Address, defaults: UserDefaults = UserDefaults.standard) -> Int? {
guard let dictionary = defaults.value(forKey: generateLastFetchedErc721InteractionBlockNumberKey(wallet)) as? [String: NSNumber] else { return nil }
return dictionary["\(server.chainID)"]?.intValue
}
static func setLastFetchedAutoDetectedTransactedTokenErc20BlockNumber(_ blockNumber: Int, server: RPCServer, wallet: AlphaWallet.Address, defaults: UserDefaults = UserDefaults.standard) {
var dictionary: [String: NSNumber] = (defaults.value(forKey: generateLastFetchedAutoDetectedTransactedTokenErc20BlockNumberKey(wallet)) as? [String: NSNumber]) ?? .init()
dictionary["\(server.chainID)"] = NSNumber(value: blockNumber)
@ -134,6 +149,7 @@ struct Config {
static let enabledServers = "enabledChains"
static let lastFetchedErc20InteractionBlockNumber = "lastFetchedErc20InteractionBlockNumber"
static let lastFetchedAutoDetectedTransactedTokenErc20BlockNumber = "lastFetchedAutoDetectedTransactedTokenErc20BlockNumber"
static let lastFetchedAutoDetectedTransactedTokenErc721BlockNumber = "lastFetchedAutoDetectedTransactedTokenErc721BlockNumber"
static let lastFetchedAutoDetectedTransactedTokenNonErc20BlockNumber = "lastFetchedAutoDetectedTransactedTokenNonErc20BlockNumber"
static let walletNames = "walletNames"
static let useTaiChiNetwork = "useTaiChiNetworkKey"

@ -76,24 +76,12 @@ public struct Constants {
//UEFA 721 balances function hash
static let balances165Hash721Ticket = "0xc84aae17"
//etherscan-compatible APIs
public static let mainnetEtherscanAPI = "https://api-cn.etherscan.com/api?module=account&action=txlist&address="
public static let ropstenEtherscanAPI = "https://ropsten.etherscan.io/api?module=account&action=txlist&address="
public static let kovanEtherscanAPI = "https://api-kovan.etherscan.io/api?module=account&action=txlist&address="
public static let rinkebyEtherscanAPI = "https://rinkeby.etherscan.io/api?module=account&action=txlist&address="
public static let classicEtherscanAPI = "https://blockscout.com/etc/mainnet/api?module=account&action=txlist&address="
public static let xDaiAPI = "https://blockscout.com/poa/dai/api?module=account&action=txlist&address="
public static let poaNetworkCoreAPI = "https://blockscout.com/poa/core/api?module=account&action=txlist&address="
public static let goerliEtherscanAPI = "https://api-goerli.etherscan.io/api?module=account&action=txlist&address="
public static let artisSigma1NetworkCoreAPI = "https://explorer.sigma1.artis.network/api?module=account&action=txlist&address="
public static let artisTau1NetworkCoreAPI = "https://explorer.tau1.artis.network/api?module=account&action=txlist&address="
//etherscan-compatible erc20 transaction event APIs
//The fetch ERC20 transactions endpoint from Etherscan returns only ERC20 token transactions but the Blockscout version also includes ERC721 transactions too (so it's likely other types that it can detect will be returned too); thus we check the token type rather than assume that they are all ERC20
public static let mainnetEtherscanAPIErc20Events = "https://api-cn.etherscan.com/api?module=account&action=tokentx&address="
public static let ropstenEtherscanAPIErc20Events = "https://ropsten.etherscan.io/api?module=account&action=tokentx&address="
public static let ropstenEtherscanAPIErc20Events = "https://api-ropsten.etherscan.io/api?module=account&action=tokentx&address="
public static let kovanEtherscanAPIErc20Events = "https://api-kovan.etherscan.io/api?module=account&action=tokentx&address="
public static let rinkebyEtherscanAPIErc20Events = "https://rinkeby.etherscan.io/api?module=account&action=tokentx&address="
public static let rinkebyEtherscanAPIErc20Events = "https://api-rinkeby.etherscan.io/api?module=account&action=tokentx&address="
public static let classicAPIErc20Events = "https://blockscout.com/etc/mainnet/api?module=account&action=tokentx&address="
public static let xDaiAPIErc20Events = "https://blockscout.com/poa/dai/api?module=account&action=tokentx&address="
public static let poaNetworkCoreAPIErc20Events = "https://blockscout.com/poa/core/api?module=account&action=tokentx&address="
@ -101,8 +89,9 @@ public struct Constants {
public static let artisSigma1NetworkCoreAPIErc20Events = "https://explorer.sigma1.artis.network/api?module=account&action=tokentx&address="
public static let artisTau1NetworkCoreAPIErc20Events = "https://explorer.tau1.artis.network/api?module=account&action=tokentx&address="
//etherscan-compatible contract details web page
public static let mainnetEtherscanTokenDetailsWebPageURL = "https://cn.etherscan.com/token/"
//etherscan-compatible contract details web page
public static let mainnetEtherscanContractDetailsWebPageURL = "https://cn.etherscan.com/address/"
public static let kovanEtherscanContractDetailsWebPageURL = "https://kovan.etherscan.io/address/"
public static let rinkebyEtherscanContractDetailsWebPageURL = "https://rinkeby.etherscan.io/address/"

@ -30,6 +30,12 @@ enum RPCServer: Hashable, CaseIterable {
case mumbai_testnet
case custom(CustomRPC)
private enum EtherscanCompatibleType {
case etherscan
case blockscout
case unknown
}
//Using this property avoids direct reference to `.main`, which could be a sign of a possible crash i.e. using `.main` when it is disabled by the user
static var forResolvingEns: RPCServer {
.main
@ -101,22 +107,11 @@ enum RPCServer: Hashable, CaseIterable {
}
}
var getEtherscanURL: String? {
var getEtherscanURL: URL? {
switch self {
case .main: return Constants.mainnetEtherscanAPI
case .ropsten: return Constants.ropstenEtherscanAPI
case .rinkeby: return Constants.rinkebyEtherscanAPI
case .kovan: return Constants.kovanEtherscanAPI
case .poa: return Constants.poaNetworkCoreAPI
case .sokol: return nil
case .classic: return Constants.classicEtherscanAPI
case .main, .ropsten, .rinkeby, .kovan, .poa, .classic, .goerli, .xDai, .artis_sigma1, .artis_tau1, .polygon, .binance_smart_chain, .binance_smart_chain_testnet, .sokol:
return etherscanRoot.appendingPathComponent("?module=account&action=txlist&address=")
case .callisto: return nil
case .goerli: return Constants.goerliEtherscanAPI
case .xDai: return Constants.xDaiAPI
case .artis_sigma1: return Constants.artisSigma1NetworkCoreAPI
case .artis_tau1: return Constants.artisTau1NetworkCoreAPI
case .binance_smart_chain: return nil
case .binance_smart_chain_testnet: return nil
case .heco: return nil
case .heco_testnet: return nil
case .custom: return nil
@ -124,12 +119,10 @@ enum RPCServer: Hashable, CaseIterable {
case .fantom_testnet: return nil
case .avalanche: return nil
case .avalanche_testnet: return nil
case .polygon: return nil
case .mumbai_testnet: return nil
}
}
//TODO fix up all the networks
var getEtherscanURLERC20Events: String? {
switch self {
case .main: return Constants.mainnetEtherscanAPIErc20Events
@ -153,11 +146,57 @@ enum RPCServer: Hashable, CaseIterable {
case .fantom_testnet: return nil
case .avalanche: return nil
case .avalanche_testnet: return nil
case .polygon: return nil
case .polygon: return "https://explorer-mainnet.maticvigil.com/api/v2/transactions?module=account&action=tokentx&address="
case .mumbai_testnet: return nil
}
}
var etherscanRoot: URL {
let urlString: String = {
switch self {
case .main: return "https://api-cn.etherscan.com/api"
case .kovan: return "https://api-kovan.etherscan.io/api"
case .ropsten: return "https://api-ropsten.etherscan.io/api"
case .rinkeby: return "https://api-rinkeby.etherscan.io/api"
case .goerli: return "https://api-goerli.etherscan.io/api"
case .classic: return "https://blockscout.com/etc/mainnet/api"
case .callisto: return "https://callisto.trustwalletapp.com/api"
case .poa: return "https://blockscout.com/poa/core/api"
case .xDai: return "https://blockscout.com/poa/dai/api"
case .sokol: return "https://blockscout.com/poa/sokol/api"
case .artis_sigma1: return "https://explorer.sigma1.artis.network/api"
case .artis_tau1: return "https://explorer.tau1.artis.network/api"
case .binance_smart_chain: return "https://api.bscscan.com/api"
case .binance_smart_chain_testnet: return "https://api-testnet.bscscan.com/api"
case .heco_testnet: return "https://api-testnet.hecoinfo.com/api"
case .heco: return "https://api.hecoinfo.com/api"
case .custom: return "" // Enable? make optional
case .fantom: return "https://api.ftmscan.com/api"
//TODO fix etherscan-compatible API endpoint
case .fantom_testnet: return "https://explorer.testnet.fantom.network/tx/api"
//TODO fix etherscan-compatible API endpoint
case .avalanche: return "https://cchain.explorer.avax.network/tx/api"
//TODO fix etherscan-compatible API endpoint
case .avalanche_testnet: return "https://cchain.explorer.avax-test.network/tx/api"
case .polygon: return "https://explorer-mainnet.maticvigil.com/api/v2/"
case .mumbai_testnet: return "https://explorer-mumbai.maticvigil.com/api/v2"
}
}()
return URL(string: urlString)!
}
//If Etherscan, action=tokentx for ERC20 and action=tokennfttx for ERC721. If Blockscout-compatible, action=tokentx includes both ERC20 and ERC721. tokennfttx is not supported.
var getEtherscanURLERC721Events: URL? {
switch erc721TransactionHistoryType {
case .etherscan:
return etherscanRoot.appendingPathComponent("?module=account&action=tokennfttx&address=")
case .blockscout:
return etherscanRoot.appendingPathComponent("/transactions?module=account&action=tokentx&&address=")
case .unknown:
return nil
}
}
var etherscanContractDetailsWebPageURL: String {
switch self {
case .main: return Constants.mainnetEtherscanContractDetailsWebPageURL
@ -196,13 +235,24 @@ enum RPCServer: Hashable, CaseIterable {
}
}
private var erc721TransactionHistoryType: EtherscanCompatibleType {
switch self {
case .main, .ropsten, .rinkeby, .kovan, .goerli, .fantom, .heco, .heco_testnet:
return .etherscan
case .poa, .sokol, .classic, .xDai, .artis_sigma1, .artis_tau1, .binance_smart_chain, .binance_smart_chain_testnet, .polygon, .mumbai_testnet:
return .blockscout
case .callisto, .custom, .fantom_testnet, .avalanche, .avalanche_testnet:
return .unknown
}
}
func etherscanAPIURLForTransactionList(for address: AlphaWallet.Address, startBlock: Int?) -> URL? {
getEtherscanURL.flatMap {
var url = "\($0)\(address.eip55String)&apikey=\(Constants.Credentials.etherscanKey)"
var url = $0.appendingPathComponent("\(address.eip55String)&apikey=\(Constants.Credentials.etherscanKey)")
if let startBlock = startBlock {
url = "\(url)&startBlock=\(startBlock)"
url = url.appendingPathComponent("&startBlock=\(startBlock)")
}
return URL(string: url)
return url
}
}
@ -216,6 +266,16 @@ enum RPCServer: Hashable, CaseIterable {
}
}
func etherscanAPIURLForERC721TxList(for address: AlphaWallet.Address, startBlock: Int?) -> URL? {
getEtherscanURLERC721Events.flatMap {
var url = $0.appendingPathComponent("\(address.eip55String)&apikey=\(Constants.Credentials.etherscanKey)")
if let startBlock = startBlock {
url = $0.appendingPathComponent("\(url)&startBlock=\(startBlock)")
}
return url
}
}
func etherscanContractDetailsWebPageURL(for address: AlphaWallet.Address) -> URL {
return URL(string: etherscanContractDetailsWebPageURL + address.eip55String)!
}
@ -388,28 +448,13 @@ enum RPCServer: Hashable, CaseIterable {
var transactionInfoEndpoints: URL {
let urlString: String = {
switch self {
case .main: return "https://api-cn.etherscan.com"
case .classic: return "https://blockscout.com/etc/mainnet/api"
case .main, .kovan, .ropsten, .rinkeby, .goerli, .classic, .poa, .xDai, .sokol, .artis_sigma1, .artis_tau1, .binance_smart_chain, .binance_smart_chain_testnet, .fantom, .polygon, .heco, .heco_testnet:
return etherscanRoot.absoluteString
case .callisto: return "https://callisto.trustwalletapp.com"
case .kovan: return "https://api-kovan.etherscan.io"
case .ropsten: return "https://api-ropsten.etherscan.io"
case .rinkeby: return "https://api-rinkeby.etherscan.io"
case .poa: return "https://blockscout.com/poa/core/api"
case .xDai: return "https://blockscout.com/poa/dai/api"
case .sokol: return "https://blockscout.com/poa/sokol/api"
case .goerli: return "https://api-goerli.etherscan.io"
case .artis_sigma1: return "https://explorer.sigma1.artis.network/api"
case .artis_tau1: return "https://explorer.tau1.artis.network/api"
case .binance_smart_chain: return "https://bscscan.com/tx/"
case .binance_smart_chain_testnet: return "https://testnet.bscscan.com/tx/"
case .heco_testnet: return "https://scan-testnet.hecochain.com/tx/"
case .heco: return "https://scan.hecochain.com/tx/"
case .custom: return "" // Enable? make optional
case .fantom: return "https://ftmscan.com/tx/"
case .fantom_testnet: return "https://explorer.testnet.fantom.network/tx/"
case .avalanche: return "https://cchain.explorer.avax.network/tx/"
case .avalanche_testnet: return "https://cchain.explorer.avax-test.network/tx/"
case .polygon: return "https://explorer-mainnet.maticvigil.com/tx/"
case .mumbai_testnet: return "https://explorer-mumbai.maticvigil.com/tx/"
}
}()

@ -15,6 +15,7 @@ class GetContractInteractions {
self.queue = queue
}
//TODO rename this since it might include ERC721 (blockscout and compatible like Polygon's). Or can we make this really fetch ERC20, maybe by filtering the results?
func getErc20Interactions(contractAddress: AlphaWallet.Address? = nil, address: AlphaWallet.Address, server: RPCServer, startBlock: Int? = nil, completion: @escaping ([TransactionInstance]) -> Void) {
guard let etherscanURL = server.etherscanAPIURLForERC20TxList(for: address, startBlock: startBlock) else { return }
@ -37,12 +38,94 @@ class GetContractInteractions {
let transactions: [TransactionInstance] = filteredResult.map { result in
let transactionJson = result.1
//Blockscout (and compatible like Polygon's) includes ERC721 transfers
let operationType: OperationType
//TODO check have tokenID + no "value", cos those might be ERC1155?
if let tokenId = transactionJson["tokenID"].string, !tokenId.isEmpty {
operationType = .erc721TokenTransfer
} else {
operationType = .erc20TokenTransfer
}
let localizedTokenObj = LocalizedOperationObjectInstance(
from: transactionJson["from"].stringValue,
to: transactionJson["to"].stringValue,
contract: AlphaWallet.Address(uncheckedAgainstNullAddress: transactionJson["contractAddress"].stringValue),
type: operationType.rawValue,
value: transactionJson["value"].stringValue,
tokenId: transactionJson["tokenID"].stringValue,
symbol: transactionJson["tokenSymbol"].stringValue,
name: transactionJson["tokenName"].stringValue,
decimals: transactionJson["tokenDecimal"].intValue
)
return TransactionInstance(
id: transactionJson["hash"].stringValue,
server: server,
blockNumber: transactionJson["blockNumber"].intValue,
transactionIndex: transactionJson["transactionIndex"].intValue,
from: transactionJson["from"].stringValue,
to: transactionJson["to"].stringValue,
//Must not set the value of the ERC20 token transferred as the native crypto value transferred
value: "0",
gas: transactionJson["gas"].stringValue,
gasPrice: transactionJson["gasPrice"].stringValue,
gasUsed: transactionJson["gasUsed"].stringValue,
nonce: transactionJson["nonce"].stringValue,
date: Date(timeIntervalSince1970: transactionJson["timeStamp"].doubleValue),
localizedOperations: [localizedTokenObj],
//The API only returns successful transactions
state: .completed,
isErc20Interaction: true
)
}
completion(transactions)
case .failure:
completion([])
}
})
}
//TODO Almost a duplicate of the the ERC20 version. De-dup maybe?
func getErc721Interactions(contractAddress: AlphaWallet.Address? = nil, address: AlphaWallet.Address, server: RPCServer, startBlock: Int? = nil, completion: @escaping ([TransactionInstance]) -> Void) {
guard let etherscanURL = server.etherscanAPIURLForERC721TxList(for: address, startBlock: startBlock) else { return }
Alamofire.request(etherscanURL).validate().responseJSON(queue: queue, options: [], completionHandler: { response in
switch response.result {
case .success(let value):
//Performance: process in background so UI don't have a chance of blocking if there's a long list of contracts
let json = JSON(value)
let filteredResult: [(String, JSON)]
if let contractAddress = contractAddress {
//filter based on what contract you are after
filteredResult = json["result"].filter {
$0.1["contractAddress"].stringValue == contractAddress.eip55String.lowercased()
}
} else {
filteredResult = json["result"].filter {
$0.1["to"].stringValue.hasPrefix("0x")
}
}
let transactions: [TransactionInstance] = filteredResult.map { result in
let transactionJson = result.1
//Blockscout (and compatible like Polygon's) includes ERC721 transfers
let operationType: OperationType
//TODO check have tokenID + no "value", cos those might be ERC1155?
if let tokenId = transactionJson["tokenID"].string, !tokenId.isEmpty {
operationType = .erc721TokenTransfer
} else {
operationType = .erc20TokenTransfer
}
let localizedTokenObj = LocalizedOperationObjectInstance(
from: transactionJson["from"].stringValue,
to: transactionJson["to"].stringValue,
contract: AlphaWallet.Address(uncheckedAgainstNullAddress: transactionJson["contractAddress"].stringValue),
type: OperationType.erc20TokenTransfer.rawValue,
type: operationType.rawValue,
value: transactionJson["value"].stringValue,
tokenId: transactionJson["tokenID"].stringValue,
symbol: transactionJson["tokenSymbol"].stringValue,
name: transactionJson["tokenName"].stringValue,
decimals: transactionJson["tokenDecimal"].intValue

@ -5,6 +5,7 @@
import Foundation
import BigInt
import PromiseKit
import Result
class GetERC721BalanceCoordinator {
@ -17,7 +18,7 @@ class GetERC721BalanceCoordinator {
func getERC721TokenBalance(
for address: AlphaWallet.Address,
contract: AlphaWallet.Address,
completion: @escaping (Result<BigUInt, AnyError>) -> Void
completion: @escaping (ResultResult<BigUInt, AnyError>.t) -> Void
) {
let function = GetERC721Balance()
callSmartContract(withServer: server, contract: contract, functionName: function.name, abiString: function.abi, parameters: [address.eip55String] as [AnyObject], timeout: TokensDataStore.fetchContractDataTimeout).done { balanceResult in
@ -35,4 +36,4 @@ class GetERC721BalanceCoordinator {
return BigUInt(0)
}
}
}
}

@ -24,19 +24,19 @@ class TokenAdaptor {
public func getTokenHolders(forWallet account: Wallet, sourceFromEvents: Bool = true) -> [TokenHolder] {
switch token.type {
case .nativeCryptocurrency, .erc20, .erc875, .erc721ForTickets:
return getNotSupportedByOpenSeaTokenHolders(forWallet: account)
return getNotSupportedByNonFungibleJsonTokenHolders(forWallet: account)
case .erc721:
let tokenType = OpenSeaSupportedNonFungibleTokenHandling(token: token)
let tokenType = NonFungibleFromJsonSupportedTokenHandling(token: token)
switch tokenType {
case .supportedByOpenSea:
return getSupportedByOpenSeaTokenHolders(forWallet: account, sourceFromEvents: sourceFromEvents)
case .notSupportedByOpenSea:
return getNotSupportedByOpenSeaTokenHolders(forWallet: account)
case .supported:
return getSupportedByNonFungibleJsonTokenHolders(forWallet: account, sourceFromEvents: sourceFromEvents)
case .notSupported:
return getNotSupportedByNonFungibleJsonTokenHolders(forWallet: account)
}
}
}
private func getNotSupportedByOpenSeaTokenHolders(forWallet account: Wallet) -> [TokenHolder] {
private func getNotSupportedByNonFungibleJsonTokenHolders(forWallet account: Wallet) -> [TokenHolder] {
let balance = token.balance
var tokens = [Token]()
switch token.type {
@ -66,12 +66,12 @@ class TokenAdaptor {
}
}
private func getSupportedByOpenSeaTokenHolders(forWallet account: Wallet, sourceFromEvents: Bool) -> [TokenHolder] {
private func getSupportedByNonFungibleJsonTokenHolders(forWallet account: Wallet, sourceFromEvents: Bool) -> [TokenHolder] {
let balance = token.balance
var tokens = [Token]()
for item in balance {
let jsonString = item.balance
if let token = getTokenForOpenSeaNonFungible(forJSONString: jsonString, inWallet: account, server: self.token.server, sourceFromEvents: sourceFromEvents) {
if let token = getTokenForNonFungible(forJSONString: jsonString, inWallet: account, server: self.token.server, sourceFromEvents: sourceFromEvents) {
tokens.append(token)
}
}
@ -157,8 +157,9 @@ 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 getTokenForOpenSeaNonFungible(forJSONString jsonString: String, inWallet account: Wallet, server: RPCServer, sourceFromEvents: Bool) -> Token? {
guard let data = jsonString.data(using: .utf8), let nonFungible = try? JSONDecoder().decode(OpenSeaNonFungible.self, from: data) else { return nil }
private func getTokenForNonFungible(forJSONString jsonString: String, inWallet account: Wallet, server: RPCServer, sourceFromEvents: Bool) -> Token? {
guard let data = jsonString.data(using: .utf8), let nonFungible = nonFungible(fromJsonData: data) else { return nil }
let xmlHandler = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore)
let event: EventInstance?
if sourceFromEvents, let attributeWithEventSource = xmlHandler.attributesWithEventSource.first, let eventFilter = attributeWithEventSource.eventOrigin?.eventFilter, let eventName = attributeWithEventSource.eventOrigin?.eventName, let eventContract = attributeWithEventSource.eventOrigin?.contract {

@ -0,0 +1,48 @@
// Copyright © 2021 Stormbird PTE. LTD.
import Foundation
import PromiseKit
class Erc721Contract {
private let server: RPCServer
init(server: RPCServer) {
self.server = server
}
func getErc721TokenUri(for tokenId: String, contract: AlphaWallet.Address) -> Promise<URL> {
firstly {
getErc721TokenUriImpl(for: tokenId, contract: contract)
}.recover { _ in
self.getErc721Uri(for: tokenId, contract: contract)
}
}
private func getErc721TokenUriImpl(for tokenId: String, contract: AlphaWallet.Address) -> Promise<URL> {
let function = GetERC721TokenUri()
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) ?? ""
if let url = URL(string: string) {
return url
} else {
throw Web3Error(description: "Error extracting tokenUri for contract \(contract.eip55String) tokenId: \(tokenId)")
}
}
}
private func getErc721Uri(for tokenId: String, contract: AlphaWallet.Address) -> Promise<URL> {
let function = GetERC721Uri()
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) ?? ""
if let url = URL(string: string) {
return url
} else {
throw Web3Error(description: "Error extracting tokenUri uri for contract \(contract.eip55String) tokenId: \(tokenId)")
}
}
}
}

@ -0,0 +1,29 @@
// Copyright © 2021 Stormbird PTE. LTD.
import Foundation
//Shape of this originally created to match OpenSea's API output
protocol NonFungibleFromJson: Codable {
var tokenId: String { get }
var contractName: String { get }
var symbol: String { get }
var name: String { get }
var description: String { get }
var thumbnailUrl: String { get }
var imageUrl: String { get }
var contractImageUrl: String { get }
var externalLink: String { get }
var backgroundColor: String? { get }
var traits: [OpenSeaNonFungibleTrait] { get }
var generationTrait: OpenSeaNonFungibleTrait? { get }
}
func nonFungible(fromJsonData jsonData: Data) -> NonFungibleFromJson? {
if let nonFungible = try? JSONDecoder().decode(OpenSeaNonFungible.self, from: jsonData) {
return nonFungible
}
if let nonFungible = try? JSONDecoder().decode(NonFungibleFromTokenUri.self, from: jsonData) {
return nonFungible
}
return nil
}

@ -0,0 +1,19 @@
// Copyright © 2021 Stormbird PTE. LTD.
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 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] = .init()
let generationTrait: OpenSeaNonFungibleTrait? = nil
}

@ -3,7 +3,7 @@
import Foundation
//Some fields are duplicated across token IDs within the same contract like the contractName, symbol, contractImageUrl, etc. The space savings in the database aren't work the normalization
struct OpenSeaNonFungible: Codable {
struct OpenSeaNonFungible: Codable, NonFungibleFromJson {
//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"
@ -32,4 +32,4 @@ struct OpenSeaNonFungibleTrait: Codable {
struct OpenSeaError: Error {
var localizedDescription: String
}
}

@ -32,20 +32,20 @@ enum OpenSeaBackedNonFungibleTokenHandling {
}
}
///Use this enum to "mark" where we handle non-fungible tokens supported by OpenSea differently instead of accessing the contract directly
///Even if there is a TokenScript file available for the contract, it is assumed to still be supported by OpenSea
///Use this enum to "mark" where we handle non-fungible tokens supported by JSON (either via OpenSea or with ERC721's `tokenURI`)
///Even if there is a TokenScript file available for the contract, it is assumed to still be supported by this way
///If there are other special casing for tokens that doesn't fit this model, create another enum type (not case)
enum OpenSeaSupportedNonFungibleTokenHandling {
case supportedByOpenSea
case notSupportedByOpenSea
enum NonFungibleFromJsonSupportedTokenHandling {
case supported
case notSupported
init(token: TokenObject) {
self = {
if !token.balance.isEmpty && token.balance[0].balance.hasPrefix("{") {
return .supportedByOpenSea
return .supported
} else {
return .notSupportedByOpenSea
return .notSupported
}
}()
}
}
}

@ -21,6 +21,8 @@ protocol TokensDataStorePriceDelegate: class {
// swiftlint:disable type_body_length
class TokensDataStore {
typealias ContractAndJson = (contract: AlphaWallet.Address, json: String)
static let fetchContractDataTimeout = TimeInterval(4)
private lazy var getNameCoordinator: GetNameCoordinator = {
@ -89,6 +91,7 @@ class TokensDataStore {
let server: RPCServer
weak var delegate: TokensDataStoreDelegate?
weak var priceDelegate: TokensDataStorePriceDelegate?
weak var erc721TokenIdsFetcher: Erc721TokenIdsFetcher?
//TODO why is this a dictionary? There seems to be only at most 1 key-value pair in the dictionary
var tickers: [AddressAndRPCServer: CoinTicker] = .init() {
didSet {
@ -359,6 +362,7 @@ class TokensDataStore {
}
}
//TODO should callers call tokenURI and so on, instead?
func getERC721Balance(for address: AlphaWallet.Address, completion: @escaping (ResultResult<[String], AnyError>.t) -> Void) {
withRetry(times: numberOfTimesToRetryFetchContractData) { [weak self] triggerRetry in
guard let strongSelf = self else { return }
@ -536,7 +540,6 @@ class TokensDataStore {
private func refreshBalanceForERC721Tokens(tokens: [TokenObject]) {
assert(!tokens.contains { !$0.isERC721AndNotForTickets })
guard OpenSea.isServerSupported(server) else { return }
firstly {
getTokensFromOpenSea()
}.done { [weak self] contractToOpenSeaNonFungibles in
@ -548,26 +551,96 @@ class TokensDataStore {
strongSelf.updateDelegate()
}.cauterize()
}
func foo1() {
firstly {
Alamofire.request("https://httpbin.org/get", method: .get).responseJSON()
//}.then { json, rsp in
// //
}.catch{ error in
//
}
}
private func updateNonOpenSeaNonFungiblesBalance(erc721ContractsNotFoundInOpenSea: [AlphaWallet.Address], tokens: [TokenObject]) {
var count = 0
for address in erc721ContractsNotFoundInOpenSea {
getERC721Balance(for: address) { [weak self] result in
guard let strongSelf = self else { return }
defer {
count += 1
if count == erc721ContractsNotFoundInOpenSea.count {
strongSelf.updateDelegate()
}
private func updateNonOpenSeaNonFungiblesBalance(erc721ContractsNotFoundInOpenSea contracts: [AlphaWallet.Address], tokens: [TokenObject]) {
let promises = contracts.map { updateNonOpenSeaNonFungiblesBalance(contract: $0, tokens: tokens) }
firstly {
when(resolved: promises)
}.done { _ in
self.updateDelegate()
}
}
private func updateNonOpenSeaNonFungiblesBalance(contract: AlphaWallet.Address, tokens: [TokenObject]) -> Promise<Void> {
guard let erc721TokenIdsFetcher = erc721TokenIdsFetcher else { return Promise { seal in } }
return firstly {
erc721TokenIdsFetcher.tokenIdsForErc721Token(contract: contract, inAccount: account.address)
}.then { tokenIds -> Promise<[ContractAndJson]> in
let guarantees: [Guarantee<ContractAndJson>] = tokenIds.map { self.fetchNonFungibleJson(forTokenId: $0, address: contract, tokens: tokens) }
return when(fulfilled: guarantees)
}.done { listOfContractAndJsonResult in
var contractsAndJsons: [AlphaWallet.Address: [String]] = .init()
for each in listOfContractAndJsonResult {
if var listOfJson = contractsAndJsons[each.contract] {
listOfJson.append(each.json)
contractsAndJsons[each.contract] = listOfJson
} else {
contractsAndJsons[each.contract] = [each.json]
}
switch result {
case .success(let balance):
if let token = tokens.first(where: { $0.contractAddress.sameContract(as: address) }) {
strongSelf.update(token: token, action: .nonFungibleBalance(balance))
}
for (contract, jsons) in contractsAndJsons {
guard let tokenObject = tokens.first(where: { $0.contractAddress.sameContract(as: contract) }) else { continue }
self.update(token: tokenObject, action: .nonFungibleBalance(jsons))
}
}.asVoid()
}
private func fetchNonFungibleJson(forTokenId tokenId: String, address: AlphaWallet.Address, tokens: [TokenObject]) -> Guarantee<ContractAndJson> {
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"] = ""
}
return .value((contract: address, json: jsonDictionary.rawString()!))
}
}
private func fetchTokenJson(forTokenId tokenId: String, uri: URL, address: AlphaWallet.Address, tokens: [TokenObject]) -> Promise<ContractAndJson> {
struct Error: Swift.Error {
}
return firstly {
Alamofire.request(uri, method: .get).responseData()
}.map { data, rsp 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) }) {
jsonDictionary["tokenId"] = JSON(tokenId)
jsonDictionary["contractName"] = JSON(tokenObject.name)
jsonDictionary["symbol"] = JSON(tokenObject.symbol)
jsonDictionary["name"] = jsonDictionary["name"]
jsonDictionary["imageUrl"] = jsonDictionary["image"]
jsonDictionary["thumbnailUrl"] = jsonDictionary["image"]
}
if let jsonString = jsonDictionary.rawString() {
return (contract: address, json: jsonString)
} else {
throw Error()
}
case .failure:
break
}
} else {
throw Error()
}
}
}

@ -65,11 +65,11 @@ class TokenInstanceActionViewController: UIViewController, TokenVerifiableStatus
}
var canPeekToken: Bool {
let tokenType = OpenSeaSupportedNonFungibleTokenHandling(token: tokenObject)
let tokenType = NonFungibleFromJsonSupportedTokenHandling(token: tokenObject)
switch tokenType {
case .supportedByOpenSea:
case .supported:
return true
case .notSupportedByOpenSea:
case .notSupported:
return false
}
}

@ -43,11 +43,11 @@ class TokenInstanceViewController: UIViewController, TokenVerifiableStatusViewCo
}
var canPeekToken: Bool {
let tokenType = OpenSeaSupportedNonFungibleTokenHandling(token: tokenObject)
let tokenType = NonFungibleFromJsonSupportedTokenHandling(token: tokenObject)
switch tokenType {
case .supportedByOpenSea:
case .supported:
return true
case .notSupportedByOpenSea:
case .notSupported:
return false
}
}

@ -68,11 +68,11 @@ class TokensCardViewController: UIViewController, TokenVerifiableStatusViewContr
}
var canPeekToken: Bool {
let tokenType = OpenSeaSupportedNonFungibleTokenHandling(token: tokenObject)
let tokenType = NonFungibleFromJsonSupportedTokenHandling(token: tokenObject)
switch tokenType {
case .supportedByOpenSea:
case .supported:
return true
case .notSupportedByOpenSea:
case .notSupported:
return false
}
}

@ -398,7 +398,6 @@ extension TokensViewController: StatefulViewController {
extension TokensViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let token = viewModel.item(for: indexPath.row, section: indexPath.section)
delegate?.didSelect(token: token, in: self)
}

@ -63,7 +63,6 @@ class NonFungibleTokenViewCell: UITableViewCell {
$0.alpha = viewModel.alpha
}
tokenIconImageView.subscribable = viewModel.iconImage
blockChainTagLabel.configure(viewModel: viewModel.blockChainTagViewModel)
}
}

@ -25,6 +25,7 @@ class SingleChainTransactionEtherscanDataCoordinator: SingleChainTransactionData
private let alphaWalletProvider = AlphaWalletProviderFactory.makeProvider()
private var isAutoDetectingERC20Transactions: Bool = false
private var isAutoDetectingErc721Transactions: Bool = false
private var isFetchingLatestTransactions = false
var coordinators: [Coordinator] = []
weak var delegate: SingleChainTransactionDataCoordinatorDelegate?
@ -50,6 +51,7 @@ class SingleChainTransactionEtherscanDataCoordinator: SingleChainTransactionData
if transactionsTracker.fetchingState != .done {
fetchOlderTransactions(for: session.account.address)
autoDetectERC20Transactions()
autoDetectErc721Transactions()
}
}
@ -79,11 +81,13 @@ class SingleChainTransactionEtherscanDataCoordinator: SingleChainTransactionData
strongSelf.queue.async {
strongSelf.fetchLatestTransactions()
strongSelf.autoDetectERC20Transactions()
strongSelf.autoDetectErc721Transactions()
}
}, selector: #selector(Operation.main), userInfo: nil, repeats: true)
}
//TODO should this be added to the queue?
//TODO when blockscout-compatible, this includes ERC721 too. Maybe rename?
private func autoDetectERC20Transactions() {
guard !isAutoDetectingERC20Transactions else { return }
isAutoDetectingERC20Transactions = true
@ -98,7 +102,7 @@ class SingleChainTransactionEtherscanDataCoordinator: SingleChainTransactionData
let blockNumbers = result.map(\.blockNumber)
if let minBlockNumber = blockNumbers.min(), let maxBlockNumber = blockNumbers.max() {
firstly {
strongSelf.backFillErc20TransactionGroup(result, startBlock: minBlockNumber, endBlock: maxBlockNumber)
strongSelf.backFillTransactionGroup(result, startBlock: minBlockNumber, endBlock: maxBlockNumber)
}.done(on: strongSelf.queue) { backFilledTransactions in
Config.setLastFetchedErc20InteractionBlockNumber(maxBlockNumber, server: server, wallet: wallet)
@ -114,7 +118,37 @@ class SingleChainTransactionEtherscanDataCoordinator: SingleChainTransactionData
}
}
private func backFillErc20TransactionGroup(_ transactionsToFill: [TransactionInstance], startBlock: Int, endBlock: Int) -> Promise<[TransactionInstance]> {
private func autoDetectErc721Transactions() {
guard !isAutoDetectingErc721Transactions else { return }
isAutoDetectingErc721Transactions = true
let server = session.server
let wallet = session.account.address
let startBlock = Config.getLastFetchedErc721InteractionBlockNumber(session.server, wallet: wallet).flatMap { $0 + 1 }
GetContractInteractions(queue: self.queue).getErc721Interactions(address: wallet, server: server, startBlock: startBlock) { [weak self] result in
guard let strongSelf = self else { return }
let blockNumbers = result.map(\.blockNumber)
if let minBlockNumber = blockNumbers.min(), let maxBlockNumber = blockNumbers.max() {
firstly {
strongSelf.backFillTransactionGroup(result, startBlock: minBlockNumber, endBlock: maxBlockNumber)
}.done(on: strongSelf.queue) { backFilledTransactions in
Config.setLastFetchedErc721InteractionBlockNumber(maxBlockNumber, server: server, wallet: wallet)
strongSelf.update(items: backFilledTransactions)
}.cauterize()
.finally {
strongSelf.isAutoDetectingErc721Transactions = false
}
} else {
strongSelf.isAutoDetectingErc721Transactions = false
strongSelf.update(items: result)
}
}
}
private func backFillTransactionGroup(_ transactionsToFill: [TransactionInstance], startBlock: Int, endBlock: Int) -> Promise<[TransactionInstance]> {
return firstly {
fetchTransactions(for: session.account.address, startBlock: startBlock, endBlock: endBlock, sortOrder: .asc)
}.map(on: self.queue) { fillerTransactions -> [TransactionInstance] in
@ -164,7 +198,7 @@ class SingleChainTransactionEtherscanDataCoordinator: SingleChainTransactionData
}
private func filterTransactionsToPullContractsFrom(_ transactions: [TransactionInstance]) -> Promise<(transactions: [TransactionInstance], contractTypes: [AlphaWallet.Address: TokenType])> {
return Promise { seal in
Promise { seal in
let contractsToAvoid = self.contractsToAvoid
let filteredTransactions = transactions.filter {
if let toAddressToCheck = AlphaWallet.Address(string: $0.to), contractsToAvoid.contains(toAddressToCheck) {
@ -177,21 +211,16 @@ class SingleChainTransactionEtherscanDataCoordinator: SingleChainTransactionData
}
//The fetch ERC20 transactions endpoint from Etherscan returns only ERC20 token transactions but the Blockscout version also includes ERC721 transactions too (so it's likely other types that it can detect will be returned too); thus we check the token type rather than assume that they are all ERC20
switch self.session.server {
case .xDai, .poa:
let contracts = Array(Set(filteredTransactions.compactMap { $0.localizedOperations.first?.contractAddress }))
let tokenTypePromises = contracts.map { self.tokensStorage.getTokenType(for: $0) }
when(fulfilled: tokenTypePromises).map { tokenTypes in
let contractsToTokenTypes = Dictionary(uniqueKeysWithValues: zip(contracts, tokenTypes))
return (transactions: filteredTransactions, contractTypes: contractsToTokenTypes)
}.done { val in
seal.fulfill(val)
}.catch { error in
seal.reject(error)
}
case .main, .classic, .kovan, .ropsten, .rinkeby, .sokol, .callisto, .goerli, .artis_sigma1, .artis_tau1, .binance_smart_chain, .binance_smart_chain_testnet, .custom, .heco, .heco_testnet, .fantom, .fantom_testnet, .avalanche, .avalanche_testnet, .polygon, .mumbai_testnet:
seal.fulfill((transactions: filteredTransactions, contractTypes: .init()))
let contracts = Array(Set(filteredTransactions.compactMap { $0.localizedOperations.first?.contractAddress }))
let tokenTypePromises = contracts.map { self.tokensStorage.getTokenType(for: $0) }
when(fulfilled: tokenTypePromises).map { tokenTypes in
let contractsToTokenTypes = Dictionary(uniqueKeysWithValues: zip(contracts, tokenTypes))
return (transactions: filteredTransactions, contractTypes: contractsToTokenTypes)
}.done { val in
seal.fulfill(val)
}.catch { error in
seal.reject(error)
}
}
}

@ -139,9 +139,9 @@ extension Transaction {
if let functionCallMetaData = DecodedFunctionCall(data: data), let contract = contractOrRecipient, let token = tokensDataStore.tokenThreadSafe(forContract: contract) {
switch functionCallMetaData.type {
case .erc20Approve(let spender, let value):
return (operations: [LocalizedOperationObject(from: from.eip55String, to: spender.eip55String, contract: contract, type: OperationType.erc20TokenApprove.rawValue, value: String(value), symbol: token.symbol, name: token.name, decimals: token.decimals)], isErc20Interaction: true)
return (operations: [LocalizedOperationObject(from: from.eip55String, to: spender.eip55String, contract: contract, type: OperationType.erc20TokenApprove.rawValue, value: String(value), tokenId: "", symbol: token.symbol, name: token.name, decimals: token.decimals)], isErc20Interaction: true)
case .erc20Transfer(let recipient, let value):
return (operations: [LocalizedOperationObject(from: from.eip55String, to: recipient.eip55String, contract: contract, type: OperationType.erc20TokenTransfer.rawValue, value: String(value), symbol: token.symbol, name: token.name, decimals: token.decimals)], isErc20Interaction: true)
return (operations: [LocalizedOperationObject(from: from.eip55String, to: recipient.eip55String, contract: contract, type: OperationType.erc20TokenTransfer.rawValue, value: String(value), tokenId: "", symbol: token.symbol, name: token.name, decimals: token.decimals)], isErc20Interaction: true)
case .nativeCryptoTransfer, .others:
break
}

@ -200,13 +200,32 @@ class TransactionsStorage {
guard let contract = operation.contractAddress else { return nil }
guard let name = operation.name else { return nil }
guard let symbol = operation.symbol else { return nil }
let tokenType: TokenType
if let t = contractsAndTokenTypes[contract] {
tokenType = t
} else {
switch operation.operationType {
case .nativeCurrencyTokenTransfer:
tokenType = .nativeCryptocurrency
case .erc20TokenTransfer:
tokenType = .erc20
case .erc20TokenApprove:
tokenType = .erc20
case .erc721TokenTransfer:
tokenType = .erc721
case .erc875TokenTransfer:
tokenType = .erc875
case .unknown:
tokenType = .erc20
}
}
return TokenUpdate(
address: contract,
server: server,
name: name,
symbol: symbol,
decimals: operation.decimals,
tokenType: contractsAndTokenTypes[contract] ?? .erc20
tokenType: tokenType
)
}
return tokenUpdates
@ -245,3 +264,28 @@ class TransactionsStorage {
}
}
}
extension TransactionsStorage: Erc721TokenIdsFetcher {
func tokenIdsForErc721Token(contract: AlphaWallet.Address, inAccount account: AlphaWallet.Address) -> Promise<[String]> {
Promise { seal in
//Important to sort ascending to figure out ownership from transfers in and out
let transactions = objects
.filter("isERC20Interaction == true")
.sorted(byKeyPath: "date", ascending: true)
let operations: [LocalizedOperationObject] = transactions.flatMap { $0.localizedOperations.filter { $0.contractAddress?.sameContract(as: contract) ?? false } }
var tokenIds: Set<String> = .init()
for each in operations {
let tokenId = each.tokenId
guard !tokenId.isEmpty else { continue }
if account.sameContract(as: each.from) {
tokenIds.remove(tokenId)
} else if account.sameContract(as: each.to) {
tokenIds.insert(tokenId)
} else {
//no-op
}
}
seal.fulfill(Array(tokenIds))
}
}
}

@ -0,0 +1,8 @@
// Copyright © 2021 Stormbird PTE. LTD.
import Foundation
import PromiseKit
protocol Erc721TokenIdsFetcher: class {
func tokenIdsForErc721Token(contract: AlphaWallet.Address, inAccount account: AlphaWallet.Address) -> Promise<[String]>
}

@ -10,6 +10,7 @@ class LocalizedOperationObject: Object {
@objc dynamic var contract: String? = .none
@objc dynamic var type: String = ""
@objc dynamic var value: String = ""
@objc dynamic var tokenId: String = ""
@objc dynamic var name: String? = .none
@objc dynamic var symbol: String? = .none
@objc dynamic var decimals: Int = 18
@ -20,6 +21,7 @@ class LocalizedOperationObject: Object {
contract: AlphaWallet.Address?,
type: String,
value: String,
tokenId: String,
symbol: String?,
name: String?,
decimals: Int
@ -30,6 +32,7 @@ class LocalizedOperationObject: Object {
self.contract = contract?.eip55String
self.type = type
self.value = value
self.tokenId = tokenId
self.symbol = symbol
self.name = name
self.decimals = decimals
@ -42,6 +45,7 @@ class LocalizedOperationObject: Object {
self.contract = object.contract
self.type = object.type
self.value = object.value
self.tokenId = object.tokenId
self.symbol = object.symbol
self.name = object.name
self.decimals = object.decimals
@ -63,6 +67,7 @@ struct LocalizedOperationObjectInstance {
var contract: String? = .none
var type: String = ""
var value: String = ""
var tokenId: String = ""
var name: String? = .none
var symbol: String? = .none
var decimals: Int = 18
@ -73,6 +78,7 @@ struct LocalizedOperationObjectInstance {
self.contract = object.contract
self.type = object.type
self.value = object.value
self.tokenId = object.tokenId
self.symbol = object.symbol
self.name = object.name
self.decimals = object.decimals
@ -84,6 +90,7 @@ struct LocalizedOperationObjectInstance {
contract: AlphaWallet.Address?,
type: String,
value: String,
tokenId: String,
symbol: String?,
name: String?,
decimals: Int
@ -93,6 +100,7 @@ struct LocalizedOperationObjectInstance {
self.contract = contract?.eip55String
self.type = type
self.value = value
self.tokenId = tokenId
self.symbol = symbol
self.name = name
self.decimals = decimals

@ -97,13 +97,15 @@ private class TokenImageFetcher {
Promise { seal in
switch type {
case .erc721:
if let json = balance, let data = json.data(using: .utf8), let openSeaNonFungible = try? JSONDecoder().decode(OpenSeaNonFungible.self, from: data), !openSeaNonFungible.contractImageUrl.isEmpty {
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).done { image in
seal.fulfill(image)
}.catch { _ in
seal.reject(ImageAvailabilityError.notAvailable)
}
} else {
seal.reject(ImageAvailabilityError.notAvailable)
}
case .nativeCryptocurrency, .erc20, .erc875, .erc721ForTickets:
seal.reject(ImageAvailabilityError.notAvailable)

Loading…
Cancel
Save