From b6d03e69695ede4b6e0e68d2ca051c61bec27226 Mon Sep 17 00:00:00 2001 From: James Brown Date: Mon, 31 Jan 2022 13:47:48 +1100 Subject: [PATCH 1/2] temp --- .../alphawallet/app/entity/tokens/ERC1155Token.java | 6 ++++-- .../alphawallet/app/repository/TokenRepository.java | 10 ++++++++++ .../com/alphawallet/app/service/TokensService.java | 10 ++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/ERC1155Token.java b/app/src/main/java/com/alphawallet/app/entity/tokens/ERC1155Token.java index 7c9ab8c36e..037e45e9c0 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/ERC1155Token.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/ERC1155Token.java @@ -592,6 +592,10 @@ private EthFilter getSendBalanceFilter(Event event, DefaultBlockParameter startB @Override public BigDecimal updateBalance(Realm realm) { + if (getAddress().equalsIgnoreCase("0x85F0e02cb992aa1F9F47112F815F519EF1A59E2D")) + { + System.out.println("YOLESS"); + } try { BigInteger lastEventBlockRead = getLastBlockRead(realm); @@ -637,8 +641,6 @@ private Pair, BigInteger> processBatchTransferEvents(Web3j w HashSet tokenIds = new HashSet<>(); BigInteger lastEventBlockRead = Numeric.toBigInt(startBlock.getValue()); - - for (EthLog.LogResult ethLog : receiveLogs.getLogs()) { String block = ((Log) ethLog.get()).getBlockNumberRaw(); diff --git a/app/src/main/java/com/alphawallet/app/repository/TokenRepository.java b/app/src/main/java/com/alphawallet/app/repository/TokenRepository.java index e277cc92a2..3993de48d3 100644 --- a/app/src/main/java/com/alphawallet/app/repository/TokenRepository.java +++ b/app/src/main/java/com/alphawallet/app/repository/TokenRepository.java @@ -386,6 +386,11 @@ public String getTokenImageUrl(long networkId, String address) private Single updateBalance(final Wallet wallet, final Token token) { + if (token.getAddress().equalsIgnoreCase("0x85F0e02cb992aa1F9F47112F815F519EF1A59E2D")) + { + System.out.println("YOLESS"); + } + return Single.fromCallable(() -> { BigDecimal balance = BigDecimal.valueOf(-1); try @@ -393,6 +398,11 @@ private Single updateBalance(final Wallet wallet, final Token token) List balanceArray = null; Token thisToken = token; + if (thisToken.getAddress().equalsIgnoreCase("0x85F0e02cb992aa1F9F47112F815F519EF1A59E2D")) + { + System.out.println("YOLESS"); + } + switch (token.getInterfaceSpec()) { case ETHEREUM: diff --git a/app/src/main/java/com/alphawallet/app/service/TokensService.java b/app/src/main/java/com/alphawallet/app/service/TokensService.java index 1d7c96e7af..ab725d7350 100644 --- a/app/src/main/java/com/alphawallet/app/service/TokensService.java +++ b/app/src/main/java/com/alphawallet/app/service/TokensService.java @@ -846,6 +846,11 @@ public Token getNextInBalanceUpdateQueue() float weighting = check.calculateBalanceUpdateWeight(); + if (check.getAddress().equalsIgnoreCase("0x85F0e02cb992aa1F9F47112F815F519EF1A59E2D")) + { + System.out.println("YOLESS"); + } + if ((!check.isEnabled || check.isNFT()) && !isSynced()) continue; //don't start looking at NFT balances until we sync the chain/ERC20 tokens if (!isSynced() && check.lastUpdate > syncStart) continue; //don't start updating already updated tokens until all ERC20 are checked if (!appHasFocus && (!check.isEthereum() && !isFocusToken(check))) continue; //only check chains when wallet out of focus @@ -889,6 +894,11 @@ else if (focusToken != null) if (highestToken != null) { + if (highestToken.getAddress().equalsIgnoreCase("0x85F0e02cb992aa1F9F47112F815F519EF1A59E2D")) + { + System.out.println("YOLESS"); + } + pendingTokenMap.put(databaseKey(highestToken.getChain(), highestToken.getAddress()), System.currentTimeMillis()); return getToken(highestToken.getChain(), highestToken.getAddress()); } From dfc2c4f0720a69f74c77a44c3187fadce78feca7 Mon Sep 17 00:00:00 2001 From: James Brown Date: Tue, 1 Feb 2022 12:02:26 +1100 Subject: [PATCH 2/2] Update to check ERC721 balance with restricted logs --- .../alphawallet/app/entity/NetworkInfo.java | 2 +- .../app/entity/tokens/ERC1155Token.java | 4 - .../app/entity/tokens/ERC721Token.java | 223 +++++++++++++++++- .../alphawallet/app/entity/tokens/Token.java | 12 + .../app/repository/EthereumNetworkBase.java | 12 + .../app/repository/TokenRepository.java | 22 +- .../app/repository/TokensRealmSource.java | 1 + .../app/service/TokensService.java | 21 +- .../service/TransactionsNetworkClient.java | 34 ++- .../TransactionsNetworkClientType.java | 1 + .../app/service/TransactionsService.java | 45 ++++ .../com/alphawallet/app/ui/HomeActivity.java | 2 +- 12 files changed, 347 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/alphawallet/app/entity/NetworkInfo.java b/app/src/main/java/com/alphawallet/app/entity/NetworkInfo.java index e303d954bc..d51951bc6a 100644 --- a/app/src/main/java/com/alphawallet/app/entity/NetworkInfo.java +++ b/app/src/main/java/com/alphawallet/app/entity/NetworkInfo.java @@ -54,7 +54,7 @@ public String getShortName() public boolean usesSeparateNFTTransferQuery() { - return (etherscanAPI != null && !etherscanAPI.contains(BLOCKSCOUT_API) && !etherscanAPI.contains(MATIC_API) + return (etherscanAPI != null && !etherscanAPI.contains(BLOCKSCOUT_API) && !etherscanAPI.contains(COVALENT) && !etherscanAPI.contains(PALM_API)); } diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/ERC1155Token.java b/app/src/main/java/com/alphawallet/app/entity/tokens/ERC1155Token.java index 037e45e9c0..479dc207d4 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/ERC1155Token.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/ERC1155Token.java @@ -592,10 +592,6 @@ private EthFilter getSendBalanceFilter(Event event, DefaultBlockParameter startB @Override public BigDecimal updateBalance(Realm realm) { - if (getAddress().equalsIgnoreCase("0x85F0e02cb992aa1F9F47112F815F519EF1A59E2D")) - { - System.out.println("YOLESS"); - } try { BigInteger lastEventBlockRead = getLastBlockRead(realm); diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/ERC721Token.java b/app/src/main/java/com/alphawallet/app/entity/tokens/ERC721Token.java index 01e6142cff..9244012300 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/ERC721Token.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/ERC721Token.java @@ -1,40 +1,62 @@ package com.alphawallet.app.entity.tokens; import android.app.Activity; +import android.util.Pair; +import com.alphawallet.app.BuildConfig; import com.alphawallet.app.R; import com.alphawallet.app.entity.ContractType; import com.alphawallet.app.entity.Transaction; import com.alphawallet.app.entity.TransactionInput; import com.alphawallet.app.entity.nftassets.NFTAsset; import com.alphawallet.app.entity.tokendata.TokenGroup; +import com.alphawallet.app.repository.EthereumNetworkBase; import com.alphawallet.app.repository.TokenRepository; +import com.alphawallet.app.repository.entity.RealmNFTAsset; import com.alphawallet.app.repository.entity.RealmToken; +import com.alphawallet.app.service.TransactionsService; import com.alphawallet.app.viewmodel.BaseViewModel; +import org.web3j.abi.EventEncoder; +import org.web3j.abi.EventValues; import org.web3j.abi.FunctionEncoder; import org.web3j.abi.FunctionReturnDecoder; +import org.web3j.abi.TypeEncoder; import org.web3j.abi.TypeReference; import org.web3j.abi.datatypes.Address; +import org.web3j.abi.datatypes.DynamicArray; +import org.web3j.abi.datatypes.Event; import org.web3j.abi.datatypes.Function; import org.web3j.abi.datatypes.Type; import org.web3j.abi.datatypes.generated.Uint256; import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.DefaultBlockParameter; import org.web3j.protocol.core.DefaultBlockParameterName; +import org.web3j.protocol.core.methods.request.EthFilter; import org.web3j.protocol.core.methods.response.EthCall; +import org.web3j.protocol.core.methods.response.EthLog; +import org.web3j.protocol.core.methods.response.Log; import org.web3j.utils.Numeric; +import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import static com.alphawallet.app.repository.TokensRealmSource.databaseKey; import static com.alphawallet.app.util.Utils.parseTokenId; import static org.web3j.protocol.core.methods.request.Transaction.createEthCallTransaction; +import static org.web3j.tx.Contract.staticExtractEventParameters; + +import io.realm.Realm; /** * Created by James on 3/10/2018. @@ -382,6 +404,21 @@ else if (owner.toLowerCase().equals(getWallet())) return tokenBalanceAssets; } + private HashSet checkBalances(Web3j web3j, HashSet eventIds) + { + HashSet heldTokens = new HashSet<>(); + for (BigInteger tokenId : eventIds) + { + String owner = callSmartContractFunction(web3j, ownerOf(tokenId), getAddress(), getWallet()); + if (owner == null || owner.toLowerCase().equals(getWallet())) + { + heldTokens.add(tokenId); + } + } + + return heldTokens; + } + @Override public List getChangeList(Map assetMap) { @@ -392,9 +429,10 @@ public List getChangeList(Map assetMap) List changeList = new ArrayList<>(oldAssetIdList); //Now detect differences or new tokens - for (BigInteger tokenId : tokenBalanceAssets.keySet()) + for (Map.Entry entry : tokenBalanceAssets.entrySet()) { - NFTAsset newAsset = tokenBalanceAssets.get(tokenId); + BigInteger tokenId = entry.getKey(); + NFTAsset newAsset = entry.getValue(); NFTAsset oldAsset = assetMap.get(tokenId); if (oldAsset == null || newAsset.hashCode() != oldAsset.hashCode()) @@ -442,6 +480,185 @@ private static Function ownerOf(BigInteger token) { @Override public List getStandardFunctions() { - return Arrays.asList(R.string.action_transfer); + return Collections.singletonList(R.string.action_transfer); + } + + + //determine token balance + //1 determine first transaction + + + //get balance + private Event getTransferEvents() + { + //event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); + List> paramList = new ArrayList<>(); + paramList.add(new TypeReference
(true) {}); + paramList.add(new TypeReference
(true) {}); + paramList.add(new TypeReference(true) {}); + + return new Event("Transfer", paramList); + } + + /** + * Uses both events and balance call. Each call to updateBalance uses 3 Node calls: + * 1. Get new Transfer events since last call + * 2. + * + * Once we have the current balance for potential tokens the database is updated to reflect the current status + * + * Note that this function is used even for contracts covered by OpenSea: This is because we could be looking at + * a contract 'join' between successive opensea reads. With accounts with huge quantity of NFT, this happens a lot + * + * @param realm + * @return + */ + @Override + public BigDecimal updateBalance(Realm realm) + { + try + { + //first get current block + BigInteger currentBlock = TransactionsService.getCurrentBlock(tokenInfo.chainId); + BigInteger startingBlockVal = BigInteger.valueOf(firstTransactionBlock); + DefaultBlockParameter startBlock = DefaultBlockParameter.valueOf(startingBlockVal); + BigInteger calcEndBlock = startingBlockVal.add(EthereumNetworkBase.getMaxEventFetch(tokenInfo.chainId)); + DefaultBlockParameter endBlock = (currentBlock.compareTo(BigInteger.ZERO) > 0 && calcEndBlock.compareTo(currentBlock) < 0) ? + DefaultBlockParameter.valueOf(calcEndBlock) : DefaultBlockParameterName.LATEST; + + final Web3j web3j = TokenRepository.getWeb3jService(tokenInfo.chainId); + + HashSet eventIds = processTransferEvents(web3j, startBlock, endBlock); + updateStartBlock(realm, calcEndBlock); + + HashSet tokenIdsHeld = checkBalances(web3j, eventIds); + + //should we check existing assets too? + + //add to realm + updateRealmBalance(realm, tokenIdsHeld); + } + catch (Exception e) + { + if (BuildConfig.DEBUG) e.printStackTrace(); + } + + return BigDecimal.ONE; + } + + private void updateRealmBalance(Realm realm, Set tokenIds) + { + boolean updated = false; + //fill in balances + if (tokenIds != null && tokenIds.size() > 0) + { + for (BigInteger tokenId : tokenIds) + { + NFTAsset asset = tokenBalanceAssets.get(tokenId); + if (asset == null) + { + tokenBalanceAssets.put(tokenId, new NFTAsset(tokenId)); + updated = true; + } + } + + if (updated) + { + updateRealmBalances(realm, tokenIds); + } + } + } + + private void updateRealmBalances(Realm realm, Set tokenIds) + { + if (realm == null) return; + realm.executeTransaction(r -> { + for (BigInteger tokenId : tokenIds) + { + String key = RealmNFTAsset.databaseKey(this, tokenId); + RealmNFTAsset realmAsset = realm.where(RealmNFTAsset.class) + .equalTo("tokenIdAddr", key) + .findFirst(); + + if (realmAsset == null) + { + realmAsset = r.createObject(RealmNFTAsset.class, key); //create asset in realm + realmAsset.setMetaData(tokenBalanceAssets.get(tokenId).jsonMetaData()); + r.insertOrUpdate(realmAsset); + } + } + }); + } + + private HashSet processTransferEvents(Web3j web3j, DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) throws IOException + { + final Event event = getTransferEvents(); + EthFilter filter = getTransferFilter(event, startBlock, endBlock); + EthLog receiveLogs = web3j.ethGetLogs(filter).send(); + HashSet tokenIds = new HashSet<>(); + BigInteger lastEventBlockRead = Numeric.toBigInt(startBlock.getValue()); + + for (EthLog.LogResult ethLog : receiveLogs.getLogs()) + { + String block = ((Log) ethLog.get()).getBlockNumberRaw(); + if (block == null || block.length() == 0) continue; + BigInteger blockNumber = new BigInteger(Numeric.cleanHexPrefix(block), 16); + + final EventValues eventValues = staticExtractEventParameters(event, (Log) ethLog.get()); + BigInteger _id = new BigInteger(eventValues.getIndexedValues().get(2).getValue().toString()); + tokenIds.add(_id); + + if (blockNumber.compareTo(lastEventBlockRead) > 0) + lastEventBlockRead = blockNumber; + } + + return tokenIds; + } + + private EthFilter getTransferFilter(Event event, DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) + { + final org.web3j.protocol.core.methods.request.EthFilter filter = + new org.web3j.protocol.core.methods.request.EthFilter( + startBlock, + endBlock, + tokenInfo.address) // retort contract address + .addSingleTopic(EventEncoder.encode(event));// commit event format + + filter.addSingleTopic(null); + filter.addSingleTopic("0x" + TypeEncoder.encode(new Address(getWallet()))); //listen for events 'to' our wallet, we can check balance at end + filter.addSingleTopic(null); + return filter; + } + + private void updateEventBlock(Realm realm, BigInteger lastEventBlockRead) + { + if (realm == null) return; + + realm.executeTransaction(r -> { + RealmToken realmToken = r.where(RealmToken.class) + .equalTo("address", databaseKey(tokenInfo.chainId, getAddress())) + .findFirst(); + + if (realmToken != null) + { + realmToken.setErc1155BlockRead(lastEventBlockRead.add(BigInteger.ONE)); + } + }); + } + + private void updateStartBlock(Realm realm, BigInteger startingEventBlock) + { + if (realm == null) return; + + realm.executeTransaction(r -> { + RealmToken realmToken = r.where(RealmToken.class) + .equalTo("address", databaseKey(tokenInfo.chainId, getAddress())) + .findFirst(); + + if (realmToken != null) + { + realmToken.setEarliestTransactionBlock(startingEventBlock.add(BigInteger.ONE).longValue()); + } + }); } } diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/Token.java b/app/src/main/java/com/alphawallet/app/entity/tokens/Token.java index 1eb40b67ed..89dac8d6ea 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/Token.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/Token.java @@ -75,6 +75,8 @@ public class Token public int itemViewHeight; public TokenGroup group; + protected long firstTransactionBlock; + private final Map> resultMap = new ConcurrentHashMap<>(); //Build result map for function parse, per tokenId private Map> functionAvailabilityMap = null; @@ -998,4 +1000,14 @@ public boolean checkInfoRequiresUpdate(RealmToken realmToken) if (realmToken.getContractType() != contractType) { return true; } return realmToken.getDecimals() != tokenInfo.decimals; } + + public void setFirstTransactionBlock(long earliestTransactionBlock) + { + firstTransactionBlock = earliestTransactionBlock; + } + + public long getFirstTransactionBlock() + { + return firstTransactionBlock; + } } \ No newline at end of file diff --git a/app/src/main/java/com/alphawallet/app/repository/EthereumNetworkBase.java b/app/src/main/java/com/alphawallet/app/repository/EthereumNetworkBase.java index 146cbcc46f..739ca85690 100644 --- a/app/src/main/java/com/alphawallet/app/repository/EthereumNetworkBase.java +++ b/app/src/main/java/com/alphawallet/app/repository/EthereumNetworkBase.java @@ -940,4 +940,16 @@ public static String getChainSymbol(long chainId) return networkMap.get(MAINNET_ID).symbol; } } + + public static BigInteger getMaxEventFetch(long chainId) + { + if (chainId == MATIC_ID || chainId == MATIC_TEST_ID) + { + return BigInteger.valueOf(3500L); + } + else + { + return BigInteger.valueOf(10000L); + } + } } diff --git a/app/src/main/java/com/alphawallet/app/repository/TokenRepository.java b/app/src/main/java/com/alphawallet/app/repository/TokenRepository.java index 3993de48d3..16ea077169 100644 --- a/app/src/main/java/com/alphawallet/app/repository/TokenRepository.java +++ b/app/src/main/java/com/alphawallet/app/repository/TokenRepository.java @@ -386,11 +386,6 @@ public String getTokenImageUrl(long networkId, String address) private Single updateBalance(final Wallet wallet, final Token token) { - if (token.getAddress().equalsIgnoreCase("0x85F0e02cb992aa1F9F47112F815F519EF1A59E2D")) - { - System.out.println("YOLESS"); - } - return Single.fromCallable(() -> { BigDecimal balance = BigDecimal.valueOf(-1); try @@ -398,11 +393,6 @@ private Single updateBalance(final Wallet wallet, final Token token) List balanceArray = null; Token thisToken = token; - if (thisToken.getAddress().equalsIgnoreCase("0x85F0e02cb992aa1F9F47112F815F519EF1A59E2D")) - { - System.out.println("YOLESS"); - } - switch (token.getInterfaceSpec()) { case ETHEREUM: @@ -415,6 +405,8 @@ private Single updateBalance(final Wallet wallet, final Token token) break; case ERC721_LEGACY: case ERC721: + balance = updateERC721Balance(token, wallet); + break; case ERC20: case DYNAMIC_CONTRACT: //checking raw balance, this only gives the count of tokens @@ -469,6 +461,16 @@ private BigDecimal updateERC1155Balance(Token token, Wallet wallet) return newBalance; } + private BigDecimal updateERC721Balance(Token token, Wallet wallet) + { + try (Realm realm = getRealmInstance(wallet)) + { + token.updateBalance(realm); + } + + return checkUint256Balance(wallet, token.tokenInfo.chainId, token.getAddress()); + } + private Single updateBalances(Wallet wallet, Token[] tokens) { return Single.fromCallable(() -> { diff --git a/app/src/main/java/com/alphawallet/app/repository/TokensRealmSource.java b/app/src/main/java/com/alphawallet/app/repository/TokensRealmSource.java index ead548945c..67dc8f5802 100644 --- a/app/src/main/java/com/alphawallet/app/repository/TokensRealmSource.java +++ b/app/src/main/java/com/alphawallet/app/repository/TokensRealmSource.java @@ -1404,6 +1404,7 @@ private Token convertSingle(RealmToken realmItem, Realm realm, TokenFactory tf, } loadAssetContract(realm, result); + result.setFirstTransactionBlock(realmItem.getEarliestTransactionBlock()); } return result; } diff --git a/app/src/main/java/com/alphawallet/app/service/TokensService.java b/app/src/main/java/com/alphawallet/app/service/TokensService.java index ab725d7350..6942a83dcd 100644 --- a/app/src/main/java/com/alphawallet/app/service/TokensService.java +++ b/app/src/main/java/com/alphawallet/app/service/TokensService.java @@ -88,6 +88,7 @@ public class TokensService private long syncStart; private ServiceSyncCallback completionCallback; private int syncCount = 0; + private final ConcurrentLinkedQueue firstTransactionList = new ConcurrentLinkedQueue<>(); @Nullable private Disposable eventTimer; @@ -618,6 +619,10 @@ private void onBalanceChange(BigDecimal newBalance, Token t) { checkERC20(t.tokenInfo.chainId); } + else if (t.isNonFungible() && t.getFirstTransactionBlock() == 0) + { + firstTransactionList.add(t); + } } private void checkChainVisibility(Token t) @@ -846,18 +851,13 @@ public Token getNextInBalanceUpdateQueue() float weighting = check.calculateBalanceUpdateWeight(); - if (check.getAddress().equalsIgnoreCase("0x85F0e02cb992aa1F9F47112F815F519EF1A59E2D")) - { - System.out.println("YOLESS"); - } - if ((!check.isEnabled || check.isNFT()) && !isSynced()) continue; //don't start looking at NFT balances until we sync the chain/ERC20 tokens if (!isSynced() && check.lastUpdate > syncStart) continue; //don't start updating already updated tokens until all ERC20 are checked if (!appHasFocus && (!check.isEthereum() && !isFocusToken(check))) continue; //only check chains when wallet out of focus //simply multiply the weighting by the last diff. float updateFactor = weighting * (float) lastCheckDiff * (check.isEnabled ? 1 : 0.25f); - long cutoffCheck = 30*DateUtils.SECOND_IN_MILLIS / (check.isEnabled ? 1 : 10); //normal minimum update frequency for token 30 seconds, 5 minutes for hidden token + long cutoffCheck = 30*DateUtils.SECOND_IN_MILLIS * (check.isEnabled ? 1 : 10); //normal minimum update frequency for token 30 seconds, 5 minutes for hidden token if (!check.isEthereum() && lastUpdateDiff > DateUtils.DAY_IN_MILLIS) { @@ -894,11 +894,6 @@ else if (focusToken != null) if (highestToken != null) { - if (highestToken.getAddress().equalsIgnoreCase("0x85F0e02cb992aa1F9F47112F815F519EF1A59E2D")) - { - System.out.println("YOLESS"); - } - pendingTokenMap.put(databaseKey(highestToken.getChain(), highestToken.getAddress()), System.currentTimeMillis()); return getToken(highestToken.getChain(), highestToken.getAddress()); } @@ -1251,4 +1246,8 @@ public TokenGroup getTokenGroup(Token token) return tokenRepository.getTokenGroup(token.tokenInfo.chainId, token.tokenInfo.address, token.getInterfaceSpec()); } + public Token getNextTokenInFetchList() + { + return firstTransactionList.poll(); + } } diff --git a/app/src/main/java/com/alphawallet/app/service/TransactionsNetworkClient.java b/app/src/main/java/com/alphawallet/app/service/TransactionsNetworkClient.java index b241d35cbd..ae863e1b34 100644 --- a/app/src/main/java/com/alphawallet/app/service/TransactionsNetworkClient.java +++ b/app/src/main/java/com/alphawallet/app/service/TransactionsNetworkClient.java @@ -54,6 +54,7 @@ import static com.alphawallet.ethereum.EthereumNetworkBase.ARTIS_TAU1_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.BINANCE_MAIN_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.BINANCE_TEST_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.MATIC_ID; public class TransactionsNetworkClient implements TransactionsNetworkClientType { @@ -66,7 +67,7 @@ public class TransactionsNetworkClient implements TransactionsNetworkClientType private final String BLOCK_ENTRY = "-erc20blockCheck-"; private final String ERC20_QUERY = "tokentx"; private final String ERC721_QUERY = "tokennfttx"; - private final int AUX_DATABASE_ID = 23; //increment this to do a one off refresh the AUX database, in case of changed design etc + private final int AUX_DATABASE_ID = 25; //increment this to do a one off refresh the AUX database, in case of changed design etc private final String DB_RESET = BLOCK_ENTRY + AUX_DATABASE_ID; private final String ETHERSCAN_API_KEY; private final String BSC_EXPLORER_API_KEY; @@ -124,6 +125,31 @@ public void checkRequiresAuxReset(String walletAddr) } } + @Override + public Single getEarliestContractTransaction(NetworkInfo network, String walletAddress, String tokenAddress) + { + return Single.fromCallable(() -> { + long earliestTx = 0; + try (Realm instance = realmManager.getRealmInstance(walletAddress)) + { + earliestTx = getFirstTransactionBlock(instance, network.chainId, tokenAddress); + if (earliestTx > 0) return earliestTx; + + //fetch earliest + //https://api.polygonscan.com/api?module=account&action=txlist&address=0x85F0e02cb992aa1F9F47112F815F519EF1A59E2D&startblock=0&endblock=999999999&sort=asc&page=1&offset=1 + EtherscanTransaction[] earliest = readTransactions(network, walletAddress, tokenAddress, "1", true, 1, 1); + if (earliest.length > 0) + { + earliestTx = Long.parseLong(earliest[0].blockNumber); + storeEarliestBlockRead(instance, network.chainId, tokenAddress, earliestTx); + } + + return earliestTx; + } + }); + + } + /* * * Transaction sync strategy: @@ -789,6 +815,10 @@ private long getFirstTransactionBlock(Realm instance, long chainId, String walle { txBlockRead = realmToken.getEarliestTransactionBlock(); } + else + { + txBlockRead = -1; + } } catch (Exception e) { @@ -850,7 +880,7 @@ private void storeEarliestBlockRead(Realm instance, long chainId, String walletA { try { - instance.executeTransactionAsync(r -> { + instance.executeTransaction(r -> { RealmToken realmToken = r.where(RealmToken.class) .equalTo("address", databaseKey(chainId, walletAddress)) .findFirst(); diff --git a/app/src/main/java/com/alphawallet/app/service/TransactionsNetworkClientType.java b/app/src/main/java/com/alphawallet/app/service/TransactionsNetworkClientType.java index bc72d826e8..e7c5919e71 100644 --- a/app/src/main/java/com/alphawallet/app/service/TransactionsNetworkClientType.java +++ b/app/src/main/java/com/alphawallet/app/service/TransactionsNetworkClientType.java @@ -11,4 +11,5 @@ public interface TransactionsNetworkClientType { Single fetchMoreTransactions(String walletAddress, NetworkInfo network, long lastTxTime); Single readTransfers(String currentAddress, NetworkInfo networkByChain, TokensService tokensService, boolean nftCheck); void checkRequiresAuxReset(String walletAddr); + Single getEarliestContractTransaction(NetworkInfo network, String walletAddress, String address); } diff --git a/app/src/main/java/com/alphawallet/app/service/TransactionsService.java b/app/src/main/java/com/alphawallet/app/service/TransactionsService.java index c741167065..2809e4ff92 100644 --- a/app/src/main/java/com/alphawallet/app/service/TransactionsService.java +++ b/app/src/main/java/com/alphawallet/app/service/TransactionsService.java @@ -21,9 +21,11 @@ import com.alphawallet.app.repository.TransactionLocalSource; import com.alphawallet.app.util.Utils; import com.alphawallet.token.entity.ContractAddress; +import com.alphawallet.token.tools.Numeric; import org.web3j.exceptions.MessageDecodingException; import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.DefaultBlockParameterName; import org.web3j.protocol.core.methods.response.EthBlock; import org.web3j.protocol.core.methods.response.EthGetTransactionReceipt; import org.web3j.protocol.core.methods.response.EthTransaction; @@ -32,6 +34,10 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import io.reactivex.Observable; @@ -54,6 +60,8 @@ public class TransactionsService private int currentChainIndex; private boolean nftCheck; + private static final LongSparseArray currentBlocks = new LongSparseArray<>(); + private final LongSparseArray chainTransferCheckTimes = new LongSparseArray<>(); //TODO: Use this to coordinate token checks on chains private final LongSparseArray chainTransactionCheckTimes = new LongSparseArray<>(); @@ -155,6 +163,10 @@ private void checkTransfers() if (currentChainIndex >= filters.size()) currentChainIndex = 0; readTokenMoves(filters.get(currentChainIndex), nftCheck); //check NFTs for same chain on next iteration or advance to next chain Pair pendingChainData = getNextChainIndex(currentChainIndex, nftCheck, filters); + if (pendingChainData.first != currentChainIndex) + { + updateCurrentBlock(filters.get(currentChainIndex)); + } currentChainIndex = pendingChainData.first; nftCheck = pendingChainData.second; } @@ -244,6 +256,20 @@ private void checkTransactionQueue() } } + private void updateCurrentBlock(final long chainId) + { + Single.fromCallable(() -> { + Web3j web3j = TokenRepository.getWeb3jService(chainId); + EthBlock ethBlock = + web3j.ethGetBlockByNumber(DefaultBlockParameterName.LATEST, false).send(); + String blockValStr = ethBlock.getBlock().getNumberRaw(); + if (!TextUtils.isEmpty(blockValStr) && blockValStr.length() > 2) return Numeric.toBigInt(blockValStr); + else return currentBlocks.get(chainId, BigInteger.ZERO); + }).subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(blockValue -> currentBlocks.put(chainId, blockValue), onError -> currentBlocks.put(chainId, BigInteger.ZERO)).isDisposed(); + } + private Token getRequiresTransactionUpdate() { List chains = tokensService.getNetworkFilters(); @@ -315,6 +341,7 @@ private void onTxError(Throwable throwable) private void onUpdateTransactions(Transaction[] transactions, Token token) { + checkEventPoints(); //got a new transaction fetchTransactionDisposable = null; if (transactions.length == 0) return; @@ -327,6 +354,19 @@ private void onUpdateTransactions(Transaction[] transactions, Token token) checkTokens(transactions); } + private void checkEventPoints() + { + Token checkToken = tokensService.getNextTokenInFetchList(); + if (checkToken != null) + { + NetworkInfo network = ethereumNetworkRepository.getNetworkByChain(checkToken.tokenInfo.chainId); + transactionsClient.getEarliestContractTransaction(network, tokensService.getCurrentAddress(), checkToken.tokenInfo.address) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe().isDisposed(); + } + } + /** * Check new tokens for any unknowns, then find the unknowns * @@ -515,4 +555,9 @@ public Single wipeTickerData() { return transactionsCache.deleteAllTickers(); } + + public static BigInteger getCurrentBlock(long chainId) + { + return currentBlocks.get(chainId, BigInteger.ZERO); + } } diff --git a/app/src/main/java/com/alphawallet/app/ui/HomeActivity.java b/app/src/main/java/com/alphawallet/app/ui/HomeActivity.java index 342cec0c18..44a4366d5b 100644 --- a/app/src/main/java/com/alphawallet/app/ui/HomeActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/HomeActivity.java @@ -849,7 +849,7 @@ public int getCount() private BaseFragment getFragment(WalletPage page) { //build map, return correct fragment. - if (getSupportFragmentManager().getFragments().size() < page.ordinal()) + if (getSupportFragmentManager().getFragments().size() == 0) { switch (page) {