From 967725649317787fc03361126cdbb2f9327535f0 Mon Sep 17 00:00:00 2001 From: James Brown Date: Sat, 10 Feb 2024 14:06:24 +1100 Subject: [PATCH] Improve gas calc (#3356) * update to use Infura gas provider if available. * Ensure up to date gas information has been received before sending transactions. --- app/build.gradle | 2 +- .../app/entity/ActionSheetInterface.java | 13 ++ .../app/entity/GasPriceSpread.java | 62 +++++++++ .../app/repository/AWRealmMigration.java | 12 ++ .../app/repository/EthereumNetworkBase.java | 28 +++- .../app/repository/HttpServiceHelper.java | 5 + .../repository/TransactionsRealmCache.java | 2 + .../app/repository/entity/Realm1559Gas.java | 7 +- .../app/service/BlockNativeGasAPI.java | 6 +- .../alphawallet/app/service/GasService.java | 19 +-- .../alphawallet/app/service/InfuraGasAPI.java | 126 ++++++++++++++++++ .../ui/widget/entity/GasWidgetInterface.java | 6 + .../app/widget/ActionSheetDialog.java | 30 ++++- .../com/alphawallet/app/widget/GasWidget.java | 55 ++++++-- .../alphawallet/app/widget/GasWidget2.java | 56 ++++++-- app/src/main/res/layout/item_gas_settings.xml | 17 ++- 16 files changed, 397 insertions(+), 49 deletions(-) create mode 100644 app/src/main/java/com/alphawallet/app/service/InfuraGasAPI.java diff --git a/app/build.gradle b/app/build.gradle index f887186f1e..e72e194b1f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -90,7 +90,7 @@ android { def DEFUALT_WALLETCONNECT_PROJECT_ID = "\"40c6071febfd93f4fe485c232a8a4cd9\"" def DEFAULT_AURORA_API_KEY = "\"HFDDY5BNKGXBB82DE2G8S64C3C41B76PYI\""; //Put your Aurorascan.dev API key here - this one will rate limit as it is common - buildConfigField 'int', 'DB_VERSION', '53' + buildConfigField 'int', 'DB_VERSION', '54' buildConfigField "String", XInfuraAPI, DEFAULT_INFURA_API_KEY buildConfigField "String", "WALLETCONNECT_PROJECT_ID", DEFUALT_WALLETCONNECT_PROJECT_ID diff --git a/app/src/main/java/com/alphawallet/app/entity/ActionSheetInterface.java b/app/src/main/java/com/alphawallet/app/entity/ActionSheetInterface.java index a2ad3a8d01..d6b85a6c47 100644 --- a/app/src/main/java/com/alphawallet/app/entity/ActionSheetInterface.java +++ b/app/src/main/java/com/alphawallet/app/entity/ActionSheetInterface.java @@ -1,6 +1,9 @@ package com.alphawallet.app.entity; +import android.content.Intent; + import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultLauncher; import com.alphawallet.app.web3.entity.Web3Transaction; @@ -58,4 +61,14 @@ default void setSignOnly() default void setCurrentGasIndex(ActivityResult result) { } + + default ActivityResultLauncher gasSelectLauncher() + { + return null; + } + + default void gasEstimateReady() + { + + } } diff --git a/app/src/main/java/com/alphawallet/app/entity/GasPriceSpread.java b/app/src/main/java/com/alphawallet/app/entity/GasPriceSpread.java index ef9cd260b4..29d896eb64 100644 --- a/app/src/main/java/com/alphawallet/app/entity/GasPriceSpread.java +++ b/app/src/main/java/com/alphawallet/app/entity/GasPriceSpread.java @@ -19,6 +19,8 @@ import javax.annotation.Nullable; +import timber.log.Timber; + /** * Created by JB on 20/01/2022. */ @@ -165,6 +167,66 @@ public GasPriceSpread(Context ctx, BigInteger gasPrice) hasLockedGas = false; } + public GasPriceSpread(Context ctx, String apiReturn) //ChainId is unused but we need to disambiguate from etherscan API return + { + this.timeStamp = System.currentTimeMillis(); + BigDecimal rBaseFee = BigDecimal.ZERO; + hasLockedGas = false; + + try + { + JSONObject result = new JSONObject(apiReturn); + if (result.has("estimatedBaseFee")) + { + rBaseFee = new BigDecimal(result.getString("estimatedBaseFee")); + } + + EIP1559FeeOracleResult low = readFeeResult(result, "low", rBaseFee); + EIP1559FeeOracleResult medium = readFeeResult(result, "medium", rBaseFee); + EIP1559FeeOracleResult high = readFeeResult(result, "high", rBaseFee); + + if (low == null || medium == null || high == null) + { + return; + } + + BigInteger rapidPriorityFee = (new BigDecimal(high.priorityFee)).multiply(BigDecimal.valueOf(1.2)).toBigInteger(); + EIP1559FeeOracleResult rapid = new EIP1559FeeOracleResult(high.maxFeePerGas, rapidPriorityFee, gweiToWei(rBaseFee)); + + fees.put(TXSpeed.SLOW, new GasSpeed(ctx.getString(R.string.speed_slow), SLOW_SECONDS, low)); + fees.put(TXSpeed.STANDARD, new GasSpeed(ctx.getString(R.string.speed_average), STANDARD_SECONDS, medium)); + fees.put(TXSpeed.FAST, new GasSpeed(ctx.getString(R.string.speed_fast), FAST_SECONDS, high)); + fees.put(TXSpeed.RAPID, new GasSpeed(ctx.getString(R.string.speed_rapid), RAPID_SECONDS, rapid)); + } + catch (JSONException e) + { + // + } + } + + private EIP1559FeeOracleResult readFeeResult(JSONObject result, String speed, BigDecimal rBaseFee) + { + EIP1559FeeOracleResult oracleResult = null; + + try + { + if (result.has(speed)) + { + JSONObject thisSpeed = result.getJSONObject(speed); + BigDecimal maxFeePerGas = new BigDecimal(thisSpeed.getString("suggestedMaxFeePerGas")); + BigDecimal priorityFee = new BigDecimal(thisSpeed.getString("suggestedMaxPriorityFeePerGas")); + oracleResult = new EIP1559FeeOracleResult(gweiToWei(maxFeePerGas), gweiToWei(priorityFee), gweiToWei(rBaseFee)); + } + } + catch (Exception e) + { + Timber.e("Infura GasOracle read failing; please adjust your Infura API settings."); + } + + return oracleResult; + } + + // For etherscan return public GasPriceSpread(String apiReturn) { this.timeStamp = System.currentTimeMillis(); diff --git a/app/src/main/java/com/alphawallet/app/repository/AWRealmMigration.java b/app/src/main/java/com/alphawallet/app/repository/AWRealmMigration.java index 8d537a5a0c..9a2c0076ff 100644 --- a/app/src/main/java/com/alphawallet/app/repository/AWRealmMigration.java +++ b/app/src/main/java/com/alphawallet/app/repository/AWRealmMigration.java @@ -490,6 +490,18 @@ else if (!realmData.hasField("attestation")) oldVersion = 53; } + + if (oldVersion == 53) + { + RealmObjectSchema realmData = schema.get("Realm1559Gas"); + if (realmData != null) schema.remove("Realm1559Gas"); + schema.create("Realm1559Gas") + .addField("chainId", long.class, FieldAttribute.PRIMARY_KEY) + .addField("timeStamp", long.class) + .addField("resultData", String.class); + + oldVersion = 54; + } } @Override 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 c6d4e66a89..2defcda9b0 100644 --- a/app/src/main/java/com/alphawallet/app/repository/EthereumNetworkBase.java +++ b/app/src/main/java/com/alphawallet/app/repository/EthereumNetworkBase.java @@ -119,6 +119,7 @@ public abstract class EthereumNetworkBase implements EthereumNetworkRepositoryTy private static final KeyProvider keyProvider = KeyProviderFactory.get(); public static final boolean usesProductionKey = !keyProvider.getInfuraKey().equals(DEFAULT_INFURA_KEY); + private static final String INFURA_GAS_API = "https://gas.api.infura.io/networks/CHAIN_ID/suggestedGasFees"; public static final String FREE_MAINNET_RPC_URL = "https://rpc.ankr.com/eth"; public static final String FREE_POLYGON_RPC_URL = "https://polygon-rpc.com"; @@ -493,7 +494,9 @@ public static boolean isInfura(String rpcServerUrl) //Add it to this list here if so. Note that so far, all gas oracles follow the same format: // + GAS_API //If the gas oracle you're adding doesn't follow this spec then you'll have to change the getGasOracle method - private static final List hasGasOracleAPI = Arrays.asList(MAINNET_ID, HECO_ID, BINANCE_MAIN_ID, POLYGON_ID); + private static final List hasGasOracleAPI = Arrays.asList(MAINNET_ID, POLYGON_ID, ARBITRUM_MAIN_ID, AVALANCHE_ID, BINANCE_MAIN_ID, CRONOS_MAIN_ID, GOERLI_ID, + SEPOLIA_TESTNET_ID, FANTOM_ID, LINEA_ID, OPTIMISTIC_MAIN_ID, POLYGON_TEST_ID); + private static final List hasEtherscanGasOracleAPI = Arrays.asList(MAINNET_ID, HECO_ID, BINANCE_MAIN_ID, POLYGON_ID); private static final List hasBlockNativeGasOracleAPI = Arrays.asList(MAINNET_ID, POLYGON_ID); //These chains don't allow custom gas private static final List hasLockedGas = Arrays.asList(KLAYTN_ID, KLAYTN_BAOBAB_ID); @@ -508,11 +511,24 @@ public static boolean isInfura(String rpcServerUrl) } }; + public static String getEtherscanGasOracle(long chainId) + { + if (hasEtherscanGasOracleAPI.contains(chainId) && networkMap.indexOfKey(chainId) >= 0) + { + return networkMap.get(chainId).etherscanAPI + GAS_API; + } + else + { + return ""; + } + } + public static String getGasOracle(long chainId) { if (hasGasOracleAPI.contains(chainId) && networkMap.indexOfKey(chainId) >= 0) { - return networkMap.get(chainId).etherscanAPI + GAS_API; + //construct API route: + return INFURA_GAS_API.replace("CHAIN_ID", Long.toString(chainId)); } else { @@ -603,7 +619,9 @@ private static void setBatchProcessingLimits() public static int getBatchProcessingLimit(long chainId) { if (batchProcessingLimitMap.size() == 0) setBatchProcessingLimits(); //If batch limits not set, init them and proceed - return batchProcessingLimitMap.get(chainId, 0); //default to zero / no batching + { + return batchProcessingLimitMap.get(chainId, 0); //default to zero / no batching + } } @Override @@ -861,8 +879,6 @@ public static NetworkInfo getNetwork(long chainId) return networkMap.get(chainId); } - // fetches the last transaction nonce; if it's identical to the last used one then increment by one - // to ensure we don't get transaction replacement @Override public Single getLastTransactionNonce(Web3j web3j, String walletAddress) { @@ -871,7 +887,7 @@ public Single getLastTransactionNonce(Web3j web3j, String walletAddr try { EthGetTransactionCount ethGetTransactionCount = web3j - .ethGetTransactionCount(walletAddress, DefaultBlockParameterName.LATEST) + .ethGetTransactionCount(walletAddress, DefaultBlockParameterName.PENDING) .send(); return ethGetTransactionCount.getTransactionCount(); } diff --git a/app/src/main/java/com/alphawallet/app/repository/HttpServiceHelper.java b/app/src/main/java/com/alphawallet/app/repository/HttpServiceHelper.java index 7e32cd0d84..2e8fb1de57 100644 --- a/app/src/main/java/com/alphawallet/app/repository/HttpServiceHelper.java +++ b/app/src/main/java/com/alphawallet/app/repository/HttpServiceHelper.java @@ -38,4 +38,9 @@ else if (isInfura && usesProductionKey && !TextUtils.isEmpty(infuraKey)) service.addHeader("Authorization", "Basic " + infuraKey); } } + + public static void addInfuraGasCredentials(Request.Builder service, String infuraSecret) + { + service.addHeader("Authorization", "Basic " + infuraSecret); + } } diff --git a/app/src/main/java/com/alphawallet/app/repository/TransactionsRealmCache.java b/app/src/main/java/com/alphawallet/app/repository/TransactionsRealmCache.java index a65504ebf8..42e441381c 100644 --- a/app/src/main/java/com/alphawallet/app/repository/TransactionsRealmCache.java +++ b/app/src/main/java/com/alphawallet/app/repository/TransactionsRealmCache.java @@ -10,6 +10,7 @@ import com.alphawallet.app.entity.Transaction; import com.alphawallet.app.entity.TransactionMeta; import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.repository.entity.Realm1559Gas; import com.alphawallet.app.repository.entity.RealmAuxData; import com.alphawallet.app.repository.entity.RealmNFTAsset; import com.alphawallet.app.repository.entity.RealmToken; @@ -295,6 +296,7 @@ public Single deleteAllForWallet(String currentAddress) r.where(RealmAuxData.class).findAll().deleteAllFromRealm(); r.where(RealmNFTAsset.class).findAll().deleteAllFromRealm(); r.where(RealmTransfer.class).findAll().deleteAllFromRealm(); + r.where(Realm1559Gas.class).findAll().deleteAllFromRealm(); }); instance.refresh(); } diff --git a/app/src/main/java/com/alphawallet/app/repository/entity/Realm1559Gas.java b/app/src/main/java/com/alphawallet/app/repository/entity/Realm1559Gas.java index 50c73026c7..3010d88548 100644 --- a/app/src/main/java/com/alphawallet/app/repository/entity/Realm1559Gas.java +++ b/app/src/main/java/com/alphawallet/app/repository/entity/Realm1559Gas.java @@ -24,7 +24,12 @@ public class Realm1559Gas extends RealmObject public Map getResult() { Type entry = new TypeToken>() {}.getType(); - return new Gson().fromJson(resultData, entry); + return new Gson().fromJson(getResultData(), entry); + } + + public String getResultData() + { + return resultData; } public void setResultData(Map result, long ts) diff --git a/app/src/main/java/com/alphawallet/app/service/BlockNativeGasAPI.java b/app/src/main/java/com/alphawallet/app/service/BlockNativeGasAPI.java index dacbde0e63..23ecec1936 100644 --- a/app/src/main/java/com/alphawallet/app/service/BlockNativeGasAPI.java +++ b/app/src/main/java/com/alphawallet/app/service/BlockNativeGasAPI.java @@ -55,8 +55,12 @@ private Request buildRequest(String api) return requestB.build(); } - public Single> fetchGasEstimates(long chainId) + public Single> get1559GasEstimates(Map result, long chainId) { + if (result.size() > 0) + { + return Single.fromCallable(() -> result); + } String oracleAPI = EthereumNetworkBase.getBlockNativeOracle(chainId); return Single.fromCallable(() -> buildOracleResult(executeRequest(oracleAPI))); // any kind of error results in blank mapping, // if blank, fall back to calculation method diff --git a/app/src/main/java/com/alphawallet/app/service/GasService.java b/app/src/main/java/com/alphawallet/app/service/GasService.java index 106e5254c6..372d9d8605 100644 --- a/app/src/main/java/com/alphawallet/app/service/GasService.java +++ b/app/src/main/java/com/alphawallet/app/service/GasService.java @@ -30,7 +30,6 @@ import com.alphawallet.app.repository.entity.Realm1559Gas; import com.alphawallet.app.repository.entity.RealmGasSpread; import com.alphawallet.app.web3.entity.Web3Transaction; -import org.web3j.utils.Numeric; import com.google.gson.Gson; import org.jetbrains.annotations.Nullable; @@ -41,6 +40,7 @@ import org.web3j.protocol.core.methods.response.EthGasPrice; import org.web3j.protocol.http.HttpService; import org.web3j.tx.gas.ContractGasProvider; +import org.web3j.utils.Numeric; import java.math.BigInteger; import java.util.Map; @@ -82,6 +82,7 @@ public class GasService implements ContractGasProvider private final String ETHERSCAN_API_KEY; private final String POLYGONSCAN_API_KEY; private boolean keyFail; + @Nullable private Disposable gasFetchDisposable; @@ -186,7 +187,7 @@ private boolean nodeFetchValid() private Single updateCurrentGasPrices() { - String gasOracleAPI = EthereumNetworkRepository.getGasOracle(currentChainId); + String gasOracleAPI = EthereumNetworkRepository.getEtherscanGasOracle(currentChainId); if (!TextUtils.isEmpty(gasOracleAPI)) { if (!keyFail && gasOracleAPI.contains("etherscan")) gasOracleAPI += ETHERSCAN_API_KEY; @@ -303,13 +304,14 @@ private boolean updateEIP1559Realm(final Map re Realm1559Gas rgs = r.where(Realm1559Gas.class) .equalTo("chainId", chainId) .findFirst(); + if (rgs == null) { rgs = r.createObject(Realm1559Gas.class, chainId); } rgs.setResultData(result, System.currentTimeMillis()); - r.insertOrUpdate(rgs); + //r.insertOrUpdate(rgs); }); } catch (Exception e) @@ -325,11 +327,11 @@ public Single calculateGasEstimate(byte[] transactionBytes, long ch { updateChainId(chainId); return useNodeEstimate(true) - .flatMap(com -> calculateGasEstimateInternal(transactionBytes, chainId, toAddress, amount, wallet, defaultLimit)); + .flatMap(com -> calculateGasEstimateInternal(transactionBytes, chainId, toAddress, amount, wallet, defaultLimit)); } public Single calculateGasEstimateInternal(byte[] transactionBytes, long chainId, String toAddress, - BigInteger amount, Wallet wallet, final BigInteger defaultLimit) + BigInteger amount, Wallet wallet, final BigInteger defaultLimit) { String txData = ""; if (transactionBytes != null && transactionBytes.length > 0) @@ -387,7 +389,7 @@ private Single handleOutOfGasError(@NonNull EthEstimateGas estim { if (!estimate.hasError() || chainId != 1) return Single.fromCallable(() -> estimate); else return networkRepository.getLastTransactionNonce(web3j, WHALE_ACCOUNT) - .flatMap(nonce -> ethEstimateGas(chainId, WHALE_ACCOUNT, nonce, toAddress, amount, finalTxData)); + .flatMap(nonce -> ethEstimateGas(chainId, WHALE_ACCOUNT, nonce, toAddress, amount, finalTxData)); } private BigInteger getLowGasPrice() @@ -420,8 +422,9 @@ private Single ethEstimateGas(long chainId, String fromAddress, private Single> getEIP1559FeeStructure() { - return BlockNativeGasAPI.get(httpClient).fetchGasEstimates(currentChainId) - .flatMap(this::useCalculationIfRequired); //if interface doesn't have blocknative API then use calculation method + return InfuraGasAPI.get1559GasEstimates(currentChainId, httpClient) + .flatMap(result -> BlockNativeGasAPI.get(httpClient).get1559GasEstimates(result, currentChainId)) + .flatMap(this::useCalculationIfRequired); //if interface doesn't have blocknative API then use calculation method } private Single> useCalculationIfRequired(Map resultMap) diff --git a/app/src/main/java/com/alphawallet/app/service/InfuraGasAPI.java b/app/src/main/java/com/alphawallet/app/service/InfuraGasAPI.java new file mode 100644 index 0000000000..3de3e6ad19 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/service/InfuraGasAPI.java @@ -0,0 +1,126 @@ +package com.alphawallet.app.service; + +import static com.alphawallet.app.util.BalanceUtils.gweiToWei; + +import android.text.TextUtils; + +import com.alphawallet.app.entity.EIP1559FeeOracleResult; +import com.alphawallet.app.repository.EthereumNetworkRepository; +import com.alphawallet.app.repository.HttpServiceHelper; +import com.alphawallet.app.repository.KeyProviderFactory; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.HashMap; +import java.util.Map; + +import io.reactivex.Single; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import timber.log.Timber; + +public class InfuraGasAPI +{ + public static Single> get1559GasEstimates(final long chainId, final OkHttpClient httpClient) + { + return Single.fromCallable(() -> { + Map gasMap = new HashMap<>(); + + //ensure we have correct Infura details + String gasOracleAPI = EthereumNetworkRepository.getGasOracle(chainId); + String infuraKey = KeyProviderFactory.get().getInfuraKey(); + String infuraSecret = KeyProviderFactory.get().getInfuraSecret(); + + if (TextUtils.isEmpty(gasOracleAPI) || TextUtils.isEmpty(infuraKey) || TextUtils.isEmpty(infuraSecret)) + { + //require Infura key with API and secret to operate the gas API + return gasMap; + } + + final Request.Builder rqBuilder = new Request.Builder() + .url(gasOracleAPI) + .get(); + + HttpServiceHelper.addInfuraGasCredentials(rqBuilder, KeyProviderFactory.get().getInfuraSecret()); + + try (Response response = httpClient.newCall(rqBuilder.build()).execute()) + { + if (response.code() / 200 == 1) + { + String result = response.body() + .string(); + gasMap = readGasMap(result); + } + } + catch (Exception e) + { + Timber.w(e); + } + + return gasMap; + }); + } + + private static Map readGasMap(String apiReturn) + { + Map gasMap = new HashMap<>(); + try + { + BigDecimal rBaseFee = BigDecimal.ZERO; + JSONObject result = new JSONObject(apiReturn); + if (result.has("estimatedBaseFee")) + { + rBaseFee = new BigDecimal(result.getString("estimatedBaseFee")); + } + + EIP1559FeeOracleResult low = readFeeResult(result, "low", rBaseFee); + EIP1559FeeOracleResult medium = readFeeResult(result, "medium", rBaseFee); + EIP1559FeeOracleResult high = readFeeResult(result, "high", rBaseFee); + + if (low == null || medium == null || high == null) + { + return gasMap; + } + + BigInteger rapidPriorityFee = (new BigDecimal(high.priorityFee)).multiply(BigDecimal.valueOf(1.2)).toBigInteger(); + EIP1559FeeOracleResult rapid = new EIP1559FeeOracleResult(high.maxFeePerGas, rapidPriorityFee, gweiToWei(rBaseFee)); + + gasMap.put(0, rapid); + gasMap.put(1, high); + gasMap.put(2, medium); + gasMap.put(3, low); + } + catch (JSONException e) + { + // + } + + return gasMap; + } + + private static EIP1559FeeOracleResult readFeeResult(JSONObject result, String speed, BigDecimal rBaseFee) + { + EIP1559FeeOracleResult oracleResult = null; + + try + { + if (result.has(speed)) + { + JSONObject thisSpeed = result.getJSONObject(speed); + BigDecimal maxFeePerGas = new BigDecimal(thisSpeed.getString("suggestedMaxFeePerGas")); + BigDecimal priorityFee = new BigDecimal(thisSpeed.getString("suggestedMaxPriorityFeePerGas")); + oracleResult = new EIP1559FeeOracleResult(gweiToWei(maxFeePerGas), gweiToWei(priorityFee), gweiToWei(rBaseFee)); + } + } + catch (Exception e) + { + Timber.e("Infura GasOracle read failing; please adjust your Infura API settings."); + } + + return oracleResult; + } +} diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/entity/GasWidgetInterface.java b/app/src/main/java/com/alphawallet/app/ui/widget/entity/GasWidgetInterface.java index 424a813b00..27b30c6a11 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/entity/GasWidgetInterface.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/entity/GasWidgetInterface.java @@ -28,4 +28,10 @@ public interface GasWidgetInterface void setupResendSettings(ActionSheetMode mode, BigInteger gasPrice); void setCurrentGasIndex(int gasSelectionIndex, BigInteger maxFeePerGas, BigInteger maxPriorityFee, BigDecimal customGasLimit, long expectedTxTime, long customNonce); long getExpectedTransactionTime(); + default boolean gasPriceReady(long gasEstimateTime) + { + return gasEstimateTime > (System.currentTimeMillis() - 30 * 1000); + } + + boolean gasPriceReady(); } diff --git a/app/src/main/java/com/alphawallet/app/widget/ActionSheetDialog.java b/app/src/main/java/com/alphawallet/app/widget/ActionSheetDialog.java index 4f24942d05..0d6f60a90c 100644 --- a/app/src/main/java/com/alphawallet/app/widget/ActionSheetDialog.java +++ b/app/src/main/java/com/alphawallet/app/widget/ActionSheetDialog.java @@ -10,6 +10,7 @@ import android.widget.Toast; import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.NonNull; import androidx.preference.PreferenceManager; @@ -89,7 +90,6 @@ public class ActionSheetDialog extends ActionSheet implements StandardFunctionIn private boolean use1559Transactions = false; private Transaction transaction; private final WalletType walletType; - private Disposable disposable; public ActionSheetDialog(@NonNull Activity activity, Web3Transaction tx, Token t, String destName, String destAddress, TokensService ts, @@ -329,14 +329,14 @@ private GasWidgetInterface setupGasWidget() if (use1559Transactions) { - gasWidget.setupWidget(tokensService, token, candidateTransaction, actionSheetCallback.gasSelectLauncher()); + gasWidget.setupWidget(tokensService, token, candidateTransaction, this); return gasWidget; } else { gasWidget.setVisibility(View.GONE); gasWidgetLegacy.setVisibility(View.VISIBLE); - gasWidgetLegacy.setupWidget(tokensService, token, candidateTransaction, this, actionSheetCallback.gasSelectLauncher()); + gasWidgetLegacy.setupWidget(tokensService, token, candidateTransaction, this, this); return gasWidgetLegacy; } } @@ -434,6 +434,13 @@ public void updateAmount() @SuppressWarnings("checkstyle:MissingSwitchDefault") public void handleClick(String action, int id) { + //first ensure gas estimate is up to date + if (gasEstimateOutOfDate()) + { + functionBar.setPrimaryButtonWaiting(); + return; + } + if (walletType == WalletType.HARDWARE) { //TODO: Hardware - Maybe flick a toast to tell user to apply card @@ -477,6 +484,23 @@ public void handleClick(String action, int id) } } + @Override + public void gasEstimateReady() + { + functionBar.setPrimaryButtonEnabled(true); + } + + @Override + public ActivityResultLauncher gasSelectLauncher() + { + return actionSheetCallback.gasSelectLauncher(); + } + + private boolean gasEstimateOutOfDate() + { + return !gasWidget.gasPriceReady(); + } + private BigDecimal getTransactionAmount() { BigDecimal txAmount; diff --git a/app/src/main/java/com/alphawallet/app/widget/GasWidget.java b/app/src/main/java/com/alphawallet/app/widget/GasWidget.java index 8d7fef7d22..1b0d57d372 100644 --- a/app/src/main/java/com/alphawallet/app/widget/GasWidget.java +++ b/app/src/main/java/com/alphawallet/app/widget/GasWidget.java @@ -11,11 +11,10 @@ import android.widget.LinearLayout; import android.widget.TextView; -import androidx.activity.result.ActivityResultLauncher; - import com.alphawallet.app.BuildConfig; import com.alphawallet.app.C; import com.alphawallet.app.R; +import com.alphawallet.app.entity.ActionSheetInterface; import com.alphawallet.app.entity.GasPriceSpread; import com.alphawallet.app.entity.StandardFunctionInterface; import com.alphawallet.app.entity.TXSpeed; @@ -70,6 +69,8 @@ public class GasWidget extends LinearLayout implements Runnable, GasWidgetInterf private long customNonce = -1; private boolean isSendingAll; private BigInteger resendGasPrice = BigInteger.ZERO; + long gasEstimateTime = 0; + private ActionSheetInterface actionSheetInterface; public GasWidget(Context ctx, AttributeSet attrs) { @@ -85,7 +86,7 @@ public GasWidget(Context ctx, AttributeSet attrs) //For legacy transaction, either we are sending all or the chain doesn't support EIP1559 //Since these chains are not so well used, we will compromise and send at the standard gas rate //That is - not allow selection of gas price - public void setupWidget(TokensService svs, Token t, Web3Transaction tx, StandardFunctionInterface sfi, ActivityResultLauncher gasSelectLauncher) + public void setupWidget(TokensService svs, Token t, Web3Transaction tx, StandardFunctionInterface sfi, ActionSheetInterface actionSheetIf) { tokensService = svs; token = t; @@ -96,6 +97,7 @@ public void setupWidget(TokensService svs, Token t, Web3Transaction tx, Standard isSendingAll = isSendingAll(tx); initialGasPrice = tx.gasPrice; customNonce = tx.nonce; + actionSheetInterface = actionSheetIf; if (tx.gasLimit.equals(BigInteger.ZERO)) //dapp didn't specify a limit, use default limits until node returns an estimate (see setGasEstimate()) { @@ -133,22 +135,27 @@ public void setupWidget(TokensService svs, Token t, Web3Transaction tx, Standard intent.putExtra(C.EXTRA_NONCE, customNonce); intent.putExtra(C.EXTRA_1559_TX, false); intent.putExtra(C.EXTRA_MIN_GAS_PRICE, resendGasPrice.longValue()); - gasSelectLauncher.launch(intent); + actionSheetInterface.gasSelectLauncher().launch(intent); }); } } private void setupGasSpeeds(Web3Transaction w3tx) { - RealmGasSpread rgs = getGasQuery().findFirst(); - if (rgs != null) - { - initGasSpeeds(rgs); - } - else + try (Realm realm = tokensService.getTickerRealmInstance()) { - // Couldn't get current gas. Add a blank custom gas speed node - gasSpread = new GasPriceSpread(getContext(), w3tx.gasPrice); + RealmGasSpread gasReturn = realm.where(RealmGasSpread.class) + .equalTo("chainId", token.tokenInfo.chainId).findFirst(); + + if (gasReturn != null) + { + initGasSpeeds(gasReturn); + } + else + { + // Couldn't get current gas. Add a blank custom gas speed node + gasSpread = new GasPriceSpread(getContext(), w3tx.gasPrice); + } } if (w3tx.gasPrice.compareTo(BigInteger.ZERO) > 0) @@ -301,6 +308,8 @@ private void initGasSpeeds(RealmGasSpread rgs) TextView editTxt = findViewById(R.id.edit_text); + gasEstimateTime = rgs.getTimeStamp(); + if (gasSpread.hasLockedGas() && editTxt.getVisibility() == View.VISIBLE) { findViewById(R.id.edit_text).setVisibility(View.GONE); @@ -383,6 +392,16 @@ public void run() } checkSufficientGas(); manageWarnings(); + + if (gasPriceReady(gasEstimateTime)) + { + actionSheetInterface.gasEstimateReady(); + setGasReadyStatus(true); + } + else + { + setGasReadyStatus(false); + } } @Override @@ -399,6 +418,12 @@ public BigInteger getGasPrice(BigInteger defaultPrice) } } + @Override + public boolean gasPriceReady() + { + return gasPriceReady(gasEstimateTime); + } + @Override public BigInteger getValue() { @@ -447,6 +472,12 @@ else if (dGasPrice > 2.0 * upperBound) } } + private void setGasReadyStatus(boolean ready) + { + findViewById(R.id.view_spacer).setVisibility(ready ? View.VISIBLE : View.GONE); + findViewById(R.id.gas_fetch_wait).setVisibility(ready ? View.GONE : View.VISIBLE); + } + private void showCustomSpeedWarning(boolean high) { if (currentGasSpeedIndex != TXSpeed.CUSTOM) diff --git a/app/src/main/java/com/alphawallet/app/widget/GasWidget2.java b/app/src/main/java/com/alphawallet/app/widget/GasWidget2.java index fc5c313f73..477326a40c 100644 --- a/app/src/main/java/com/alphawallet/app/widget/GasWidget2.java +++ b/app/src/main/java/com/alphawallet/app/widget/GasWidget2.java @@ -11,11 +11,10 @@ import android.widget.LinearLayout; import android.widget.TextView; -import androidx.activity.result.ActivityResultLauncher; - import com.alphawallet.app.BuildConfig; import com.alphawallet.app.C; import com.alphawallet.app.R; +import com.alphawallet.app.entity.ActionSheetInterface; import com.alphawallet.app.entity.GasPriceSpread; import com.alphawallet.app.entity.TXSpeed; import com.alphawallet.app.entity.analytics.ActionSheetMode; @@ -66,6 +65,8 @@ public class GasWidget2 extends LinearLayout implements Runnable, GasWidgetInter private TXSpeed currentGasSpeedIndex = TXSpeed.STANDARD; private long customNonce = -1; private BigInteger resendGasPrice = BigInteger.ZERO; + private long gasEstimateTime = 0; + private ActionSheetInterface actionSheetInterface; //Need to track user selected gas limit & calculated gas limit //At initial setup, we have the limit from the tx or default: presetGasLimit @@ -85,7 +86,7 @@ public GasWidget2(Context ctx, AttributeSet attrs) } // Called once from ActionSheet constructor - public void setupWidget(TokensService svs, Token t, Web3Transaction tx, ActivityResultLauncher gasSelectLauncher) + public void setupWidget(TokensService svs, Token t, Web3Transaction tx, ActionSheetInterface actionSheetIf) { tokensService = svs; token = t; @@ -93,6 +94,7 @@ public void setupWidget(TokensService svs, Token t, Web3Transaction tx, Activity adjustedValue = tx.value; initialGasPrice = tx.gasPrice; customNonce = tx.nonce; + actionSheetInterface = actionSheetIf; if (tx.gasLimit.equals(BigInteger.ZERO)) //dapp didn't specify a limit, use default limits until node returns an estimate (see setGasEstimate()) { @@ -124,7 +126,7 @@ public void setupWidget(TokensService svs, Token t, Web3Transaction tx, Activity intent.putExtra(C.EXTRA_NONCE, customNonce); intent.putExtra(C.EXTRA_1559_TX, true); intent.putExtra(C.EXTRA_MIN_GAS_PRICE, resendGasPrice.longValue()); - gasSelectLauncher.launch(intent); + actionSheetInterface.gasSelectLauncher().launch(intent); }); } } @@ -132,15 +134,20 @@ public void setupWidget(TokensService svs, Token t, Web3Transaction tx, Activity //set custom fee if specified by tx feed private void setupGasSpeeds(Web3Transaction w3tx) { - Realm1559Gas getGas = getGasQuery2().findFirst(); - if (getGas != null) - { - initGasSpeeds(getGas); - } - else + try (Realm realm = tokensService.getTickerRealmInstance()) { - // Couldn't get current gas. Add a blank custom gas speed node - gasSpread = new GasPriceSpread(getContext(), w3tx.maxFeePerGas, w3tx.maxPriorityFeePerGas); + Realm1559Gas gasReturn = realm.where(Realm1559Gas.class) + .equalTo("chainId", token.tokenInfo.chainId).findFirst(); + + if (gasReturn != null) + { + initGasSpeeds(gasReturn); + } + else + { + // Couldn't get current gas. Add a blank custom gas speed node + gasSpread = new GasPriceSpread(getContext(), w3tx.maxFeePerGas, w3tx.maxPriorityFeePerGas); + } } if (w3tx.maxFeePerGas.compareTo(BigInteger.ZERO) > 0 && w3tx.maxPriorityFeePerGas.compareTo(BigInteger.ZERO) > 0) @@ -273,6 +280,7 @@ private void initGasSpeeds(Realm1559Gas gs) GasSpeed custom = getCustomGasSpeed(); gasSpread = new GasPriceSpread(getContext(), gs.getResult()); gasSpread.setCustom(custom); + gasEstimateTime = gs.getTimeStamp(); //if we have mainnet then show timings, otherwise no timing, if the token has fiat value, show fiat value of gas, so we need the ticker handler.post(this); @@ -340,6 +348,22 @@ public void run() } checkSufficientGas(); manageWarnings(); + + if (gasPriceReady(gasEstimateTime)) + { + actionSheetInterface.gasEstimateReady(); + setGasReadyStatus(true); + } + else + { + setGasReadyStatus(false); + } + } + + private void setGasReadyStatus(boolean ready) + { + findViewById(R.id.view_spacer).setVisibility(ready ? View.VISIBLE : View.GONE); + findViewById(R.id.gas_fetch_wait).setVisibility(ready ? View.GONE : View.VISIBLE); } @Override @@ -359,7 +383,7 @@ public BigInteger getGasPrice() @Override public BigInteger getGasPrice(BigInteger defaultPrice) { - if (gasSpread != null) + if (gasSpread != null && gasSpread.getSelectedGasFee(currentGasSpeedIndex) != null) { GasSpeed gs = gasSpread.getSelectedGasFee(currentGasSpeedIndex); return gs.gasPrice.maxFeePerGas; @@ -390,6 +414,12 @@ public boolean isSendingAll(Web3Transaction tx) return false; } + @Override + public boolean gasPriceReady() + { + return gasPriceReady(gasEstimateTime); + } + @Override public BigInteger getValue() { diff --git a/app/src/main/res/layout/item_gas_settings.xml b/app/src/main/res/layout/item_gas_settings.xml index 54f461162e..ee08c46ad6 100644 --- a/app/src/main/res/layout/item_gas_settings.xml +++ b/app/src/main/res/layout/item_gas_settings.xml @@ -110,9 +110,18 @@ android:paddingEnd="@dimen/mini_4"> + android:id="@+id/view_spacer" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_weight="@integer/widget_label" /> + + - \ No newline at end of file +