Improve gas calc (#3356)

* update to use Infura gas provider if available.
* Ensure up to date gas information has been received before sending transactions.
pull/3357/head
James Brown 9 months ago committed by GitHub
parent 80e6ae788f
commit 9677256493
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      app/build.gradle
  2. 13
      app/src/main/java/com/alphawallet/app/entity/ActionSheetInterface.java
  3. 62
      app/src/main/java/com/alphawallet/app/entity/GasPriceSpread.java
  4. 12
      app/src/main/java/com/alphawallet/app/repository/AWRealmMigration.java
  5. 28
      app/src/main/java/com/alphawallet/app/repository/EthereumNetworkBase.java
  6. 5
      app/src/main/java/com/alphawallet/app/repository/HttpServiceHelper.java
  7. 2
      app/src/main/java/com/alphawallet/app/repository/TransactionsRealmCache.java
  8. 7
      app/src/main/java/com/alphawallet/app/repository/entity/Realm1559Gas.java
  9. 6
      app/src/main/java/com/alphawallet/app/service/BlockNativeGasAPI.java
  10. 19
      app/src/main/java/com/alphawallet/app/service/GasService.java
  11. 126
      app/src/main/java/com/alphawallet/app/service/InfuraGasAPI.java
  12. 6
      app/src/main/java/com/alphawallet/app/ui/widget/entity/GasWidgetInterface.java
  13. 30
      app/src/main/java/com/alphawallet/app/widget/ActionSheetDialog.java
  14. 55
      app/src/main/java/com/alphawallet/app/widget/GasWidget.java
  15. 56
      app/src/main/java/com/alphawallet/app/widget/GasWidget2.java
  16. 15
      app/src/main/res/layout/item_gas_settings.xml

@ -90,7 +90,7 @@ android {
def DEFUALT_WALLETCONNECT_PROJECT_ID = "\"40c6071febfd93f4fe485c232a8a4cd9\"" 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 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", XInfuraAPI, DEFAULT_INFURA_API_KEY
buildConfigField "String", "WALLETCONNECT_PROJECT_ID", DEFUALT_WALLETCONNECT_PROJECT_ID buildConfigField "String", "WALLETCONNECT_PROJECT_ID", DEFUALT_WALLETCONNECT_PROJECT_ID

@ -1,6 +1,9 @@
package com.alphawallet.app.entity; package com.alphawallet.app.entity;
import android.content.Intent;
import androidx.activity.result.ActivityResult; import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import com.alphawallet.app.web3.entity.Web3Transaction; import com.alphawallet.app.web3.entity.Web3Transaction;
@ -58,4 +61,14 @@ public interface ActionSheetInterface
default void setCurrentGasIndex(ActivityResult result) default void setCurrentGasIndex(ActivityResult result)
{ {
} }
default ActivityResultLauncher<Intent> gasSelectLauncher()
{
return null;
}
default void gasEstimateReady()
{
}
} }

@ -19,6 +19,8 @@ import java.util.Map;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import timber.log.Timber;
/** /**
* Created by JB on 20/01/2022. * Created by JB on 20/01/2022.
*/ */
@ -165,6 +167,66 @@ public class GasPriceSpread implements Parcelable
hasLockedGas = false; 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) public GasPriceSpread(String apiReturn)
{ {
this.timeStamp = System.currentTimeMillis(); this.timeStamp = System.currentTimeMillis();

@ -490,6 +490,18 @@ public class AWRealmMigration implements RealmMigration
oldVersion = 53; 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 @Override

@ -119,6 +119,7 @@ public abstract class EthereumNetworkBase implements EthereumNetworkRepositoryTy
private static final KeyProvider keyProvider = KeyProviderFactory.get(); private static final KeyProvider keyProvider = KeyProviderFactory.get();
public static final boolean usesProductionKey = !keyProvider.getInfuraKey().equals(DEFAULT_INFURA_KEY); 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_MAINNET_RPC_URL = "https://rpc.ankr.com/eth";
public static final String FREE_POLYGON_RPC_URL = "https://polygon-rpc.com"; public static final String FREE_POLYGON_RPC_URL = "https://polygon-rpc.com";
@ -493,7 +494,9 @@ public abstract class EthereumNetworkBase implements EthereumNetworkRepositoryTy
//Add it to this list here if so. Note that so far, all gas oracles follow the same format: //Add it to this list here if so. Note that so far, all gas oracles follow the same format:
// <etherscanAPI from the above list> + GAS_API // <etherscanAPI from the above list> + GAS_API
//If the gas oracle you're adding doesn't follow this spec then you'll have to change the getGasOracle method //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<Long> hasGasOracleAPI = Arrays.asList(MAINNET_ID, HECO_ID, BINANCE_MAIN_ID, POLYGON_ID); private static final List<Long> 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<Long> hasEtherscanGasOracleAPI = Arrays.asList(MAINNET_ID, HECO_ID, BINANCE_MAIN_ID, POLYGON_ID);
private static final List<Long> hasBlockNativeGasOracleAPI = Arrays.asList(MAINNET_ID, POLYGON_ID); private static final List<Long> hasBlockNativeGasOracleAPI = Arrays.asList(MAINNET_ID, POLYGON_ID);
//These chains don't allow custom gas //These chains don't allow custom gas
private static final List<Long> hasLockedGas = Arrays.asList(KLAYTN_ID, KLAYTN_BAOBAB_ID); private static final List<Long> hasLockedGas = Arrays.asList(KLAYTN_ID, KLAYTN_BAOBAB_ID);
@ -508,11 +511,24 @@ public abstract class EthereumNetworkBase implements EthereumNetworkRepositoryTy
} }
}; };
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) public static String getGasOracle(long chainId)
{ {
if (hasGasOracleAPI.contains(chainId) && networkMap.indexOfKey(chainId) >= 0) 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 else
{ {
@ -603,7 +619,9 @@ public abstract class EthereumNetworkBase implements EthereumNetworkRepositoryTy
public static int getBatchProcessingLimit(long chainId) public static int getBatchProcessingLimit(long chainId)
{ {
if (batchProcessingLimitMap.size() == 0) setBatchProcessingLimits(); //If batch limits not set, init them and proceed 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 @Override
@ -861,8 +879,6 @@ public abstract class EthereumNetworkBase implements EthereumNetworkRepositoryTy
return networkMap.get(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 @Override
public Single<BigInteger> getLastTransactionNonce(Web3j web3j, String walletAddress) public Single<BigInteger> getLastTransactionNonce(Web3j web3j, String walletAddress)
{ {
@ -871,7 +887,7 @@ public abstract class EthereumNetworkBase implements EthereumNetworkRepositoryTy
try try
{ {
EthGetTransactionCount ethGetTransactionCount = web3j EthGetTransactionCount ethGetTransactionCount = web3j
.ethGetTransactionCount(walletAddress, DefaultBlockParameterName.LATEST) .ethGetTransactionCount(walletAddress, DefaultBlockParameterName.PENDING)
.send(); .send();
return ethGetTransactionCount.getTransactionCount(); return ethGetTransactionCount.getTransactionCount();
} }

@ -38,4 +38,9 @@ public class HttpServiceHelper
service.addHeader("Authorization", "Basic " + infuraKey); service.addHeader("Authorization", "Basic " + infuraKey);
} }
} }
public static void addInfuraGasCredentials(Request.Builder service, String infuraSecret)
{
service.addHeader("Authorization", "Basic " + infuraSecret);
}
} }

@ -10,6 +10,7 @@ import com.alphawallet.app.entity.EventMeta;
import com.alphawallet.app.entity.Transaction; import com.alphawallet.app.entity.Transaction;
import com.alphawallet.app.entity.TransactionMeta; import com.alphawallet.app.entity.TransactionMeta;
import com.alphawallet.app.entity.Wallet; 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.RealmAuxData;
import com.alphawallet.app.repository.entity.RealmNFTAsset; import com.alphawallet.app.repository.entity.RealmNFTAsset;
import com.alphawallet.app.repository.entity.RealmToken; import com.alphawallet.app.repository.entity.RealmToken;
@ -295,6 +296,7 @@ public class TransactionsRealmCache implements TransactionLocalSource {
r.where(RealmAuxData.class).findAll().deleteAllFromRealm(); r.where(RealmAuxData.class).findAll().deleteAllFromRealm();
r.where(RealmNFTAsset.class).findAll().deleteAllFromRealm(); r.where(RealmNFTAsset.class).findAll().deleteAllFromRealm();
r.where(RealmTransfer.class).findAll().deleteAllFromRealm(); r.where(RealmTransfer.class).findAll().deleteAllFromRealm();
r.where(Realm1559Gas.class).findAll().deleteAllFromRealm();
}); });
instance.refresh(); instance.refresh();
} }

@ -24,7 +24,12 @@ public class Realm1559Gas extends RealmObject
public Map<Integer, EIP1559FeeOracleResult> getResult() public Map<Integer, EIP1559FeeOracleResult> getResult()
{ {
Type entry = new TypeToken<Map<Integer, EIP1559FeeOracleResult>>() {}.getType(); Type entry = new TypeToken<Map<Integer, EIP1559FeeOracleResult>>() {}.getType();
return new Gson().fromJson(resultData, entry); return new Gson().fromJson(getResultData(), entry);
}
public String getResultData()
{
return resultData;
} }
public void setResultData(Map<Integer, EIP1559FeeOracleResult> result, long ts) public void setResultData(Map<Integer, EIP1559FeeOracleResult> result, long ts)

@ -55,8 +55,12 @@ public class BlockNativeGasAPI
return requestB.build(); return requestB.build();
} }
public Single<Map<Integer, EIP1559FeeOracleResult>> fetchGasEstimates(long chainId) public Single<Map<Integer, EIP1559FeeOracleResult>> get1559GasEstimates(Map<Integer, EIP1559FeeOracleResult> result, long chainId)
{ {
if (result.size() > 0)
{
return Single.fromCallable(() -> result);
}
String oracleAPI = EthereumNetworkBase.getBlockNativeOracle(chainId); String oracleAPI = EthereumNetworkBase.getBlockNativeOracle(chainId);
return Single.fromCallable(() -> buildOracleResult(executeRequest(oracleAPI))); // any kind of error results in blank mapping, return Single.fromCallable(() -> buildOracleResult(executeRequest(oracleAPI))); // any kind of error results in blank mapping,
// if blank, fall back to calculation method // if blank, fall back to calculation method

@ -30,7 +30,6 @@ import com.alphawallet.app.repository.KeyProviderFactory;
import com.alphawallet.app.repository.entity.Realm1559Gas; import com.alphawallet.app.repository.entity.Realm1559Gas;
import com.alphawallet.app.repository.entity.RealmGasSpread; import com.alphawallet.app.repository.entity.RealmGasSpread;
import com.alphawallet.app.web3.entity.Web3Transaction; import com.alphawallet.app.web3.entity.Web3Transaction;
import org.web3j.utils.Numeric;
import com.google.gson.Gson; import com.google.gson.Gson;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -41,6 +40,7 @@ import org.web3j.protocol.core.methods.response.EthEstimateGas;
import org.web3j.protocol.core.methods.response.EthGasPrice; import org.web3j.protocol.core.methods.response.EthGasPrice;
import org.web3j.protocol.http.HttpService; import org.web3j.protocol.http.HttpService;
import org.web3j.tx.gas.ContractGasProvider; import org.web3j.tx.gas.ContractGasProvider;
import org.web3j.utils.Numeric;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.Map; import java.util.Map;
@ -82,6 +82,7 @@ public class GasService implements ContractGasProvider
private final String ETHERSCAN_API_KEY; private final String ETHERSCAN_API_KEY;
private final String POLYGONSCAN_API_KEY; private final String POLYGONSCAN_API_KEY;
private boolean keyFail; private boolean keyFail;
@Nullable @Nullable
private Disposable gasFetchDisposable; private Disposable gasFetchDisposable;
@ -186,7 +187,7 @@ public class GasService implements ContractGasProvider
private Single<Boolean> updateCurrentGasPrices() private Single<Boolean> updateCurrentGasPrices()
{ {
String gasOracleAPI = EthereumNetworkRepository.getGasOracle(currentChainId); String gasOracleAPI = EthereumNetworkRepository.getEtherscanGasOracle(currentChainId);
if (!TextUtils.isEmpty(gasOracleAPI)) if (!TextUtils.isEmpty(gasOracleAPI))
{ {
if (!keyFail && gasOracleAPI.contains("etherscan")) gasOracleAPI += ETHERSCAN_API_KEY; if (!keyFail && gasOracleAPI.contains("etherscan")) gasOracleAPI += ETHERSCAN_API_KEY;
@ -303,13 +304,14 @@ public class GasService implements ContractGasProvider
Realm1559Gas rgs = r.where(Realm1559Gas.class) Realm1559Gas rgs = r.where(Realm1559Gas.class)
.equalTo("chainId", chainId) .equalTo("chainId", chainId)
.findFirst(); .findFirst();
if (rgs == null) if (rgs == null)
{ {
rgs = r.createObject(Realm1559Gas.class, chainId); rgs = r.createObject(Realm1559Gas.class, chainId);
} }
rgs.setResultData(result, System.currentTimeMillis()); rgs.setResultData(result, System.currentTimeMillis());
r.insertOrUpdate(rgs); //r.insertOrUpdate(rgs);
}); });
} }
catch (Exception e) catch (Exception e)
@ -325,11 +327,11 @@ public class GasService implements ContractGasProvider
{ {
updateChainId(chainId); updateChainId(chainId);
return useNodeEstimate(true) return useNodeEstimate(true)
.flatMap(com -> calculateGasEstimateInternal(transactionBytes, chainId, toAddress, amount, wallet, defaultLimit)); .flatMap(com -> calculateGasEstimateInternal(transactionBytes, chainId, toAddress, amount, wallet, defaultLimit));
} }
public Single<GasEstimate> calculateGasEstimateInternal(byte[] transactionBytes, long chainId, String toAddress, public Single<GasEstimate> calculateGasEstimateInternal(byte[] transactionBytes, long chainId, String toAddress,
BigInteger amount, Wallet wallet, final BigInteger defaultLimit) BigInteger amount, Wallet wallet, final BigInteger defaultLimit)
{ {
String txData = ""; String txData = "";
if (transactionBytes != null && transactionBytes.length > 0) if (transactionBytes != null && transactionBytes.length > 0)
@ -387,7 +389,7 @@ public class GasService implements ContractGasProvider
{ {
if (!estimate.hasError() || chainId != 1) return Single.fromCallable(() -> estimate); if (!estimate.hasError() || chainId != 1) return Single.fromCallable(() -> estimate);
else return networkRepository.getLastTransactionNonce(web3j, WHALE_ACCOUNT) 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() private BigInteger getLowGasPrice()
@ -420,8 +422,9 @@ public class GasService implements ContractGasProvider
private Single<Map<Integer, EIP1559FeeOracleResult>> getEIP1559FeeStructure() private Single<Map<Integer, EIP1559FeeOracleResult>> getEIP1559FeeStructure()
{ {
return BlockNativeGasAPI.get(httpClient).fetchGasEstimates(currentChainId) return InfuraGasAPI.get1559GasEstimates(currentChainId, httpClient)
.flatMap(this::useCalculationIfRequired); //if interface doesn't have blocknative API then use calculation method .flatMap(result -> BlockNativeGasAPI.get(httpClient).get1559GasEstimates(result, currentChainId))
.flatMap(this::useCalculationIfRequired); //if interface doesn't have blocknative API then use calculation method
} }
private Single<Map<Integer, EIP1559FeeOracleResult>> useCalculationIfRequired(Map<Integer, EIP1559FeeOracleResult> resultMap) private Single<Map<Integer, EIP1559FeeOracleResult>> useCalculationIfRequired(Map<Integer, EIP1559FeeOracleResult> resultMap)

@ -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<Map<Integer, EIP1559FeeOracleResult>> get1559GasEstimates(final long chainId, final OkHttpClient httpClient)
{
return Single.fromCallable(() -> {
Map<Integer, EIP1559FeeOracleResult> 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<Integer, EIP1559FeeOracleResult> readGasMap(String apiReturn)
{
Map<Integer, EIP1559FeeOracleResult> 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;
}
}

@ -28,4 +28,10 @@ public interface GasWidgetInterface
void setupResendSettings(ActionSheetMode mode, BigInteger gasPrice); void setupResendSettings(ActionSheetMode mode, BigInteger gasPrice);
void setCurrentGasIndex(int gasSelectionIndex, BigInteger maxFeePerGas, BigInteger maxPriorityFee, BigDecimal customGasLimit, long expectedTxTime, long customNonce); void setCurrentGasIndex(int gasSelectionIndex, BigInteger maxFeePerGas, BigInteger maxPriorityFee, BigDecimal customGasLimit, long expectedTxTime, long customNonce);
long getExpectedTransactionTime(); long getExpectedTransactionTime();
default boolean gasPriceReady(long gasEstimateTime)
{
return gasEstimateTime > (System.currentTimeMillis() - 30 * 1000);
}
boolean gasPriceReady();
} }

@ -10,6 +10,7 @@ import android.view.View;
import android.widget.Toast; import android.widget.Toast;
import androidx.activity.result.ActivityResult; import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
@ -89,7 +90,6 @@ public class ActionSheetDialog extends ActionSheet implements StandardFunctionIn
private boolean use1559Transactions = false; private boolean use1559Transactions = false;
private Transaction transaction; private Transaction transaction;
private final WalletType walletType; private final WalletType walletType;
private Disposable disposable;
public ActionSheetDialog(@NonNull Activity activity, Web3Transaction tx, Token t, public ActionSheetDialog(@NonNull Activity activity, Web3Transaction tx, Token t,
String destName, String destAddress, TokensService ts, String destName, String destAddress, TokensService ts,
@ -329,14 +329,14 @@ public class ActionSheetDialog extends ActionSheet implements StandardFunctionIn
if (use1559Transactions) if (use1559Transactions)
{ {
gasWidget.setupWidget(tokensService, token, candidateTransaction, actionSheetCallback.gasSelectLauncher()); gasWidget.setupWidget(tokensService, token, candidateTransaction, this);
return gasWidget; return gasWidget;
} }
else else
{ {
gasWidget.setVisibility(View.GONE); gasWidget.setVisibility(View.GONE);
gasWidgetLegacy.setVisibility(View.VISIBLE); gasWidgetLegacy.setVisibility(View.VISIBLE);
gasWidgetLegacy.setupWidget(tokensService, token, candidateTransaction, this, actionSheetCallback.gasSelectLauncher()); gasWidgetLegacy.setupWidget(tokensService, token, candidateTransaction, this, this);
return gasWidgetLegacy; return gasWidgetLegacy;
} }
} }
@ -434,6 +434,13 @@ public class ActionSheetDialog extends ActionSheet implements StandardFunctionIn
@SuppressWarnings("checkstyle:MissingSwitchDefault") @SuppressWarnings("checkstyle:MissingSwitchDefault")
public void handleClick(String action, int id) public void handleClick(String action, int id)
{ {
//first ensure gas estimate is up to date
if (gasEstimateOutOfDate())
{
functionBar.setPrimaryButtonWaiting();
return;
}
if (walletType == WalletType.HARDWARE) if (walletType == WalletType.HARDWARE)
{ {
//TODO: Hardware - Maybe flick a toast to tell user to apply card //TODO: Hardware - Maybe flick a toast to tell user to apply card
@ -477,6 +484,23 @@ public class ActionSheetDialog extends ActionSheet implements StandardFunctionIn
} }
} }
@Override
public void gasEstimateReady()
{
functionBar.setPrimaryButtonEnabled(true);
}
@Override
public ActivityResultLauncher<Intent> gasSelectLauncher()
{
return actionSheetCallback.gasSelectLauncher();
}
private boolean gasEstimateOutOfDate()
{
return !gasWidget.gasPriceReady();
}
private BigDecimal getTransactionAmount() private BigDecimal getTransactionAmount()
{ {
BigDecimal txAmount; BigDecimal txAmount;

@ -11,11 +11,10 @@ import android.view.View;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import androidx.activity.result.ActivityResultLauncher;
import com.alphawallet.app.BuildConfig; import com.alphawallet.app.BuildConfig;
import com.alphawallet.app.C; import com.alphawallet.app.C;
import com.alphawallet.app.R; import com.alphawallet.app.R;
import com.alphawallet.app.entity.ActionSheetInterface;
import com.alphawallet.app.entity.GasPriceSpread; import com.alphawallet.app.entity.GasPriceSpread;
import com.alphawallet.app.entity.StandardFunctionInterface; import com.alphawallet.app.entity.StandardFunctionInterface;
import com.alphawallet.app.entity.TXSpeed; import com.alphawallet.app.entity.TXSpeed;
@ -70,6 +69,8 @@ public class GasWidget extends LinearLayout implements Runnable, GasWidgetInterf
private long customNonce = -1; private long customNonce = -1;
private boolean isSendingAll; private boolean isSendingAll;
private BigInteger resendGasPrice = BigInteger.ZERO; private BigInteger resendGasPrice = BigInteger.ZERO;
long gasEstimateTime = 0;
private ActionSheetInterface actionSheetInterface;
public GasWidget(Context ctx, AttributeSet attrs) public GasWidget(Context ctx, AttributeSet attrs)
{ {
@ -85,7 +86,7 @@ public class GasWidget extends LinearLayout implements Runnable, GasWidgetInterf
//For legacy transaction, either we are sending all or the chain doesn't support EIP1559 //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 //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 //That is - not allow selection of gas price
public void setupWidget(TokensService svs, Token t, Web3Transaction tx, StandardFunctionInterface sfi, ActivityResultLauncher<Intent> gasSelectLauncher) public void setupWidget(TokensService svs, Token t, Web3Transaction tx, StandardFunctionInterface sfi, ActionSheetInterface actionSheetIf)
{ {
tokensService = svs; tokensService = svs;
token = t; token = t;
@ -96,6 +97,7 @@ public class GasWidget extends LinearLayout implements Runnable, GasWidgetInterf
isSendingAll = isSendingAll(tx); isSendingAll = isSendingAll(tx);
initialGasPrice = tx.gasPrice; initialGasPrice = tx.gasPrice;
customNonce = tx.nonce; 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()) 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 class GasWidget extends LinearLayout implements Runnable, GasWidgetInterf
intent.putExtra(C.EXTRA_NONCE, customNonce); intent.putExtra(C.EXTRA_NONCE, customNonce);
intent.putExtra(C.EXTRA_1559_TX, false); intent.putExtra(C.EXTRA_1559_TX, false);
intent.putExtra(C.EXTRA_MIN_GAS_PRICE, resendGasPrice.longValue()); intent.putExtra(C.EXTRA_MIN_GAS_PRICE, resendGasPrice.longValue());
gasSelectLauncher.launch(intent); actionSheetInterface.gasSelectLauncher().launch(intent);
}); });
} }
} }
private void setupGasSpeeds(Web3Transaction w3tx) private void setupGasSpeeds(Web3Transaction w3tx)
{ {
RealmGasSpread rgs = getGasQuery().findFirst(); try (Realm realm = tokensService.getTickerRealmInstance())
if (rgs != null)
{
initGasSpeeds(rgs);
}
else
{ {
// Couldn't get current gas. Add a blank custom gas speed node RealmGasSpread gasReturn = realm.where(RealmGasSpread.class)
gasSpread = new GasPriceSpread(getContext(), w3tx.gasPrice); .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) if (w3tx.gasPrice.compareTo(BigInteger.ZERO) > 0)
@ -301,6 +308,8 @@ public class GasWidget extends LinearLayout implements Runnable, GasWidgetInterf
TextView editTxt = findViewById(R.id.edit_text); TextView editTxt = findViewById(R.id.edit_text);
gasEstimateTime = rgs.getTimeStamp();
if (gasSpread.hasLockedGas() && editTxt.getVisibility() == View.VISIBLE) if (gasSpread.hasLockedGas() && editTxt.getVisibility() == View.VISIBLE)
{ {
findViewById(R.id.edit_text).setVisibility(View.GONE); findViewById(R.id.edit_text).setVisibility(View.GONE);
@ -383,6 +392,16 @@ public class GasWidget extends LinearLayout implements Runnable, GasWidgetInterf
} }
checkSufficientGas(); checkSufficientGas();
manageWarnings(); manageWarnings();
if (gasPriceReady(gasEstimateTime))
{
actionSheetInterface.gasEstimateReady();
setGasReadyStatus(true);
}
else
{
setGasReadyStatus(false);
}
} }
@Override @Override
@ -399,6 +418,12 @@ public class GasWidget extends LinearLayout implements Runnable, GasWidgetInterf
} }
} }
@Override
public boolean gasPriceReady()
{
return gasPriceReady(gasEstimateTime);
}
@Override @Override
public BigInteger getValue() public BigInteger getValue()
{ {
@ -447,6 +472,12 @@ public class GasWidget extends LinearLayout implements Runnable, GasWidgetInterf
} }
} }
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) private void showCustomSpeedWarning(boolean high)
{ {
if (currentGasSpeedIndex != TXSpeed.CUSTOM) if (currentGasSpeedIndex != TXSpeed.CUSTOM)

@ -11,11 +11,10 @@ import android.view.View;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import androidx.activity.result.ActivityResultLauncher;
import com.alphawallet.app.BuildConfig; import com.alphawallet.app.BuildConfig;
import com.alphawallet.app.C; import com.alphawallet.app.C;
import com.alphawallet.app.R; import com.alphawallet.app.R;
import com.alphawallet.app.entity.ActionSheetInterface;
import com.alphawallet.app.entity.GasPriceSpread; import com.alphawallet.app.entity.GasPriceSpread;
import com.alphawallet.app.entity.TXSpeed; import com.alphawallet.app.entity.TXSpeed;
import com.alphawallet.app.entity.analytics.ActionSheetMode; 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 TXSpeed currentGasSpeedIndex = TXSpeed.STANDARD;
private long customNonce = -1; private long customNonce = -1;
private BigInteger resendGasPrice = BigInteger.ZERO; private BigInteger resendGasPrice = BigInteger.ZERO;
private long gasEstimateTime = 0;
private ActionSheetInterface actionSheetInterface;
//Need to track user selected gas limit & calculated gas limit //Need to track user selected gas limit & calculated gas limit
//At initial setup, we have the limit from the tx or default: presetGasLimit //At initial setup, we have the limit from the tx or default: presetGasLimit
@ -85,7 +86,7 @@ public class GasWidget2 extends LinearLayout implements Runnable, GasWidgetInter
} }
// Called once from ActionSheet constructor // Called once from ActionSheet constructor
public void setupWidget(TokensService svs, Token t, Web3Transaction tx, ActivityResultLauncher<Intent> gasSelectLauncher) public void setupWidget(TokensService svs, Token t, Web3Transaction tx, ActionSheetInterface actionSheetIf)
{ {
tokensService = svs; tokensService = svs;
token = t; token = t;
@ -93,6 +94,7 @@ public class GasWidget2 extends LinearLayout implements Runnable, GasWidgetInter
adjustedValue = tx.value; adjustedValue = tx.value;
initialGasPrice = tx.gasPrice; initialGasPrice = tx.gasPrice;
customNonce = tx.nonce; 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()) 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 class GasWidget2 extends LinearLayout implements Runnable, GasWidgetInter
intent.putExtra(C.EXTRA_NONCE, customNonce); intent.putExtra(C.EXTRA_NONCE, customNonce);
intent.putExtra(C.EXTRA_1559_TX, true); intent.putExtra(C.EXTRA_1559_TX, true);
intent.putExtra(C.EXTRA_MIN_GAS_PRICE, resendGasPrice.longValue()); intent.putExtra(C.EXTRA_MIN_GAS_PRICE, resendGasPrice.longValue());
gasSelectLauncher.launch(intent); actionSheetInterface.gasSelectLauncher().launch(intent);
}); });
} }
} }
@ -132,15 +134,20 @@ public class GasWidget2 extends LinearLayout implements Runnable, GasWidgetInter
//set custom fee if specified by tx feed //set custom fee if specified by tx feed
private void setupGasSpeeds(Web3Transaction w3tx) private void setupGasSpeeds(Web3Transaction w3tx)
{ {
Realm1559Gas getGas = getGasQuery2().findFirst(); try (Realm realm = tokensService.getTickerRealmInstance())
if (getGas != null)
{
initGasSpeeds(getGas);
}
else
{ {
// Couldn't get current gas. Add a blank custom gas speed node Realm1559Gas gasReturn = realm.where(Realm1559Gas.class)
gasSpread = new GasPriceSpread(getContext(), w3tx.maxFeePerGas, w3tx.maxPriorityFeePerGas); .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) if (w3tx.maxFeePerGas.compareTo(BigInteger.ZERO) > 0 && w3tx.maxPriorityFeePerGas.compareTo(BigInteger.ZERO) > 0)
@ -273,6 +280,7 @@ public class GasWidget2 extends LinearLayout implements Runnable, GasWidgetInter
GasSpeed custom = getCustomGasSpeed(); GasSpeed custom = getCustomGasSpeed();
gasSpread = new GasPriceSpread(getContext(), gs.getResult()); gasSpread = new GasPriceSpread(getContext(), gs.getResult());
gasSpread.setCustom(custom); 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 //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); handler.post(this);
@ -340,6 +348,22 @@ public class GasWidget2 extends LinearLayout implements Runnable, GasWidgetInter
} }
checkSufficientGas(); checkSufficientGas();
manageWarnings(); 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 @Override
@ -359,7 +383,7 @@ public class GasWidget2 extends LinearLayout implements Runnable, GasWidgetInter
@Override @Override
public BigInteger getGasPrice(BigInteger defaultPrice) public BigInteger getGasPrice(BigInteger defaultPrice)
{ {
if (gasSpread != null) if (gasSpread != null && gasSpread.getSelectedGasFee(currentGasSpeedIndex) != null)
{ {
GasSpeed gs = gasSpread.getSelectedGasFee(currentGasSpeedIndex); GasSpeed gs = gasSpread.getSelectedGasFee(currentGasSpeedIndex);
return gs.gasPrice.maxFeePerGas; return gs.gasPrice.maxFeePerGas;
@ -390,6 +414,12 @@ public class GasWidget2 extends LinearLayout implements Runnable, GasWidgetInter
return false; return false;
} }
@Override
public boolean gasPriceReady()
{
return gasPriceReady(gasEstimateTime);
}
@Override @Override
public BigInteger getValue() public BigInteger getValue()
{ {

@ -110,9 +110,18 @@
android:paddingEnd="@dimen/mini_4"> android:paddingEnd="@dimen/mini_4">
<View <View
android:layout_width="0dp" android:id="@+id/view_spacer"
android:layout_height="0dp" android:layout_width="0dp"
android:layout_weight="@integer/widget_label" /> android:layout_height="0dp"
android:layout_weight="@integer/widget_label" />
<ProgressBar
android:id="@+id/gas_fetch_wait"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:progressDrawable="@drawable/progress_bar_spinner"
android:visibility="gone"
android:layout_weight="@integer/widget_control"/>
<TextView <TextView
android:id="@+id/text_time_estimate" android:id="@+id/text_time_estimate"

Loading…
Cancel
Save