diff --git a/android/java/org/chromium/chrome/browser/crypto_wallet/activities/BuySendSwapActivity.java b/android/java/org/chromium/chrome/browser/crypto_wallet/activities/BuySendSwapActivity.java index a1b74fe60919..8807831e150e 100644 --- a/android/java/org/chromium/chrome/browser/crypto_wallet/activities/BuySendSwapActivity.java +++ b/android/java/org/chromium/chrome/browser/crypto_wallet/activities/BuySendSwapActivity.java @@ -50,6 +50,8 @@ import org.chromium.brave_wallet.mojom.EthJsonRpcController; import org.chromium.brave_wallet.mojom.EthTxController; import org.chromium.brave_wallet.mojom.EthTxControllerObserver; +import org.chromium.brave_wallet.mojom.EthereumChain; +import org.chromium.brave_wallet.mojom.GasEstimation1559; import org.chromium.brave_wallet.mojom.KeyringController; import org.chromium.brave_wallet.mojom.KeyringInfo; import org.chromium.brave_wallet.mojom.SwapController; @@ -58,6 +60,7 @@ import org.chromium.brave_wallet.mojom.TransactionInfo; import org.chromium.brave_wallet.mojom.TransactionStatus; import org.chromium.brave_wallet.mojom.TxData; +import org.chromium.brave_wallet.mojom.TxData1559; import org.chromium.chrome.R; import org.chromium.chrome.browser.crypto_wallet.AssetRatioControllerFactory; import org.chromium.chrome.browser.crypto_wallet.ERCTokenRegistryFactory; @@ -448,10 +451,31 @@ private void workWithSwapQuota(boolean success, SwapResponse response, String er TxData data = Utils.getTxData("", Utils.toWeiHex(response.gasPrice), Utils.toWeiHex(response.estimatedGas), response.to, Utils.toWeiHex(response.value), Utils.hexStrToNumberArray(response.data)); - addUnapprovedTransaction(data, from); + sendSwapTransaction(data, from); } } + private void sendSwapTransaction(TxData data, String from) { + assert mAssetRatioController != null; + mAssetRatioController.getGasOracle(estimation -> { + String maxPriorityFeePerGas = ""; + String maxFeePerGas = ""; + if (estimation.fastMaxPriorityFeePerGas.equals(estimation.avgMaxPriorityFeePerGas)) { + // Bump fast priority fee and max fee by 1 GWei if same as average fees. + maxPriorityFeePerGas = Utils.concatHexBN( + estimation.fastMaxPriorityFeePerGas, Utils.toHexWei("1", 9)); + maxFeePerGas = + Utils.concatHexBN(estimation.fastMaxFeePerGas, Utils.toHexWei("1", 9)); + } else { + // Always suggest fast gas fees as default + maxPriorityFeePerGas = estimation.fastMaxPriorityFeePerGas; + maxFeePerGas = estimation.fastMaxFeePerGas; + } + data.gasPrice = ""; + sendTransaction(data, from, maxPriorityFeePerGas, maxFeePerGas); + }); + } + private void updateSwapControls( SwapResponse response, boolean calculatePerSellAsset, String errorResponse) { EditText fromValueText = findViewById(R.id.from_value_text); @@ -794,7 +818,7 @@ private void adjustControls() { if (mCurrentErcToken == null || mCurrentErcToken.contractAddress.isEmpty()) { TxData data = Utils.getTxData("", "", "", to, Utils.toHexWei(value, 18), new byte[0]); - addUnapprovedTransaction(data, from); + sendTransaction(data, from, "", ""); } else { addUnapprovedTransactionERC20(to, Utils.toHexWei(value, mCurrentErcToken.decimals), from, @@ -1062,22 +1086,61 @@ private void activateErc20Allowance() { "", "", "", mCurrentErcToken.contractAddress, "0x0", data); String from = mCustomAccountAdapter.getTitleAtPosition( mAccountSpinner.getSelectedItemPosition()); - addUnapprovedTransaction(txData, from); + sendTransaction(txData, from, "", ""); }); } - private void addUnapprovedTransaction(TxData data, String from) { - assert mEthTxController != null; - if (mEthTxController == null) { - return; - } - mEthTxController.addUnapprovedTransaction(data, from, - (success, tx_meta_id, error_message) - -> { - // Do nothing here as we will receive an - // unapproved transaction in - // EthTxControllerObserverImpl - }); + private void sendTransaction( + TxData data, String from, String maxPriorityFeePerGas, String maxFeePerGas) { + assert mEthJsonRpcController != null; + mEthJsonRpcController.getAllNetworks(networks -> { + boolean isEIP1559 = false; + // We have hardcoded EIP-1559 gas fields. + if (!maxPriorityFeePerGas.isEmpty() && !maxFeePerGas.isEmpty()) { + isEIP1559 = true; + } else if (!data.gasPrice.isEmpty()) { + // We have hardcoded legacy tx gas fields. + isEIP1559 = false; + } + for (EthereumChain network : networks) { + if (!mCurrentChainId.equals(network.chainId)) { + continue; + } + isEIP1559 = network.isEip1559; + } + + assert mEthTxController != null; + if (isEIP1559) { + TxData1559 txData1559 = new TxData1559(); + txData1559.baseData = data; + txData1559.chainId = mCurrentChainId; + txData1559.maxPriorityFeePerGas = maxPriorityFeePerGas; + txData1559.maxFeePerGas = maxFeePerGas; + txData1559.gasEstimation = new GasEstimation1559(); + txData1559.gasEstimation.slowMaxPriorityFeePerGas = ""; + txData1559.gasEstimation.slowMaxFeePerGas = ""; + txData1559.gasEstimation.avgMaxPriorityFeePerGas = ""; + txData1559.gasEstimation.avgMaxFeePerGas = ""; + txData1559.gasEstimation.fastMaxPriorityFeePerGas = ""; + txData1559.gasEstimation.fastMaxFeePerGas = ""; + txData1559.gasEstimation.baseFeePerGas = ""; + mEthTxController.addUnapproved1559Transaction(txData1559, from, + (success, tx_meta_id, error_message) + -> { + // Do nothing here as we will receive an + // unapproved transaction in + // EthTxControllerObserverImpl + }); + } else { + mEthTxController.addUnapprovedTransaction(data, from, + (success, tx_meta_id, error_message) + -> { + // Do nothing here as we will receive an + // unapproved transaction in + // EthTxControllerObserverImpl + }); + } + }); } private void addUnapprovedTransactionERC20( @@ -1091,7 +1154,7 @@ private void addUnapprovedTransactionERC20( return; } TxData txData = Utils.getTxData("", "", "", contractAddress, "0x0", data); - addUnapprovedTransaction(txData, from); + sendTransaction(txData, from, "", ""); }); } diff --git a/android/java/org/chromium/chrome/browser/crypto_wallet/fragments/TxFragment.java b/android/java/org/chromium/chrome/browser/crypto_wallet/fragments/TxFragment.java index abc2c9e83644..79c65ebc4077 100644 --- a/android/java/org/chromium/chrome/browser/crypto_wallet/fragments/TxFragment.java +++ b/android/java/org/chromium/chrome/browser/crypto_wallet/fragments/TxFragment.java @@ -8,11 +8,15 @@ import android.app.Activity; import android.app.Dialog; import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.RadioGroup; import android.widget.TextView; import androidx.annotation.NonNull; @@ -37,6 +41,10 @@ public class TxFragment extends Fragment { private TransactionInfo mTxInfo; private String mAsset; private double mTotalPrice; + private boolean mIsEIP1559; + private int mCheckedPriorityId; + private int mPreviousCheckedPriorityId; + private double mEthRate; public static TxFragment newInstance(TransactionInfo txInfo, String asset, double totalPrice) { return new TxFragment(txInfo, asset, totalPrice); @@ -68,6 +76,10 @@ private TxFragment(TransactionInfo txInfo, String asset, double totalPrice) { mTxInfo = txInfo; mAsset = asset; mTotalPrice = totalPrice; + mEthRate = 0; + mCheckedPriorityId = -1; + mPreviousCheckedPriorityId = -1; + mIsEIP1559 = false; } @Override @@ -80,6 +92,8 @@ public void onCreate(@Nullable Bundle savedInstanceState) { public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { final View view = inflater.inflate(R.layout.fragment_transaction, container, false); + mIsEIP1559 = !mTxInfo.txData.maxPriorityFeePerGas.isEmpty() + && !mTxInfo.txData.maxFeePerGas.isEmpty(); setupView(view); @@ -90,19 +104,107 @@ public void onClick(View v) { final Dialog dialog = new Dialog(getActivity()); dialog.setContentView(R.layout.brave_wallet_edit_gas); dialog.show(); + mPreviousCheckedPriorityId = mCheckedPriorityId; - EditText gasFeeEdit = dialog.findViewById(R.id.gas_fee_edit); - gasFeeEdit.setText(String.format(Locale.getDefault(), "%.0f", - Utils.fromHexWei(mTxInfo.txData.baseData.gasPrice, 9))); + LinearLayout gasPriceLayout = dialog.findViewById(R.id.gas_price_layout); + LinearLayout gasLimitLayout = dialog.findViewById(R.id.gas_limit_layout); + if (!mIsEIP1559) { + EditText gasFeeEdit = dialog.findViewById(R.id.gas_fee_edit); + gasFeeEdit.setText(String.format(Locale.getDefault(), "%.0f", + Utils.fromHexWei(mTxInfo.txData.baseData.gasPrice, 9))); - EditText gasLimitEdit = dialog.findViewById(R.id.gas_limit_edit); - gasLimitEdit.setText(String.format(Locale.getDefault(), "%.0f", - Utils.fromHexGWeiToGWEI(mTxInfo.txData.baseData.gasLimit))); + EditText gasLimitEdit = dialog.findViewById(R.id.gas_limit_edit); + gasLimitEdit.setText(String.format(Locale.getDefault(), "%.0f", + Utils.fromHexGWeiToGWEI(mTxInfo.txData.baseData.gasLimit))); + } else { + TextView dialogTitle = dialog.findViewById(R.id.edit_gas_dialog_title); + dialogTitle.setText( + getResources().getString(R.string.wallet_max_priority_fee_title)); + gasPriceLayout.setVisibility(View.GONE); + gasLimitLayout.setVisibility(View.GONE); + dialog.findViewById(R.id.max_priority_fee_msg).setVisibility(View.VISIBLE); + dialog.findViewById(R.id.max_priority_radio_group).setVisibility(View.VISIBLE); + RadioGroup radioGroup = dialog.findViewById(R.id.max_priority_radio_group); + radioGroup.clearCheck(); + radioGroup.setOnCheckedChangeListener((group, checkedId) -> { + AssetRatioController assetRatioController = getAssetRatioController(); + assert assetRatioController != null; + assetRatioController.getGasOracle(estimation -> { + mTxInfo.txData.gasEstimation = estimation; + mCheckedPriorityId = checkedId; + String gasLimit = mTxInfo.txData.baseData.gasLimit; + String maxPriorityFeePerGas = mTxInfo.txData.maxPriorityFeePerGas; + String maxFeePerGas = mTxInfo.txData.maxFeePerGas; + TextView currentBaseFeeMsg = + dialog.findViewById(R.id.current_base_fee_msg); + currentBaseFeeMsg.setVisibility(View.GONE); + LinearLayout gasAmountLimitLayout = + dialog.findViewById(R.id.gas_amount_limit_layout); + gasAmountLimitLayout.setVisibility(View.GONE); + LinearLayout perGasTipLimitLayout = + dialog.findViewById(R.id.per_gas_tip_limit_layout); + perGasTipLimitLayout.setVisibility(View.GONE); + LinearLayout perGasPriceLimitLayout = + dialog.findViewById(R.id.per_gas_price_limit_layout); + perGasPriceLimitLayout.setVisibility(View.GONE); + if (mCheckedPriorityId == R.id.radio_low) { + maxPriorityFeePerGas = + mTxInfo.txData.gasEstimation.slowMaxPriorityFeePerGas; + maxFeePerGas = mTxInfo.txData.gasEstimation.slowMaxFeePerGas; + } else if (mCheckedPriorityId == R.id.radio_optimal) { + maxPriorityFeePerGas = + mTxInfo.txData.gasEstimation.avgMaxPriorityFeePerGas; + maxFeePerGas = mTxInfo.txData.gasEstimation.avgMaxFeePerGas; + } else if (mCheckedPriorityId == R.id.radio_high) { + maxPriorityFeePerGas = + mTxInfo.txData.gasEstimation.fastMaxPriorityFeePerGas; + maxFeePerGas = mTxInfo.txData.gasEstimation.fastMaxFeePerGas; + } else if (mCheckedPriorityId == R.id.radio_custom) { + currentBaseFeeMsg.setVisibility(View.VISIBLE); + currentBaseFeeMsg.setText(String.format( + getResources().getString(R.string.wallet_current_base_fee), + String.format(Locale.getDefault(), "%.0f", + Utils.fromHexWei( + mTxInfo.txData.gasEstimation.baseFeePerGas, + 9)))); + gasAmountLimitLayout.setVisibility(View.VISIBLE); + EditText gasAmountLimitEdit = + dialog.findViewById(R.id.gas_amount_limit_edit); + gasAmountLimitEdit.setText(String.format(Locale.getDefault(), + "%.0f", + Utils.fromHexGWeiToGWEI(mTxInfo.txData.baseData.gasLimit))); + perGasTipLimitLayout.setVisibility(View.VISIBLE); + EditText perGasTipLimitEdit = + dialog.findViewById(R.id.per_gas_tip_limit_edit); + perGasTipLimitEdit.setText(String.format(Locale.getDefault(), + "%.0f", Utils.fromHexWei(maxPriorityFeePerGas, 9))); + perGasPriceLimitLayout.setVisibility(View.VISIBLE); + EditText perGasPriceLimitEdit = + dialog.findViewById(R.id.per_gas_price_limit_edit); + perGasPriceLimitEdit.setText(String.format(Locale.getDefault(), + "%.0f", Utils.fromHexWei(maxFeePerGas, 9))); + filterEIP1559TextWatcher.setDialog(dialog); + gasAmountLimitEdit.addTextChangedListener(filterEIP1559TextWatcher); + perGasTipLimitEdit.addTextChangedListener(filterEIP1559TextWatcher); + perGasPriceLimitEdit.addTextChangedListener( + filterEIP1559TextWatcher); + } + fillMaxFee(dialog.findViewById(R.id.maximum_fee_msg), gasLimit, + maxPriorityFeePerGas, maxFeePerGas); + }); + }); + if (mCheckedPriorityId == -1) { + mCheckedPriorityId = R.id.radio_optimal; + } + radioGroup.check(mCheckedPriorityId); + dialog.findViewById(R.id.maximum_fee_msg).setVisibility(View.VISIBLE); + } Button cancel = dialog.findViewById(R.id.cancel); cancel.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { + mCheckedPriorityId = mPreviousCheckedPriorityId; dialog.dismiss(); } }); @@ -110,6 +212,7 @@ public void onClick(View v) { ok.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { + mPreviousCheckedPriorityId = mCheckedPriorityId; EthTxController ethTxController = getEthTxController(); assert ethTxController != null; if (ethTxController == null) { @@ -117,19 +220,64 @@ public void onClick(View v) { return; } - mTxInfo.txData.baseData.gasPrice = - Utils.toHexWei(gasFeeEdit.getText().toString(), 9); - mTxInfo.txData.baseData.gasLimit = - Utils.toHexWeiFromGWEI(gasLimitEdit.getText().toString()); - ethTxController.setGasPriceAndLimitForUnapprovedTransaction(mTxInfo.id, - mTxInfo.txData.baseData.gasPrice, mTxInfo.txData.baseData.gasLimit, - success -> { - if (!success) { - return; - } - setupView(view); - dialog.dismiss(); - }); + if (!mIsEIP1559) { + EditText gasLimitEdit = dialog.findViewById(R.id.gas_limit_edit); + mTxInfo.txData.baseData.gasLimit = + Utils.toHexWeiFromGWEI(gasLimitEdit.getText().toString()); + EditText gasFeeEdit = dialog.findViewById(R.id.gas_fee_edit); + mTxInfo.txData.baseData.gasPrice = + Utils.toHexWei(gasFeeEdit.getText().toString(), 9); + ethTxController.setGasPriceAndLimitForUnapprovedTransaction(mTxInfo.id, + mTxInfo.txData.baseData.gasPrice, + mTxInfo.txData.baseData.gasLimit, success -> { + if (!success) { + return; + } + setupView(view); + dialog.dismiss(); + }); + } else { + String gasLimit = mTxInfo.txData.baseData.gasLimit; + String maxPriorityFeePerGas = mTxInfo.txData.maxPriorityFeePerGas; + String maxFeePerGas = mTxInfo.txData.maxFeePerGas; + if (mCheckedPriorityId == R.id.radio_low) { + maxPriorityFeePerGas = + mTxInfo.txData.gasEstimation.slowMaxPriorityFeePerGas; + maxFeePerGas = mTxInfo.txData.gasEstimation.slowMaxFeePerGas; + } else if (mCheckedPriorityId == R.id.radio_optimal) { + maxPriorityFeePerGas = + mTxInfo.txData.gasEstimation.avgMaxPriorityFeePerGas; + maxFeePerGas = mTxInfo.txData.gasEstimation.avgMaxFeePerGas; + } else if (mCheckedPriorityId == R.id.radio_high) { + maxPriorityFeePerGas = + mTxInfo.txData.gasEstimation.fastMaxPriorityFeePerGas; + maxFeePerGas = mTxInfo.txData.gasEstimation.fastMaxFeePerGas; + } else if (mCheckedPriorityId == R.id.radio_custom) { + EditText gasAmountLimitEdit = + dialog.findViewById(R.id.gas_amount_limit_edit); + EditText perGasTipLimitEdit = + dialog.findViewById(R.id.per_gas_tip_limit_edit); + EditText perGasPriceLimitEdit = + dialog.findViewById(R.id.per_gas_price_limit_edit); + gasLimit = Utils.toHexWeiFromGWEI( + gasAmountLimitEdit.getText().toString()); + maxPriorityFeePerGas = + Utils.toHexWei(perGasTipLimitEdit.getText().toString(), 9); + maxFeePerGas = Utils.toHexWei( + perGasPriceLimitEdit.getText().toString(), 9); + } + mTxInfo.txData.baseData.gasLimit = gasLimit; + mTxInfo.txData.maxPriorityFeePerGas = maxPriorityFeePerGas; + mTxInfo.txData.maxFeePerGas = maxFeePerGas; + ethTxController.setGasFeeAndLimitForUnapprovedTransaction(mTxInfo.id, + maxPriorityFeePerGas, maxFeePerGas, gasLimit, success -> { + if (!success) { + return; + } + setupView(view); + dialog.dismiss(); + }); + } } }); } @@ -138,11 +286,57 @@ public void onClick(View v) { return view; } + private TextWatcherImpl filterEIP1559TextWatcher = new TextWatcherImpl(); + + private class TextWatcherImpl implements TextWatcher { + Dialog mDialog; + + public void setDialog(Dialog dialog) { + mDialog = dialog; + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + EditText gasAmountLimitEdit = mDialog.findViewById(R.id.gas_amount_limit_edit); + EditText perGasTipLimitEdit = mDialog.findViewById(R.id.per_gas_tip_limit_edit); + EditText perGasPriceLimitEdit = mDialog.findViewById(R.id.per_gas_price_limit_edit); + if (gasAmountLimitEdit.hasFocus() || perGasTipLimitEdit.hasFocus() + || perGasPriceLimitEdit.hasFocus()) { + fillMaxFee(mDialog.findViewById(R.id.maximum_fee_msg), + Utils.toHexWeiFromGWEI(gasAmountLimitEdit.getText().toString()), + Utils.toHexWei(perGasTipLimitEdit.getText().toString(), 9), + Utils.toHexWei(perGasPriceLimitEdit.getText().toString(), 9)); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void afterTextChanged(Editable s) {} + }; + + private void fillMaxFee( + TextView textView, String gasLimit, String maxPriorityFeePerGas, String maxFeePerGas) { + double totalGas = + Utils.fromHexWei(Utils.multiplyHexBN(gasLimit, + Utils.concatHexBN(maxPriorityFeePerGas, maxFeePerGas)), + 18); + double price = totalGas * mEthRate; + textView.setText(String.format(getResources().getString(R.string.wallet_maximum_fee), + String.format(Locale.getDefault(), "%.2f", price), + String.format(Locale.getDefault(), "%.8f", totalGas))); + } + private void setupView(View view) { TextView gasFeeAmount = view.findViewById(R.id.gas_fee_amount); - final double totalGas = - Utils.fromHexWei(Utils.multiplyHexBN(mTxInfo.txData.baseData.gasLimit, - mTxInfo.txData.baseData.gasPrice), + final double totalGas = mIsEIP1559 + ? Utils.fromHexWei(Utils.multiplyHexBN(mTxInfo.txData.baseData.gasLimit, + Utils.concatHexBN(mTxInfo.txData.maxPriorityFeePerGas, + mTxInfo.txData.maxFeePerGas)), + 18) + : Utils.fromHexWei(Utils.multiplyHexBN(mTxInfo.txData.baseData.gasLimit, + mTxInfo.txData.baseData.gasPrice), 18); gasFeeAmount.setText( String.format(getResources().getString(R.string.crypto_wallet_gas_fee_amount), @@ -165,8 +359,8 @@ private void setupView(View view) { if (!success || values.length == 0) { return; } - double price = Double.valueOf(values[0].price); - double totalPrice = totalGas * price; + mEthRate = Double.valueOf(values[0].price); + double totalPrice = totalGas * mEthRate; TextView gasFeeAmountFiat = view.findViewById(R.id.gas_fee_amount_fiat); gasFeeAmountFiat.setText(String.format( getResources().getString(R.string.crypto_wallet_amount_fiat), diff --git a/android/java/org/chromium/chrome/browser/crypto_wallet/util/Utils.java b/android/java/org/chromium/chrome/browser/crypto_wallet/util/Utils.java index 2df214d9a2c2..c186f0f3b465 100644 --- a/android/java/org/chromium/chrome/browser/crypto_wallet/util/Utils.java +++ b/android/java/org/chromium/chrome/browser/crypto_wallet/util/Utils.java @@ -527,6 +527,21 @@ public static String multiplyHexBN(String number1, String number2) { return "0x" + res.toString(16); } + public static String concatHexBN(String number1, String number2) { + if (number1.startsWith("0x")) { + number1 = number1.substring(2); + } + if (number2.startsWith("0x")) { + number2 = number2.substring(2); + } + BigInteger bigNumber1 = new BigInteger(number1, 16); + BigInteger bigNumber2 = new BigInteger(number2, 16); + + BigInteger res = bigNumber1.add(bigNumber2); + + return "0x" + res.toString(16); + } + public static byte[] hexStrToNumberArray(String value) { if (value.startsWith("0x")) { value = value.substring(2); diff --git a/android/java/res/layout/brave_wallet_edit_gas.xml b/android/java/res/layout/brave_wallet_edit_gas.xml index 8216b3f319a9..2e5d7220f9d1 100644 --- a/android/java/res/layout/brave_wallet_edit_gas.xml +++ b/android/java/res/layout/brave_wallet_edit_gas.xml @@ -6,7 +6,20 @@ android:layout_height="wrap_content" android:orientation="vertical" > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + %1$s0.0015 %2$sETH + %3$s0.00000015 ETH - Gas Price(GWEI) + Gas Price (Gwei) - Gas Limit(GWEI) + Gas Limit Disable Sharing Hub @@ -2549,6 +2549,42 @@ If you don't accept this request, VPN will not reconnect and your internet conne • No annoying ads + + Edit gas + + + Max Priority Fee + + + While not a guarantee, miners will likely prioritize your transaction if you pay a higher fee. + + + Low + + + Optimal + + + High + + + Custom + + + Maximum fee: ~$%1$s140.20 (%2$s0.000025 ETH) + + + Current base fee: %1$s150 Gwei + + + Gas amount limit + + + Per-gas tip limit (Gwei) + + + Per-gas price limit (Gwei) +