package com.alphawallet.app.repository; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import com.alphawallet.app.BuildConfig; import com.alphawallet.app.entity.ContractLocator; import com.alphawallet.app.entity.ContractType; import com.alphawallet.app.entity.NetworkInfo; import com.alphawallet.app.entity.SubscribeWrapper; import com.alphawallet.app.entity.TransferFromEventResponse; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.entity.tokens.TokenFactory; import com.alphawallet.app.entity.tokens.TokenInfo; import com.alphawallet.app.entity.tokens.TokenTicker; import com.alphawallet.app.service.AWHttpService; import com.alphawallet.app.service.TokensService; import com.alphawallet.app.util.AWEnsResolver; import com.alphawallet.app.util.Utils; import com.alphawallet.token.entity.MagicLinkData; import org.web3j.abi.FunctionEncoder; import org.web3j.abi.FunctionReturnDecoder; import org.web3j.abi.TypeReference; import org.web3j.abi.datatypes.Address; import org.web3j.abi.datatypes.Bool; import org.web3j.abi.datatypes.DynamicArray; import org.web3j.abi.datatypes.Function; import org.web3j.abi.datatypes.Type; import org.web3j.abi.datatypes.Uint; import org.web3j.abi.datatypes.Utf8String; import org.web3j.abi.datatypes.generated.Bytes4; import org.web3j.abi.datatypes.generated.Int256; import org.web3j.abi.datatypes.generated.Uint256; import org.web3j.abi.datatypes.generated.Uint8; import org.web3j.protocol.Web3j; import org.web3j.protocol.core.DefaultBlockParameterName; import org.web3j.protocol.core.methods.response.EthBlockNumber; import org.web3j.protocol.core.methods.response.EthCall; import org.web3j.utils.Numeric; import java.io.IOException; import java.io.InterruptedIOException; import java.math.BigDecimal; import java.math.BigInteger; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import io.reactivex.Completable; import io.reactivex.Observable; import io.reactivex.Single; import io.reactivex.SingleTransformer; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import okhttp3.OkHttpClient; import static com.alphawallet.app.entity.tokenscript.TokenscriptFunction.ZERO_ADDRESS; import static org.web3j.protocol.core.methods.request.Transaction.createEthCallTransaction; public class TokenRepository implements TokenRepositoryType { private static final String TAG = "TRT"; private final TokenLocalSource localSource; private final EthereumNetworkRepositoryType ethereumNetworkRepository; private final OkHttpClient okClient; private final Context context; public static final String INVALID_CONTRACT = "<invalid>"; public static final BigInteger INTERFACE_CRYPTOKITTIES = new BigInteger ("9a20483d", 16); public static final BigInteger INTERFACE_OFFICIAL_ERC721 = new BigInteger ("80ac58cd", 16); public static final BigInteger INTERFACE_OLD_ERC721 = new BigInteger ("6466353c", 16); public static final BigInteger INTERFACE_BALANCES_721_TICKET = new BigInteger ("c84aae17", 16); private static final int NODE_COMMS_ERROR = -1; private static final int CONTRACT_BALANCE_NULL = -2; private final Map<Integer, Web3j> web3jNodeServers; private AWEnsResolver ensResolver; public TokenRepository( EthereumNetworkRepositoryType ethereumNetworkRepository, TokenLocalSource localSource, OkHttpClient okClient, Context context) { this.ethereumNetworkRepository = ethereumNetworkRepository; this.localSource = localSource; this.ethereumNetworkRepository.addOnChangeDefaultNetwork(this::buildWeb3jClient); this.okClient = okClient; this.context = context; web3jNodeServers = new ConcurrentHashMap<>(); } private void buildWeb3jClient(NetworkInfo networkInfo) { AWHttpService publicNodeService = new AWHttpService(networkInfo.rpcServerUrl, networkInfo.backupNodeUrl, okClient, false); EthereumNetworkRepository.addRequiredCredentials(networkInfo.chainId, publicNodeService); web3jNodeServers.put(networkInfo.chainId, Web3j.build(publicNodeService)); } private Web3j getService(int chainId) { if (!web3jNodeServers.containsKey(chainId)) { buildWeb3jClient(ethereumNetworkRepository.getNetworkByChain(chainId)); } return web3jNodeServers.get(chainId); } @Override public Observable<Token[]> fetchActiveStoredPlusEth(String walletAddress) { Wallet wallet = new Wallet(walletAddress); return fetchStoredTokens(wallet) // fetch tokens from cache .compose(attachDefaultTokens(wallet)) .compose(attachEthereumStored(wallet)) //add cached eth balance .toObservable(); } private SingleTransformer<Token[], Token[]> attachDefaultTokens(Wallet wallet) { return upstream -> Single.zip( upstream, ethereumNetworkRepository.getBlankOverrideTokens(wallet), (tokens, defaultTokens) -> { List<Token> result = mergeTokens(tokens, defaultTokens); return result.toArray(new Token[0]); }); } private List<Token> mergeTokens(Token[] tokens, Token[] defaultTokens) { Map<Integer, Map<String, Token>> tokenMergeMap = new HashMap<>(); for (Token t : defaultTokens) { if (!tokenMergeMap.containsKey(t.tokenInfo.chainId)) tokenMergeMap.put(t.tokenInfo.chainId, new HashMap<>()); tokenMergeMap.get(t.tokenInfo.chainId).put(t.tokenInfo.address, t); } //replace with cached tokens for (Token t : tokens) { if (!tokenMergeMap.containsKey(t.tokenInfo.chainId)) tokenMergeMap.put(t.tokenInfo.chainId, new HashMap<>()); tokenMergeMap.get(t.tokenInfo.chainId).put(t.tokenInfo.address, t); } List<Token> tokenList = new ArrayList<>(); for (int i : tokenMergeMap.keySet()) { tokenList.addAll(tokenMergeMap.get(i).values()); } return tokenList; } @Override public Single<BigInteger> fetchLatestBlockNumber(int chainId) { return Single.fromCallable(() -> { try { EthBlockNumber blk = getService(chainId).ethBlockNumber() .send(); return blk.getBlockNumber(); } catch (Exception e) { return BigInteger.ZERO; } }); } @Override public Observable<Token[]> fetchStored(String walletAddress) { Wallet wallet = new Wallet(walletAddress); return fetchStoredTokens(wallet) // fetch tokens from cache .toObservable(); } private SingleTransformer<Token[], Token[]> attachEthereumStored(Wallet wallet) { return upstream -> Single.zip( upstream, attachCachedEth(wallet), (tokens, ethTokens) -> { List<Token> result = new ArrayList<>(); result.addAll(ethTokens); for (Token t : tokens) if (!t.isEthereum()) result.add(t); return result.toArray(new Token[0]); }); } private Single<List<Token>> attachCachedEth(Wallet wallet) { //get stored eth balance return Single.fromCallable(() -> { Map<Integer, Token> currencies = localSource.getTokenBalances(wallet, wallet.address); //always show eth balance, others optional NetworkInfo[] allNetworks = ethereumNetworkRepository.getAvailableNetworkList(); for (NetworkInfo info : allNetworks) { if (!currencies.containsKey(info.chainId)) { currencies.put(info.chainId, createCurrencyToken(info, wallet)); } } return new ArrayList(currencies.values()); }); } private Token createCurrencyToken(NetworkInfo network, Wallet wallet) { TokenInfo tokenInfo = new TokenInfo(wallet.address, network.name, network.symbol, 18, true, network.chainId); BigDecimal balance = BigDecimal.ZERO; Token eth = new Token(tokenInfo, balance, 0, network.getShortName(), ContractType.ETHEREUM); //create with zero time index to ensure it's updated immediately eth.setTokenWallet(wallet.address); eth.setIsEthereum(); eth.pendingBalance = balance; return eth; } @Override public Single<Token> fetchActiveSingle(String walletAddress, Token token) { NetworkInfo network = ethereumNetworkRepository.getNetworkByChain(token.tokenInfo.chainId); Wallet wallet = new Wallet(walletAddress); return fetchCachedToken(token.tokenInfo.chainId, wallet, token.getAddress()) .flatMap(t -> updateBalance(network, wallet, t)); // Looking for new tokens } @Override public Observable<Token> fetchCachedSingleToken(int chainId, String walletAddress, String tokenAddress) { Wallet wallet = new Wallet(walletAddress); return fetchCachedToken(chainId, wallet, tokenAddress) .toObservable(); } @Override public Token fetchToken(int chainId, String walletAddress, String address) { Wallet wallet = new Wallet(walletAddress); return localSource.fetchToken(chainId, wallet, address); } /** * Just updates the balance of a token * * @param walletAddress * @param token * @return */ @Override public Observable<Token> fetchActiveTokenBalance(String walletAddress, Token token) { NetworkInfo network = ethereumNetworkRepository.getNetworkByChain(token.tokenInfo.chainId); Wallet wallet = new Wallet(walletAddress); return updateBalance(network, wallet, token) .observeOn(Schedulers.newThread()) .toObservable(); } @Override public Single<Token> addToken(Wallet wallet, TokenInfo tokenInfo, ContractType contractType) { return Single.fromCallable(() -> { TokenFactory tf = new TokenFactory(); NetworkInfo network = ethereumNetworkRepository.getNetworkByChain(tokenInfo.chainId); //check balance before we store it List<BigInteger> balanceArray = null; BigDecimal balance = BigDecimal.ZERO; switch (contractType) { case ERC875: case ERC875_LEGACY: balanceArray = checkERC875BalanceArray(wallet, tokenInfo, null); break; case ERC721_LEGACY: case ERC721: break; case ERC721_TICKET: balanceArray = checkERC721TicketBalanceArray(wallet, tokenInfo, null); break; case ETHEREUM: case ERC20: balance = wrappedCheckUint256Balance(wallet, tokenInfo, null); //this checks Uint256 balance if (balance.compareTo(BigDecimal.ZERO) < 0) balance = BigDecimal.ZERO; break; default: break; } Token newToken = tf.createToken(tokenInfo, balance, balanceArray, System.currentTimeMillis(), contractType, network.getShortName(), 0); newToken.setTokenWallet(wallet.address); newToken.ticker = ethereumNetworkRepository.getTokenTicker(newToken); return newToken; }).flatMap(nToken -> localSource.saveToken(wallet, nToken)); } private String callStringFunction(String method, String address, NetworkInfo network, BigInteger tokenId) { String result; try { result = getContractData(network, address, stringParam(method, tokenId), ""); } catch (Exception e) { result = null; } return result; } private String callBoolFunction(String method, String address, NetworkInfo network) { String result; try { Boolean res = getContractData(network, address, boolParam(method), Boolean.TRUE); result = res ? "true" : "false"; } catch (Exception e) { result = "false"; } return result; } @Override public Single<Token[]> storeTokens(Wallet wallet, Token[] tokens) { return localSource.saveTokens( wallet, tokens); } @Override public Single<Token[]> addERC20(Wallet wallet, Token[] tokens) { return localSource.saveERC20Tokens( wallet, tokens); } @Override public Token updateTokenType(Token token, Wallet wallet, ContractType type) { return localSource.updateTokenType(token, wallet, type); } @Override public Single<Token[]> storeTickers(Wallet wallet, Token[] tokens) { return localSource.saveTickers(wallet, tokens); } @Override public Single<String> resolveENS(int chainId, String ensName) { if (ensResolver == null) ensResolver = new AWEnsResolver(TokenRepository.getWeb3jService(EthereumNetworkRepository.MAINNET_ID), context); return ensResolver.resolveENSAddress(ensName); } @Override public Completable setEnable(Wallet wallet, Token token, boolean isEnabled) { NetworkInfo network = ethereumNetworkRepository.getDefaultNetwork(); localSource.setEnable(network, wallet, token, isEnabled); return Completable.fromAction(() -> {}); } @Override public Observable<TokenInfo> update(String contractAddr, int chainId) { return setupTokensFromLocal(contractAddr, chainId).toObservable(); } @Override public Disposable terminateToken(Token token, Wallet wallet, NetworkInfo network) { return localSource.setTokenTerminated(token, network, wallet); } /** * Obtain live balance of token from Ethereum blockchain and cache into Realm * * @param network * @param wallet * @param token * @return */ private Single<Token> updateBalance(NetworkInfo network, Wallet wallet, final Token token) { if (token == null) return Single.fromCallable(() -> null); else if (token.isEthereum()) { return attachEth(network, wallet, token); } else return Single.fromCallable(() -> { TokenFactory tFactory = new TokenFactory(); try { List<BigInteger> balanceArray = null; BigDecimal balance = BigDecimal.ZERO; TokenInfo tInfo = token.tokenInfo; ContractType interfaceSpec = token.getInterfaceSpec(); boolean balanceChanged = false; switch (interfaceSpec) { case NOT_SET: if (token.tokenInfo.name != null && token.tokenInfo.name.length() > 0) { Log.d(TAG, "NOT SET: " + token.getFullName()); } break; case ERC875: case ERC875_LEGACY: balanceArray = checkERC875BalanceArray(wallet, tInfo, token); balanceChanged = token.checkBalanceChange(balanceArray); break; case ERC721_LEGACY: case ERC721: break; case ERC721_TICKET: balanceArray = checkERC721TicketBalanceArray(wallet, tInfo, token); balanceChanged = token.checkBalanceChange(balanceArray); break; case ETHEREUM: case ERC20: balance = wrappedCheckUint256Balance(wallet, token.tokenInfo, token); balanceChanged = token.checkBalanceChange(balance); break; case OTHER: //This token has its interface checked in the flow elsewhere break; default: break; } //check if we need an update if (balanceChanged) { if (BuildConfig.DEBUG) Log.d(TAG, "Token balance changed! " + tInfo.name); Token updated = tFactory.createToken(tInfo, balance, balanceArray, System.currentTimeMillis(), interfaceSpec, network.getShortName(), token.lastBlockCheck); localSource.updateTokenBalance(network, wallet, updated); updated.setTokenWallet(wallet.address); updated.transferPreviousData(token); updated.balanceChanged = true; updated.pendingBalance = balance; return updated; } else { return token; } } catch (Exception e) { e.printStackTrace(); return token; } }) .flatMap(ethereumNetworkRepository::attachTokenTicker) .flatMap(ttoken -> localSource.saveTicker(wallet, ttoken)); } /** * Checks the balance of a token returning Uint256 value, eg ERC20 * If there was a network error the balance is taken from the previously recorded value * @param wallet * @param tokenInfo * @param token * @return */ private BigDecimal wrappedCheckUint256Balance(@NonNull Wallet wallet, @NonNull TokenInfo tokenInfo, @Nullable Token token) { BigDecimal balance = BigDecimal.ZERO; try { Function function = balanceOf(wallet.address); NetworkInfo network = ethereumNetworkRepository.getNetworkByChain(tokenInfo.chainId); String responseValue = callSmartContractFunction(function, tokenInfo.address, network, wallet); if (token != null && TextUtils.isEmpty(responseValue)) { balance = token.balance; } else { List<Type> response = FunctionReturnDecoder.decode(responseValue, function.getOutputParameters()); if (response.size() > 0) balance = new BigDecimal(((Uint256) response.get(0)).getValue()); //only perform checking if token is non-null if (token != null && tokenInfo.decimals == 18 && balance.compareTo(BigDecimal.ZERO) > 0 && balance.compareTo(BigDecimal.valueOf(10)) < 0) { //suspicious balance - check for ERC721 ticket List<BigInteger> testBalance = getBalanceArray721Ticket(wallet, tokenInfo); if (testBalance != null && testBalance.size() > 0) { addToken(wallet, tokenInfo, ContractType.ERC721_TICKET) .subscribe(this::updateInService).isDisposed(); balance = token.balance; } } else if (token != null && balance.equals(BigDecimal.valueOf(32)) && responseValue.length() > 66) { //this is a token returning an array balance. Test the interface and update determineCommonType(tokenInfo) .flatMap(tType -> addToken(wallet, tokenInfo, tType)) //changes token type .subscribe(this::updateInService).isDisposed(); balance = token.balance; } } } catch (Exception e) { //use previous balance if appropriate if (token != null) balance = token.balance; } return balance; } private void updateInService(Token t) { t.walletUIUpdateRequired = true; TokensService.setInterfaceSpec(t.tokenInfo.chainId, t.getAddress(), t.getInterfaceSpec()); } /** * Checks the balance array for an ERC875 token. * The balance function returns a value of NODE_COMMS_ERROR in the first entry if there was a comms error. * In the event of a comms error, just use the previously obtained balance of the token * @param wallet * @param tInfo * @param token * @return */ private List<BigInteger> checkERC875BalanceArray(Wallet wallet, TokenInfo tInfo, Token token) { List<BigInteger> balance = getBalanceArray875(wallet, tInfo); return checkBalanceArrayValidity(balance, token); } // Only works with 721 tickets that have a special getBalances function which returns an array of uint256 private List<BigInteger> checkERC721TicketBalanceArray(Wallet wallet, TokenInfo tInfo, Token token) { List<BigInteger> balance = getBalanceArray721Ticket(wallet, tInfo); return checkBalanceArrayValidity(balance, token); } private List<BigInteger> checkBalanceArrayValidity(List<BigInteger> balance, Token token) { if (balance.size() > 0) { BigInteger firstVal = balance.get(0); if (firstVal.compareTo(BigInteger.valueOf(NODE_COMMS_ERROR)) == 0) { //comms error, use previous token balance if (token != null) balance = token.getArrayBalance(); else balance.clear(); } else if (firstVal.compareTo(BigInteger.valueOf(CONTRACT_BALANCE_NULL)) == 0) { //token could have been terminated balance.clear(); } } return balance; } @Override public Disposable memPoolListener(int chainId, SubscribeWrapper subscriber) { return getService(chainId).pendingTransactionFlowable().subscribe(subscriber::scanReturn); } private Single<Token[]> fetchStoredTokens(Wallet wallet) { return localSource .fetchTokensWithBalance(wallet); } private Single<Token> fetchCachedToken(int chainId, Wallet wallet, String address) { return localSource .fetchEnabledToken(chainId, wallet, address); } private BigDecimal updatePending(Token oldToken, BigDecimal pendingBalance) { if (!TokensService.getCurrentWalletAddress().equalsIgnoreCase(oldToken.getWallet())) { oldToken.pendingBalance = oldToken.balance; } else if (pendingBalance.equals(BigDecimal.valueOf(-1))) { oldToken.pendingBalance = oldToken.balance; } else { oldToken.pendingBalance = pendingBalance; } return pendingBalance; } private Single<Token> attachEth(NetworkInfo network, Wallet wallet, Token oldToken) { return getEthBalanceInternal(network, wallet, true) .map(pendingBalance -> updatePending(oldToken, pendingBalance)) .flatMap(balance -> getEthBalanceInternal(network, wallet, false)) .map(balance -> { if (balance.equals(BigDecimal.valueOf(-1))) { oldToken.pendingBalance = oldToken.balance; return oldToken; } if (!balance.equals(oldToken.balance)) { if (BuildConfig.DEBUG) Log.d(TAG, "Tx Update requested for: " + oldToken.getFullName()); TokenInfo info = new TokenInfo(wallet.address, network.name, network.symbol, 18, true, network.chainId); Token eth = new Token(info, balance, System.currentTimeMillis(), network.getShortName(), ContractType.ETHEREUM); eth.setTokenWallet(wallet.address); //store token and balance localSource.updateTokenBalance(network, wallet, eth); eth.transferPreviousData(oldToken); eth.pendingBalance = balance; eth.balanceChanged = true; return eth; } else if (!balance.equals(oldToken.pendingBalance)) { Log.d(TAG, "ETH: " + balance.toPlainString() + " OLD: " + oldToken.pendingBalance.toPlainString()); return oldToken; } else { return oldToken; } }) .flatMap(token -> localSource.fetchTicker(wallet, token) .map(ticker -> ethereumNetworkRepository.updateTicker(token, ticker)) .map(ticker -> { token.ticker = ticker; return token; }) .flatMap(ttoken -> localSource.saveTicker(wallet, ttoken)) .doOnError(throwable -> { System.out.println(throwable.getMessage()); }) .onErrorResumeNext(throwable -> Single.just(token))); } /** * Either returns the live eth balance or cached if network is unavilable * We can derive the time when the balance was fetched from the Token info * * @param network * @param wallet * @return */ @Override public Single<Token> getEthBalance(NetworkInfo network, Wallet wallet) { Token currency = createCurrencyToken(network, wallet); currency.pendingBalance = BigDecimal.ZERO; return attachEth(network, wallet, currency); } @Override public Single<TokenTicker> getEthTicker(int chainId) { return ethereumNetworkRepository.getTicker(chainId); } @Override public Single<TokenTicker> getTokenTicker(Token token) { return Single.fromCallable(() -> ethereumNetworkRepository.getTokenTicker(token)); } private BigDecimal getBalance(Wallet wallet, TokenInfo tokenInfo) throws Exception { Function function = balanceOf(wallet.address); NetworkInfo network = ethereumNetworkRepository.getNetworkByChain(tokenInfo.chainId); String responseValue = callSmartContractFunction(function, tokenInfo.address, network, wallet); if (responseValue == null) return BigDecimal.valueOf(-1); //early return for network error List<Type> response = FunctionReturnDecoder.decode(responseValue, function.getOutputParameters()); if (response.size() == 1) { return new BigDecimal(((Uint256) response.get(0)).getValue()); } else { return BigDecimal.ZERO; } } private Single<BigDecimal> getEthBalanceInternal(NetworkInfo network, Wallet wallet, boolean pending) { return Single.fromCallable(() -> { try { DefaultBlockParameterName balanceCheckType = pending ? DefaultBlockParameterName.PENDING : DefaultBlockParameterName.LATEST; return new BigDecimal(getService(network.chainId).ethGetBalance(wallet.address, balanceCheckType) .send() .getBalance()); } catch (IOException e) { return BigDecimal.valueOf(-1); } catch (Exception e) { e.printStackTrace(); return BigDecimal.valueOf(-1); } }).subscribeOn(Schedulers.io()); } private List<BigInteger> getBalanceArray875(Wallet wallet, TokenInfo tokenInfo) { List<BigInteger> result = new ArrayList<>(); result.add(BigInteger.valueOf(NODE_COMMS_ERROR)); try { Function function = balanceOfArray(wallet.address); NetworkInfo network = ethereumNetworkRepository.getNetworkByChain(tokenInfo.chainId); List<Type> indices = callSmartContractFunctionArray(function, tokenInfo.address, network, wallet); if (indices != null) { result.clear(); for (Type val : indices) { result.add((BigInteger)val.getValue()); } } } catch (StringIndexOutOfBoundsException e) { //contract call error result.clear(); result.add(BigInteger.valueOf(NODE_COMMS_ERROR)); } return result; } private List<BigInteger> getBalanceArray721Ticket(Wallet wallet, TokenInfo tokenInfo) { List<BigInteger> result = new ArrayList<>(); result.add(BigInteger.valueOf(NODE_COMMS_ERROR)); try { Function function = erc721TicketBalanceArray(wallet.address); NetworkInfo network = ethereumNetworkRepository.getNetworkByChain(tokenInfo.chainId); List<Type> tokenIds = callSmartContractFunctionArray(function, tokenInfo.address, network, wallet); if (tokenIds != null) { result.clear(); for (Type val : tokenIds) { result.add((BigInteger)val.getValue()); } } } catch (StringIndexOutOfBoundsException e) { //contract call error result.clear(); result.add(BigInteger.valueOf(NODE_COMMS_ERROR)); } return result; } public Observable<TransferFromEventResponse> burnListenerObservable(String contractAddr) { return Observable.fromCallable(() -> { TransferFromEventResponse event = new TransferFromEventResponse(); event._from = ""; event._to = ""; event._indices = null; return event; }); } private <T> T getContractData(NetworkInfo network, String address, Function function, T type) throws Exception { Wallet temp = new Wallet(null); String responseValue = callSmartContractFunction(function, address, network, temp); if (TextUtils.isEmpty(responseValue)) { throw new Exception("Bad contract value"); } else if (responseValue.equals("0x")) { if (type instanceof Boolean) { return (T)Boolean.FALSE; } else { return null; } } List<Type> response = FunctionReturnDecoder.decode( responseValue, function.getOutputParameters()); if (response.size() == 1) { if (type instanceof String) { String value = (String)response.get(0).getValue(); if (value.length() == 0 && responseValue.length() > 2) { value = checkBytesString(responseValue); if (!Utils.isAlNum(value)) value = ""; return (T)value; } } return (T) response.get(0).getValue(); } else { if (type instanceof Boolean) { return (T)Boolean.FALSE; } else { return null; } } } private String checkBytesString(String responseValue) throws Exception { String name = ""; if (responseValue.length() > 0) { //try raw bytes byte[] data = Numeric.hexStringToByteArray(responseValue); //check leading bytes for non-zero if (data[0] != 0) { //truncate zeros int index = data.length - 1; while (data[index] == 0 && index > 0) index--; if (index != (data.length - 1)) { data = Arrays.copyOfRange(data, 0, index + 1); } name = new String(data, "UTF-8"); //now filter out any 'bad' chars name = filterAscii(name); } } return name; } private String filterAscii(String name) { StringBuilder sb = new StringBuilder(); for (char ch : name.toCharArray()) { if (ch >= 0x20 && ch <= 0x7E) //valid ASCII character { sb.append(ch); } } return sb.toString(); } private String getName(String address, NetworkInfo network) throws Exception { Function function = nameOf(); Wallet temp = new Wallet(null); String responseValue = callSmartContractFunction(function, address, network ,temp); if (TextUtils.isEmpty(responseValue)) return null; List<Type> response = FunctionReturnDecoder.decode( responseValue, function.getOutputParameters()); if (response.size() == 1) { String name = (String)response.get(0).getValue(); if (responseValue.length() > 2 && name.length() == 0) { name = checkBytesString(responseValue); } return name; } else { return null; } } private int getDecimals(String address, NetworkInfo network) throws Exception { if (EthereumNetworkRepository.decimalOverride(address, network.chainId) > 0) return EthereumNetworkRepository.decimalOverride(address, network.chainId); Function function = decimalsOf(); Wallet temp = new Wallet(null); String responseValue = callSmartContractFunction(function, address, network, temp); if (TextUtils.isEmpty(responseValue)) return 18; List<Type> response = FunctionReturnDecoder.decode( responseValue, function.getOutputParameters()); if (response.size() == 1) { return ((Uint8) response.get(0)).getValue().intValue(); } else { return 18; } } private static Function balanceOf(String owner) { return new Function( "balanceOf", Collections.singletonList(new Address(owner)), Collections.singletonList(new TypeReference<Uint256>() {})); } private static Function balanceOfArray(String owner) { return new Function( "balanceOf", Collections.singletonList(new Address(owner)), Collections.singletonList(new TypeReference<DynamicArray<Uint256>>() {})); } private static Function erc721TicketBalanceArray(String owner) { return new Function( "getBalances", Collections.singletonList(new Address(owner)), Collections.singletonList(new TypeReference<DynamicArray<Uint256>>() {})); } private static Function nameOf() { return new Function("name", Arrays.<Type>asList(), Arrays.<TypeReference<?>>asList(new TypeReference<Utf8String>() {})); } private static Function supportsInterface(BigInteger value) { return new Function( "supportsInterface", Arrays.<Type>asList(new Bytes4(Numeric.toBytesPadded(value, 4))), Arrays.<TypeReference<?>>asList(new TypeReference<Bool>() {})); } private static Function stringParam(String param) { return new Function(param, Arrays.<Type>asList(), Arrays.<TypeReference<?>>asList(new TypeReference<Utf8String>() {})); } private static Function boolParam(String param) { return new Function(param, Arrays.<Type>asList(), Arrays.<TypeReference<?>>asList(new TypeReference<Bool>() {})); } private static Function stringParam(String param, BigInteger value) { return new Function(param, Arrays.asList(new Uint256(value)), Arrays.<TypeReference<?>>asList(new TypeReference<Utf8String>() {})); } private static Function intParam(String param, BigInteger value) { return new Function(param, Arrays.asList(new Uint256(value)), Arrays.<TypeReference<?>>asList(new TypeReference<Uint256>() {})); } private static Function intParam(String param) { return new Function(param, Arrays.<Type>asList(), Arrays.<TypeReference<?>>asList(new TypeReference<Uint>() {})); } private static Function symbolOf() { return new Function("symbol", Arrays.<Type>asList(), Arrays.<TypeReference<?>>asList(new TypeReference<Utf8String>() {})); } private static Function decimalsOf() { return new Function("decimals", Arrays.<Type>asList(), Arrays.<TypeReference<?>>asList(new TypeReference<Uint8>() {})); } private static Function addrParam(String param) { return new Function(param, Arrays.<Type>asList(), Arrays.<TypeReference<?>>asList(new TypeReference<Address>() {})); } private Function addressFunction(String method, byte[] resultHash) { return new Function( method, Collections.singletonList(new org.web3j.abi.datatypes.generated.Bytes32(resultHash)), Collections.singletonList(new TypeReference<Address>() {})); } private static Function redeemed(BigInteger tokenId) throws NumberFormatException { return new Function( "redeemed", Collections.singletonList(new Uint256(tokenId)), Collections.singletonList(new TypeReference<Bool>() {})); } private List callSmartContractFunctionArray( Function function, String contractAddress, NetworkInfo network, Wallet wallet) { try { String encodedFunction = FunctionEncoder.encode(function); org.web3j.protocol.core.methods.response.EthCall ethCall = getService(network.chainId).ethCall( org.web3j.protocol.core.methods.request.Transaction .createEthCallTransaction(wallet.address, contractAddress, encodedFunction), DefaultBlockParameterName.LATEST).send(); String value = ethCall.getValue(); List<Type> values = FunctionReturnDecoder.decode(value, function.getOutputParameters()); Object o; if (values.isEmpty()) { values = new ArrayList<Type>(); values.add((Type)new Int256(CONTRACT_BALANCE_NULL)); o = (List)values; } else { Type T = values.get(0); o = T.getValue(); } return (List) o; } catch (IOException e) //this call is expected to be interrupted when user switches network or wallet { return null; } catch (Exception e) { e.printStackTrace(); return null; } } private String callSmartContractFunction( Function function, String contractAddress, NetworkInfo network, Wallet wallet) throws Exception { try { String encodedFunction = FunctionEncoder.encode(function); org.web3j.protocol.core.methods.request.Transaction transaction = createEthCallTransaction(wallet.address, contractAddress, encodedFunction); EthCall response = getService(network.chainId).ethCall(transaction, DefaultBlockParameterName.LATEST).send(); return response.getValue(); } catch (InterruptedIOException|UnknownHostException e) { //expected to happen when user switches wallets return "0x"; } } /** * Call smart contract function on custom network contract. This would be used for things like ENS lookup * Currently because it's tied to a mainnet contract address there's no circumstance it would work * outside of mainnet. Users may be confused if their namespace doesn't work, even if they're currently * using testnet. * * @param function * @param contractAddress * @param wallet * @return */ private String callCustomNetSmartContractFunction( Function function, String contractAddress, Wallet wallet, int chainId) { String encodedFunction = FunctionEncoder.encode(function); try { org.web3j.protocol.core.methods.request.Transaction transaction = createEthCallTransaction(wallet.address, contractAddress, encodedFunction); EthCall response = getService(chainId).ethCall(transaction, DefaultBlockParameterName.LATEST).send(); return response.getValue(); } catch (Exception e) { e.printStackTrace(); return null; } } public static byte[] createTokenTransferData(String to, BigInteger tokenAmount) { List<Type> params = Arrays.asList(new Address(to), new Uint256(tokenAmount)); List<TypeReference<?>> returnTypes = Collections.singletonList(new TypeReference<Bool>() {}); Function function = new Function("transfer", params, returnTypes); String encodedFunction = FunctionEncoder.encode(function); return Numeric.hexStringToByteArray(Numeric.cleanHexPrefix(encodedFunction)); } public static byte[] createTicketTransferData(String to, List<BigInteger> tokenIndices, Token token) { Function function = token.getTransferFunction(to, tokenIndices); String encodedFunction = FunctionEncoder.encode(function); return Numeric.hexStringToByteArray(Numeric.cleanHexPrefix(encodedFunction)); } public static byte[] createERC721TransferFunction(String to, Token token, List<BigInteger> tokenId) { Function function = token.getTransferFunction(to, tokenId); String encodedFunction = FunctionEncoder.encode(function); return Numeric.hexStringToByteArray(Numeric.cleanHexPrefix(encodedFunction)); } public static byte[] createTrade(Token token, BigInteger expiry, List<BigInteger> ticketIndices, int v, byte[] r, byte[] s) { Function function = token.getTradeFunction(expiry, ticketIndices, v, r, s); String encodedFunction = FunctionEncoder.encode(function); return Numeric.hexStringToByteArray(Numeric.cleanHexPrefix(encodedFunction)); } public static byte[] createSpawnPassTo(Token token, BigInteger expiry, List<BigInteger> tokenIds, int v, byte[] r, byte[] s, String recipient) { Function function = token.getSpawnPassToFunction(expiry, tokenIds, v, r, s, recipient); String encodedFunction = FunctionEncoder.encode(function); return Numeric.hexStringToByteArray(Numeric.cleanHexPrefix(encodedFunction)); } public static byte[] createDropCurrency(MagicLinkData order, int v, byte[] r, byte[] s, String recipient) { Function function = new Function( "dropCurrency", Arrays.asList(new org.web3j.abi.datatypes.generated.Uint32(order.nonce), new org.web3j.abi.datatypes.generated.Uint32(order.amount), new org.web3j.abi.datatypes.generated.Uint32(order.expiry), new org.web3j.abi.datatypes.generated.Uint8(v), new org.web3j.abi.datatypes.generated.Bytes32(r), new org.web3j.abi.datatypes.generated.Bytes32(s), new org.web3j.abi.datatypes.Address(recipient)), Collections.emptyList()); String encodedFunction = FunctionEncoder.encode(function); return Numeric.hexStringToByteArray(Numeric.cleanHexPrefix(encodedFunction)); } @Override public Single<ContractLocator> getTokenResponse(String address, int chainId, String method) { return Single.fromCallable(() -> { ContractLocator contractLocator = new ContractLocator(INVALID_CONTRACT, chainId); Function function = new Function(method, Arrays.<Type>asList(), Arrays.<TypeReference<?>>asList(new TypeReference<Utf8String>() {})); Wallet temp = new Wallet(null); String responseValue = callCustomNetSmartContractFunction(function, address, temp, chainId); if (responseValue == null) return contractLocator; List<Type> response = FunctionReturnDecoder.decode( responseValue, function.getOutputParameters()); if (response.size() == 1) { return new ContractLocator((String) response.get(0).getValue(), chainId); } else { return contractLocator; } }); } private Single<TokenInfo> setupTokensFromLocal(String address, int chainId) //pass exception up the chain { return Single.fromCallable(() -> { NetworkInfo network = ethereumNetworkRepository.getNetworkByChain(chainId); return new TokenInfo( address, getName(address, network), getContractData(network, address, stringParam("symbol"), ""), getDecimals(address, network), true, chainId); }); } @Override public Single<ContractType> determineCommonType(TokenInfo tokenInfo) { return Single.fromCallable(() -> { ContractType returnType; //could be ERC721, ERC721T, ERC875 or ERC20 //try some interface values NetworkInfo network = ethereumNetworkRepository.getNetworkByChain(tokenInfo.chainId); try { if (getContractData(network, tokenInfo.address, supportsInterface(INTERFACE_BALANCES_721_TICKET), Boolean.TRUE)) returnType = ContractType.ERC721_TICKET; else if (getContractData(network, tokenInfo.address, supportsInterface(INTERFACE_OFFICIAL_ERC721), Boolean.TRUE)) returnType = ContractType.ERC721; else if (getContractData(network, tokenInfo.address, supportsInterface(INTERFACE_CRYPTOKITTIES), Boolean.TRUE)) returnType = ContractType.ERC721_LEGACY; else if (getContractData(network, tokenInfo.address, supportsInterface(INTERFACE_OLD_ERC721), Boolean.TRUE)) returnType = ContractType.ERC721_LEGACY; else returnType = ContractType.OTHER; } catch (Exception e) { returnType = ContractType.OTHER; } if (returnType == ContractType.OTHER) { Boolean isERC875; String responseValue; try { isERC875 = getContractData(network, tokenInfo.address, boolParam("isStormBirdContract"), Boolean.TRUE); //Use old isStormbird as another datum point } catch (Exception e) { isERC875 = false; } try { responseValue = callSmartContractFunction(balanceOf(ZERO_ADDRESS), tokenInfo.address, network, new Wallet(ZERO_ADDRESS)); } catch (Exception e) { responseValue = ""; } returnType = findContractTypeFromResponse(responseValue, isERC875); } return returnType; }); } private ContractType findContractTypeFromResponse(String balanceResponse, Boolean isERC875) throws Exception { ContractType returnType = ContractType.OTHER; int responseLength = balanceResponse.length(); if (isERC875 || (responseLength > 66)) { returnType = ContractType.ERC875; } else if (balanceResponse.length() == 66) //expected biginteger size in hex + 0x { returnType = ContractType.ERC20; } return returnType; } @Override public Single<Boolean> fetchIsRedeemed(Token token, BigInteger tokenId) { return Single.fromCallable(() -> { NetworkInfo networkInfo = ethereumNetworkRepository.getNetworkByChain(token.tokenInfo.chainId); return getContractData(networkInfo, token.tokenInfo.address, redeemed(tokenId), Boolean.TRUE); }); } @Override public Disposable updateBlockRead(Token token, Wallet wallet) { return localSource.storeBlockRead(token, wallet); } @Override public Disposable addImageUrl(int networkId, String address, String imageUrl) { return localSource.storeTokenUrl(networkId, address, imageUrl); } public static Web3j getWeb3jService(int chainId) { OkHttpClient okClient = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .retryOnConnectionFailure(false) .build(); AWHttpService publicNodeService = new AWHttpService(EthereumNetworkRepository.getNodeURLByNetworkId (chainId), EthereumNetworkRepository.getSecondaryNodeURL(chainId), okClient, false); EthereumNetworkRepository.addRequiredCredentials(chainId, publicNodeService); return Web3j.build(publicNodeService); } }