From a39154b3193152cbb15162952df9bd87c76125c0 Mon Sep 17 00:00:00 2001 From: James Brown Date: Mon, 22 Apr 2024 13:34:02 +1000 Subject: [PATCH 1/2] Add Base networks --- .../alphawallet/app/entity/NetworkInfo.java | 2 +- .../app/repository/EthereumNetworkBase.java | 31 +++++++++++++++++-- .../service/TransactionsNetworkClient.java | 2 +- app/src/main/res/drawable/ic_base_logo.xml | 12 +++++++ .../main/res/drawable/ic_base_test_logo.xml | 12 +++++++ app/src/main/res/values/palette.xml | 1 + .../ethereum/EthereumNetworkBase.java | 9 ++++++ 7 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 app/src/main/res/drawable/ic_base_logo.xml create mode 100644 app/src/main/res/drawable/ic_base_test_logo.xml diff --git a/app/src/main/java/com/alphawallet/app/entity/NetworkInfo.java b/app/src/main/java/com/alphawallet/app/entity/NetworkInfo.java index 7824e7a049..3e3e79720e 100644 --- a/app/src/main/java/com/alphawallet/app/entity/NetworkInfo.java +++ b/app/src/main/java/com/alphawallet/app/entity/NetworkInfo.java @@ -69,7 +69,7 @@ else if (chainId == GOERLI_ID || etherscanAPI.startsWith(ARBISCAN_API)) { return new TransferFetchType[]{TransferFetchType.ERC_20, TransferFetchType.ERC_721}; } - else if (etherscanAPI.contains(MATIC_API) || etherscanAPI.contains(ETHERSCAN_API) || etherscanAPI.contains(OKX_API)) + else if (etherscanAPI.contains(MATIC_API) || etherscanAPI.contains(ETHERSCAN_API) || etherscanAPI.contains(OKX_API) || etherscanAPI.contains("basescan.org")) { return new TransferFetchType[]{TransferFetchType.ERC_20, TransferFetchType.ERC_721, TransferFetchType.ERC_1155}; } 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 0d41d53198..ae123080a1 100644 --- a/app/src/main/java/com/alphawallet/app/repository/EthereumNetworkBase.java +++ b/app/src/main/java/com/alphawallet/app/repository/EthereumNetworkBase.java @@ -17,6 +17,10 @@ import static com.alphawallet.ethereum.EthereumNetworkBase.AURORA_TESTNET_RPC_URL; import static com.alphawallet.ethereum.EthereumNetworkBase.AVALANCHE_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.AVALANCHE_RPC_URL; +import static com.alphawallet.ethereum.EthereumNetworkBase.BASE_FREE_MAINNET_RPC; +import static com.alphawallet.ethereum.EthereumNetworkBase.BASE_FREE_TESTNET_RPC; +import static com.alphawallet.ethereum.EthereumNetworkBase.BASE_MAINNET_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.BASE_TESTNET_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.BINANCE_MAIN_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.BINANCE_TEST_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.CLASSIC_ID; @@ -160,6 +164,8 @@ public abstract class EthereumNetworkBase implements EthereumNetworkRepositoryTy public static final String AMOY_RPC_FALLBACK = usesProductionKey ? AMOY_TEST_RPC_URL : "https://polygon-amoy-bor-rpc.publicnode.com"; + + public static final String USE_KLAYTN_RPC = !TextUtils.isEmpty(keyProvider.getBlockPiCypressKey()) ? "https://klaytn.blockpi.network/v1/rpc/" + keyProvider.getBlockPiCypressKey() : KLAYTN_RPC; public static final String USE_KLAYTN_BAOBAB_RPC = !TextUtils.isEmpty(keyProvider.getBlockPiBaobabKey()) ? "https://klaytn-baobab.blockpi.network/v1/rpc/" + keyProvider.getBlockPiBaobabKey() @@ -175,6 +181,10 @@ public abstract class EthereumNetworkBase implements EthereumNetworkRepositoryTy public static final String LINEA_FALLBACK_RPC = usesProductionKey ? LINEA_FREE_RPC : "https://linea.drpc.org"; public static final String LINEA_RPC = usesProductionKey ? "https://linea-mainnet.infura.io/v3/" + keyProvider.getInfuraKey() : LINEA_FALLBACK_RPC; public static final String LINEA_TEST_RPC = usesProductionKey ? "https://linea-goerli.infura.io/v3/" + keyProvider.getInfuraKey() : LINEA_TEST_FREE_RPC; + public static final String BASE_RPC = usesProductionKey ? "https://base-mainnet.infura.io/v3/" + keyProvider.getInfuraKey() : BASE_FREE_MAINNET_RPC; + public static final String BASE_FALLBACK_RPC = usesProductionKey ? BASE_FREE_MAINNET_RPC : "https://base-mainnet.public.blastapi.io"; + public static final String BASE_TEST_RPC = usesProductionKey ? "https://base-sepolia.infura.io/v3/" + keyProvider.getInfuraKey() : BASE_FREE_TESTNET_RPC; + public static final String BASE_TEST_FALLBACK_RPC = usesProductionKey ? BASE_FREE_TESTNET_RPC : "https://sepolia.base.org"; //Note that AlphaWallet now uses a double node configuration. See class AWHttpService comment 'try primary node'. //If you supply a main RPC and secondary it will try the secondary if the primary node times out after 10 seconds. @@ -199,17 +209,18 @@ public abstract class EthereumNetworkBase implements EthereumNetworkRepositoryTy //If your wallet prioritises xDai for example, you may want to move the XDAI_ID to the front of this list, //Then xDai would appear as the first token at the top of the wallet private static final List hasValue = new ArrayList<>(Arrays.asList( - MAINNET_ID, GNOSIS_ID, POLYGON_ID, ROOTSTOCK_MAINNET_ID, CLASSIC_ID, LINEA_ID, BINANCE_MAIN_ID, HECO_ID, AVALANCHE_ID, + MAINNET_ID, GNOSIS_ID, POLYGON_ID, ROOTSTOCK_MAINNET_ID, CLASSIC_ID, LINEA_ID, BASE_MAINNET_ID, BINANCE_MAIN_ID, HECO_ID, AVALANCHE_ID, FANTOM_ID, OPTIMISTIC_MAIN_ID, CRONOS_MAIN_ID, ARBITRUM_MAIN_ID, PALM_ID, KLAYTN_ID, IOTEX_MAINNET_ID, AURORA_MAINNET_ID, MILKOMEDA_C1_ID, OKX_ID)); private static final List testnetList = new ArrayList<>(Arrays.asList( - SEPOLIA_TESTNET_ID, POLYGON_AMOY_ID, HOLESKY_ID, GOERLI_ID, BINANCE_TEST_ID, + SEPOLIA_TESTNET_ID, POLYGON_AMOY_ID, HOLESKY_ID, BASE_TESTNET_ID, GOERLI_ID, BINANCE_TEST_ID, ROOTSTOCK_TESTNET_ID, CRONOS_TEST_ID, OPTIMISM_GOERLI_TEST_ID, POLYGON_TEST_ID, ARBITRUM_GOERLI_TEST_ID, LINEA_TEST_ID, KLAYTN_BAOBAB_ID, FANTOM_TEST_ID, IOTEX_TESTNET_ID, FUJI_TEST_ID, MILKOMEDA_C1_TEST_ID, AURORA_TESTNET_ID, PALM_TEST_ID)); private static final List deprecatedNetworkList = new ArrayList<>(Arrays.asList( // Add deprecated testnet IDs here + POLYGON_TEST_ID, GOERLI_ID )); private static final String INFURA_ENDPOINT = ".infura.io/v3/"; @@ -374,6 +385,14 @@ public static boolean isOKX(NetworkInfo networkInfo) HOLESKY_RPC_URL, "https://holesky.etherscan.io/tx/", HOLESKY_ID, HOLESKY_BACKUP_RPC_URL, "https://api-holesky.etherscan.io/api?")); + put(BASE_MAINNET_ID, new NetworkInfo(C.BASE_MAINNET_NAME, C.ETH_SYMBOL, + BASE_RPC, + "https://basescan.org/tx/", BASE_MAINNET_ID, BASE_FALLBACK_RPC, + "https://api.basescan.org/api?")); + put(BASE_TESTNET_ID, new NetworkInfo(C.BASE_TESTNET_NAME, C.ETH_SYMBOL, + BASE_TEST_RPC, + "https://sepolia.basescan.org/tx/", BASE_TESTNET_ID, BASE_TEST_FALLBACK_RPC, + "https://api-sepolia.basescan.org/api?")); // Add deprecated networks after this line } @@ -423,6 +442,8 @@ public static boolean isOKX(NetworkInfo networkInfo) put(LINEA_TEST_ID, R.drawable.ic_icons_linea_testnet); put(HOLESKY_ID, R.drawable.ic_icons_holesky); put(POLYGON_TEST_ID, R.drawable.ic_icons_tokens_mumbai); + put(BASE_MAINNET_ID, R.drawable.ic_base_logo); + put(BASE_TESTNET_ID, R.drawable.ic_base_test_logo); } }; @@ -466,6 +487,8 @@ public static boolean isOKX(NetworkInfo networkInfo) put(LINEA_ID, R.drawable.ic_icons_linea); put(LINEA_TEST_ID, R.drawable.ic_icons_linea_testnet); put(HOLESKY_ID, R.drawable.ic_icons_holesky); + put(BASE_MAINNET_ID, R.drawable.ic_base_logo); + put(BASE_TESTNET_ID, R.drawable.ic_base_test_logo); } }; @@ -509,6 +532,8 @@ public static boolean isOKX(NetworkInfo networkInfo) put(LINEA_ID, R.color.black); put(LINEA_TEST_ID, R.color.pinkish_grey); put(HOLESKY_ID, R.color.azure); + put(BASE_MAINNET_ID, R.color.base_logo); + put(BASE_TESTNET_ID, R.color.base_logo); } }; @@ -517,7 +542,7 @@ public static boolean isOKX(NetworkInfo networkInfo) // + 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, 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, POLYGON_AMOY_ID); + SEPOLIA_TESTNET_ID, FANTOM_ID, LINEA_ID, OPTIMISTIC_MAIN_ID, POLYGON_TEST_ID, POLYGON_AMOY_ID, BASE_MAINNET_ID, BASE_TESTNET_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 diff --git a/app/src/main/java/com/alphawallet/app/service/TransactionsNetworkClient.java b/app/src/main/java/com/alphawallet/app/service/TransactionsNetworkClient.java index 509dd81b77..9b351491f3 100644 --- a/app/src/main/java/com/alphawallet/app/service/TransactionsNetworkClient.java +++ b/app/src/main/java/com/alphawallet/app/service/TransactionsNetworkClient.java @@ -723,7 +723,7 @@ private String readNextTxBatch(String walletAddress, NetworkInfo networkInfo, lo private String getNetworkAPIToken(NetworkInfo networkInfo) { - if (networkInfo.etherscanAPI.contains("etherscan")) + if (networkInfo.etherscanAPI.contains("etherscan") /*|| networkInfo.etherscanAPI.contains("basescan.org")*/) { return ETHERSCAN_API_KEY; } diff --git a/app/src/main/res/drawable/ic_base_logo.xml b/app/src/main/res/drawable/ic_base_logo.xml new file mode 100644 index 0000000000..81219e5fa0 --- /dev/null +++ b/app/src/main/res/drawable/ic_base_logo.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_base_test_logo.xml b/app/src/main/res/drawable/ic_base_test_logo.xml new file mode 100644 index 0000000000..237c57320f --- /dev/null +++ b/app/src/main/res/drawable/ic_base_test_logo.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/values/palette.xml b/app/src/main/res/values/palette.xml index ff8f83d53d..5ee2b48c94 100644 --- a/app/src/main/res/values/palette.xml +++ b/app/src/main/res/values/palette.xml @@ -53,4 +53,5 @@ #232325 #7cca42 #FDD835 + #0052FF diff --git a/lib/src/main/java/com/alphawallet/ethereum/EthereumNetworkBase.java b/lib/src/main/java/com/alphawallet/ethereum/EthereumNetworkBase.java index 94d785a50c..1a79dc4c22 100644 --- a/lib/src/main/java/com/alphawallet/ethereum/EthereumNetworkBase.java +++ b/lib/src/main/java/com/alphawallet/ethereum/EthereumNetworkBase.java @@ -89,6 +89,10 @@ public abstract class EthereumNetworkBase public static final String LINEA_FREE_RPC = "https://linea.drpc.org"; public static final String LINEA_TEST_FREE_RPC = "https://rpc.goerli.linea.build"; + public static final String BASE_FREE_MAINNET_RPC = "https://base-rpc.publicnode.com"; + public static final String BASE_FREE_TESTNET_RPC = "https://base-sepolia-rpc.publicnode.com"; + + static Map networkMap = new LinkedHashMap() { { @@ -170,6 +174,11 @@ public abstract class EthereumNetworkBase LINEA_TEST_ID, false)); put(HOLESKY_ID, new NetworkInfo("Rootstock (Test)", "HolETH", HOLESKY_RPC_URL, "https://holesky.etherscan.io/tx/", HOLESKY_ID, false)); + + put(BASE_MAINNET_ID, new NetworkInfo("Base", "ETH", BASE_FREE_MAINNET_RPC, "https://basescan.org/tx/", + BASE_MAINNET_ID, false)); + put(BASE_TESTNET_ID, new NetworkInfo("Base (Test)", "ETH", BASE_FREE_TESTNET_RPC, "https://sepolia.basescan.org/tx/", + BASE_TESTNET_ID, false)); } }; From a30074788c4b5a14e1f00c0826f299a42b17e356 Mon Sep 17 00:00:00 2001 From: James Brown Date: Mon, 22 Apr 2024 13:37:44 +1000 Subject: [PATCH 2/2] Fix for not taking gas limit from Wallet Connect tx (#3379) --- .../java/com/alphawallet/app/service/GasService.java | 10 ++++++---- .../com/alphawallet/app/service/TickerService.java | 3 +++ .../app/ui/widget/entity/GasWidgetInterface.java | 1 + .../alphawallet/app/web3/entity/Web3Transaction.java | 3 ++- .../com/alphawallet/app/widget/ActionSheetDialog.java | 3 ++- .../java/com/alphawallet/app/widget/GasWidget.java | 6 ++++++ .../java/com/alphawallet/app/widget/GasWidget2.java | 9 +++++++-- 7 files changed, 27 insertions(+), 8 deletions(-) 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 db891d9ab7..c00f431dc3 100644 --- a/app/src/main/java/com/alphawallet/app/service/GasService.java +++ b/app/src/main/java/com/alphawallet/app/service/GasService.java @@ -380,6 +380,8 @@ public Single calculateGasEstimateInternal(byte[] transactionBytes, updateChainId(chainId); String finalTxData = txData; + BigInteger useGasLimit = defaultLimit.equals(BigInteger.ZERO) ? EthereumNetworkBase.getMaxGasLimit(chainId) : defaultLimit; + if ((toAddress.equals("") || toAddress.equals(ZERO_ADDRESS)) && txData.length() > 0) //Check gas for constructor { return networkRepository.getLastTransactionNonce(web3j, wallet.address) @@ -389,7 +391,7 @@ public Single calculateGasEstimateInternal(byte[] transactionBytes, else { return networkRepository.getLastTransactionNonce(web3j, wallet.address) - .flatMap(nonce -> ethEstimateGas(chainId, wallet.address, nonce, toAddress, amount, finalTxData)) + .flatMap(nonce -> ethEstimateGas(chainId, wallet.address, useGasLimit, nonce, toAddress, amount, finalTxData)) .flatMap(estimate -> handleOutOfGasError(estimate, chainId, toAddress, amount, finalTxData)) .map(estimate -> convertToGasLimit(estimate, defaultLimit)); } @@ -427,7 +429,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, EthereumNetworkBase.getMaxGasLimit(chainId), nonce, toAddress, amount, finalTxData)); } private BigInteger getLowGasPrice() @@ -443,14 +445,14 @@ private Single ethEstimateGas(String fromAddress, BigInteger non return Single.fromCallable(() -> web3j.ethEstimateGas(transaction).send()); } - private Single ethEstimateGas(long chainId, String fromAddress, BigInteger nonce, String toAddress, + private Single ethEstimateGas(long chainId, String fromAddress, BigInteger limit, BigInteger nonce, String toAddress, BigInteger amount, String txData) { final Transaction transaction = new Transaction ( fromAddress, nonce, currentGasPrice, - EthereumNetworkBase.getMaxGasLimit(chainId), + limit, toAddress, amount, txData); diff --git a/app/src/main/java/com/alphawallet/app/service/TickerService.java b/app/src/main/java/com/alphawallet/app/service/TickerService.java index 2524498b3e..785737dce6 100644 --- a/app/src/main/java/com/alphawallet/app/service/TickerService.java +++ b/app/src/main/java/com/alphawallet/app/service/TickerService.java @@ -4,6 +4,7 @@ import static com.alphawallet.ethereum.EthereumNetworkBase.ARBITRUM_MAIN_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.AURORA_MAINNET_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.AVALANCHE_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.BASE_MAINNET_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.BINANCE_MAIN_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.CLASSIC_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.CRONOS_MAIN_ID; @@ -837,6 +838,7 @@ private void resetTickerUpdate() put(CRONOS_MAIN_ID, "cronos"); put(ROOTSTOCK_MAINNET_ID, "rootstock"); put(LINEA_ID, "linea"); + put(BASE_MAINNET_ID, "base"); }}; // For now, don't use Dexguru unless we obtain API key @@ -875,6 +877,7 @@ public void deleteTickers() put(OKX_ID, "okb"); put(ROOTSTOCK_MAINNET_ID, "rootstock"); put(LINEA_ID, "ethereum"); + put(BASE_MAINNET_ID, "base"); }}; public static boolean validateCoinGeckoAPI(Token token) 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 27b30c6a11..90689dc04b 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 @@ -23,6 +23,7 @@ public interface GasWidgetInterface BigInteger getPriorityFee(); BigInteger getGasPrice(); void setGasEstimate(BigInteger estimate); + void setGasEstimateExact(BigInteger estimate); void onDestroy(); boolean checkSufficientGas(); void setupResendSettings(ActionSheetMode mode, BigInteger gasPrice); diff --git a/app/src/main/java/com/alphawallet/app/web3/entity/Web3Transaction.java b/app/src/main/java/com/alphawallet/app/web3/entity/Web3Transaction.java index 140555e078..265945e97d 100644 --- a/app/src/main/java/com/alphawallet/app/web3/entity/Web3Transaction.java +++ b/app/src/main/java/com/alphawallet/app/web3/entity/Web3Transaction.java @@ -197,7 +197,8 @@ public Web3Transaction( public Web3Transaction(WCEthereumTransaction wcTx, long callbackId, SignType signType) { String gasPrice = wcTx.getGasPrice() != null ? wcTx.getGasPrice() : "0"; - String gasLimit = wcTx.getGasLimit() != null ? wcTx.getGasLimit() : "0"; + //WC2 uses "gas" for gas limit + String gasLimit = wcTx.getGas() != null ? wcTx.getGas() : "0"; String nonce = wcTx.getNonce() != null ? wcTx.getNonce() : ""; this.recipient = TextUtils.isEmpty(wcTx.getTo()) ? Address.EMPTY : new Address(wcTx.getTo()); 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 61790836a2..b3b2cc3a3c 100644 --- a/app/src/main/java/com/alphawallet/app/widget/ActionSheetDialog.java +++ b/app/src/main/java/com/alphawallet/app/widget/ActionSheetDialog.java @@ -156,7 +156,8 @@ public ActionSheetDialog(@NonNull Activity activity, Web3Transaction tx, Token t if (!tx.gasLimit.equals(BigInteger.ZERO)) { - setGasEstimate(new GasEstimate(tx.gasLimit)); + gasWidgetInterface.setGasEstimateExact(tx.gasLimit); + functionBar.setPrimaryButtonEnabled(true); } updateAmount(); 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 1b0d57d372..b6b595a0c1 100644 --- a/app/src/main/java/com/alphawallet/app/widget/GasWidget.java +++ b/app/src/main/java/com/alphawallet/app/widget/GasWidget.java @@ -580,6 +580,12 @@ public void setGasEstimate(BigInteger estimate) estimate = estimate.multiply(BigInteger.valueOf(6)).divide(BigInteger.valueOf(5)); // increase estimate by 20% to be safe } + setGasEstimateExact(estimate); + } + + @Override + public void setGasEstimateExact(BigInteger estimate) + { //Override custom gas limit if (customGasLimit.equals(baseLineGasLimit)) { 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 477326a40c..b4b740ead4 100644 --- a/app/src/main/java/com/alphawallet/app/widget/GasWidget2.java +++ b/app/src/main/java/com/alphawallet/app/widget/GasWidget2.java @@ -534,12 +534,17 @@ public long getExpectedTransactionTime() */ public void setGasEstimate(BigInteger estimate) { - if (estimate.longValue() > C.GAS_LIMIT_MIN) //some kind of contract interaction + if (!customGasLimit.equals(estimate) && estimate.longValue() > C.GAS_LIMIT_MIN) //some kind of contract interaction { estimate = estimate.multiply(BigInteger.valueOf(6)).divide(BigInteger.valueOf(5)); // increase estimate by 20% to be safe } - //Override custom gas limit if required + setGasEstimateExact(estimate); + } + + @Override + public void setGasEstimateExact(BigInteger estimate) + { if (customGasLimit.equals(presetGasLimit)) { customGasLimit = estimate;