Skip to content

Commit

Permalink
Improve gas calc (#3356)
Browse files Browse the repository at this point in the history
* update to use Infura gas provider if available.
* Ensure up to date gas information has been received before sending transactions.
  • Loading branch information
JamesSmartCell authored Feb 10, 2024
1 parent 80e6ae7 commit 9677256
Show file tree
Hide file tree
Showing 16 changed files with 397 additions and 49 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -58,4 +61,14 @@ default void setSignOnly()
default void setCurrentGasIndex(ActivityResult result)
{
}

default ActivityResultLauncher<Intent> gasSelectLauncher()
{
return null;
}

default void gasEstimateReady()
{

}
}
62 changes: 62 additions & 0 deletions app/src/main/java/com/alphawallet/app/entity/GasPriceSpread.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

import javax.annotation.Nullable;

import timber.log.Timber;

/**
* Created by JB on 20/01/2022.
*/
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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:
// <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
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);
//These chains don't allow custom gas
private static final List<Long> hasLockedGas = Arrays.asList(KLAYTN_ID, KLAYTN_BAOBAB_ID);
Expand All @@ -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
{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<BigInteger> getLastTransactionNonce(Web3j web3j, String walletAddress)
{
Expand All @@ -871,7 +887,7 @@ public Single<BigInteger> getLastTransactionNonce(Web3j web3j, String walletAddr
try
{
EthGetTransactionCount ethGetTransactionCount = web3j
.ethGetTransactionCount(walletAddress, DefaultBlockParameterName.LATEST)
.ethGetTransactionCount(walletAddress, DefaultBlockParameterName.PENDING)
.send();
return ethGetTransactionCount.getTransactionCount();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -295,6 +296,7 @@ public Single<Boolean> 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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ public class Realm1559Gas extends RealmObject
public Map<Integer, EIP1559FeeOracleResult> getResult()
{
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,12 @@ private Request buildRequest(String api)
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);
return Single.fromCallable(() -> buildOracleResult(executeRequest(oracleAPI))); // any kind of error results in blank mapping,
// if blank, fall back to calculation method
Expand Down
19 changes: 11 additions & 8 deletions app/src/main/java/com/alphawallet/app/service/GasService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -186,7 +187,7 @@ private boolean nodeFetchValid()

private Single<Boolean> updateCurrentGasPrices()
{
String gasOracleAPI = EthereumNetworkRepository.getGasOracle(currentChainId);
String gasOracleAPI = EthereumNetworkRepository.getEtherscanGasOracle(currentChainId);
if (!TextUtils.isEmpty(gasOracleAPI))
{
if (!keyFail && gasOracleAPI.contains("etherscan")) gasOracleAPI += ETHERSCAN_API_KEY;
Expand Down Expand Up @@ -303,13 +304,14 @@ private boolean updateEIP1559Realm(final Map<Integer, EIP1559FeeOracleResult> 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)
Expand All @@ -325,11 +327,11 @@ public Single<GasEstimate> 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<GasEstimate> 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)
Expand Down Expand Up @@ -387,7 +389,7 @@ private Single<EthEstimateGas> 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()
Expand Down Expand Up @@ -420,8 +422,9 @@ private Single<EthEstimateGas> ethEstimateGas(long chainId, String fromAddress,

private Single<Map<Integer, EIP1559FeeOracleResult>> 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<Map<Integer, EIP1559FeeOracleResult>> useCalculationIfRequired(Map<Integer, EIP1559FeeOracleResult> resultMap)
Expand Down
Loading

0 comments on commit 9677256

Please sign in to comment.