feat(wallet): Custom Network Fee Popup (#30427)
This commit is contained in:
@@ -220,6 +220,13 @@ inline constexpr webui::LocalizedString kLocalizedStrings[] = {
|
||||
{"braveWalletEnableTransactionSimulation",
|
||||
IDS_BRAVE_WALLET_ENABLE_TRANSACTION_SIMULATION},
|
||||
{"braveWalletNetworkFees", IDS_BRAVE_WALLET_NETWORK_FEES},
|
||||
{"braveWalletCustomFeeAmount", IDS_BRAVE_WALLET_CUSTOM_FEE_AMOUNT},
|
||||
{"braveWalletGasTipLimit", IDS_BRAVE_WALLET_GAS_TIP_LIMIT},
|
||||
{"braveWalletGasPriceLimit", IDS_BRAVE_WALLET_GAS_PRICE_LIMIT},
|
||||
{"braveWalletGasPrice", IDS_BRAVE_WALLET_GAS_PRICE},
|
||||
{"braveWalletEditGasEstimatedNetworkFee",
|
||||
IDS_BRAVE_WALLET_EDIT_GAS_ESTIMATED_NETWORK_FEE},
|
||||
{"braveWalletUseDefault", IDS_BRAVE_WALLET_USE_DEFAULT},
|
||||
{"braveWalletNetworkFee", IDS_BRAVE_WALLET_NETWORK_FEE},
|
||||
{"braveWalletSolanaSysvarRentProgram",
|
||||
IDS_BRAVE_WALLET_SOLANA_SYSVAR_RENT_PROGRAM},
|
||||
|
||||
@@ -718,7 +718,7 @@ export class MockedWalletApiProxy {
|
||||
assetTimeframeChange: '1',
|
||||
fromAsset: fromAssets[0],
|
||||
toAsset: toAssets[0],
|
||||
price: '1234.56',
|
||||
price: '3873.78',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2025 The Brave Authors. All rights reserved.
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
import * as React from 'react'
|
||||
|
||||
// Mock Data
|
||||
import {
|
||||
mockTransactionInfo, //
|
||||
} from '../../../../stories/mock-data/mock-transaction-info'
|
||||
import { mockEthMainnet } from '../../../../stories/mock-data/mock-networks'
|
||||
|
||||
// Utils
|
||||
import { getLocale } from '../../../../../common/locale'
|
||||
|
||||
// Components
|
||||
import { CustomNetworkFee } from './custom_network_fee'
|
||||
import {
|
||||
WalletPanelStory, //
|
||||
} from '../../../../stories/wrappers/wallet-panel-story-wrapper'
|
||||
import { BottomSheet } from '../../../shared/bottom_sheet/bottom_sheet'
|
||||
|
||||
export const _CustomNetworkFee = {
|
||||
render: () => {
|
||||
return (
|
||||
<BottomSheet
|
||||
isOpen={true}
|
||||
title={getLocale('braveWalletCustomFeeAmount')}
|
||||
onClose={() => alert('Close Clicked')}
|
||||
>
|
||||
<CustomNetworkFee
|
||||
transactionInfo={mockTransactionInfo}
|
||||
selectedNetwork={mockEthMainnet}
|
||||
baseFeePerGas='3641000000' // (3.641 gwei)
|
||||
onUpdateCustomNetworkFee={() => alert('Update Clicked')}
|
||||
onBack={() => alert('Back Clicked')}
|
||||
onClose={() => alert('Close Clicked')}
|
||||
/>
|
||||
</BottomSheet>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
title: 'Wallet/Panel/Components/Edit Network Fee',
|
||||
component: _CustomNetworkFee,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
decorators: [
|
||||
(Story: any) => (
|
||||
<WalletPanelStory>
|
||||
<Story />
|
||||
</WalletPanelStory>
|
||||
),
|
||||
],
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) 2025 The Brave Authors. All rights reserved.
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
import styled from 'styled-components'
|
||||
import * as leo from '@brave/leo/tokens/css/variables'
|
||||
|
||||
// Shared Styles
|
||||
import { Column, Row, Text } from '../../../shared/style'
|
||||
|
||||
export const StyledWrapper = styled(Column)`
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
export const Card = styled(Column)`
|
||||
background-color: ${leo.color.container.highlight};
|
||||
border-radius: ${leo.radius.xl};
|
||||
`
|
||||
|
||||
export const Description = styled(Text)`
|
||||
font: ${leo.font.small.semibold};
|
||||
letter-spacing: ${leo.typography.letterSpacing.small};
|
||||
`
|
||||
|
||||
export const SectionLabel = styled(Text)`
|
||||
font: ${leo.font.small.semibold};
|
||||
letter-spacing: ${leo.typography.letterSpacing.small};
|
||||
`
|
||||
|
||||
export const InputWrapper = styled(Row)<{
|
||||
hasError?: boolean
|
||||
}>`
|
||||
cursor: pointer;
|
||||
background-color: ${leo.color.container.background};
|
||||
outline: 1px solid
|
||||
${(p) =>
|
||||
p.hasError
|
||||
? leo.color.systemfeedback.errorVibrant
|
||||
: leo.color.divider.subtle};
|
||||
transition:
|
||||
outline 0.1s ease-in-out,
|
||||
box-shadow 0.1s ease-in-out;
|
||||
border-radius: ${leo.radius.m};
|
||||
:hover {
|
||||
outline: 1px solid ${leo.color.divider.strong};
|
||||
box-shadow: ${leo.effect.elevation['02']};
|
||||
}
|
||||
:focus-within {
|
||||
outline: 2px solid
|
||||
${(p) =>
|
||||
p.hasError
|
||||
? leo.color.systemfeedback.errorVibrant
|
||||
: leo.color.primary[40]};
|
||||
}
|
||||
`
|
||||
|
||||
export const Input = styled.input`
|
||||
font: ${leo.font.small.regular};
|
||||
letter-spacing: ${leo.typography.letterSpacing.small};
|
||||
background-color: ${leo.color.container.background};
|
||||
color: ${leo.color.text.primary};
|
||||
outline: none;
|
||||
border: none;
|
||||
width: 100%;
|
||||
padding: 0px;
|
||||
text-align: right;
|
||||
::placeholder {
|
||||
font: ${leo.font.small.regular};
|
||||
letter-spacing: ${leo.typography.letterSpacing.small};
|
||||
color: ${leo.color.text.tertiary};
|
||||
}
|
||||
:focus {
|
||||
outline: none;
|
||||
}
|
||||
::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
`
|
||||
|
||||
export const InputLabel = styled(Text)`
|
||||
font: ${leo.font.small.regular};
|
||||
letter-spacing: ${leo.typography.letterSpacing.small};
|
||||
`
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
// Copyright (c) 2025 The Brave Authors. All rights reserved.
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
import * as React from 'react'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { Provider } from 'react-redux'
|
||||
|
||||
// Utils
|
||||
import {
|
||||
// eslint-disable-next-line import/no-named-default
|
||||
default as BraveCoreThemeProvider,
|
||||
} from '../../../../../common/BraveCoreThemeProvider'
|
||||
import { createMockStore } from '../../../../utils/test-utils'
|
||||
|
||||
// Components
|
||||
import { CustomNetworkFee } from './custom_network_fee'
|
||||
|
||||
// Mock data
|
||||
import {
|
||||
mockTransactionInfo, //
|
||||
} from '../../../../stories/mock-data/mock-transaction-info'
|
||||
import { mockEthMainnet } from '../../../../stories/mock-data/mock-networks'
|
||||
|
||||
describe('CustomNetworkFee', () => {
|
||||
const mockBaseFeePerGas = '20000000000' // 20 Gwei in Wei
|
||||
|
||||
const mockOnUpdateCustomNetworkFee = jest.fn()
|
||||
const mockOnBack = jest.fn()
|
||||
const mockOnClose = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render custom network fee component correctly', async () => {
|
||||
const store = createMockStore({})
|
||||
const { container } = render(
|
||||
<Provider store={store}>
|
||||
<BraveCoreThemeProvider>
|
||||
<CustomNetworkFee
|
||||
transactionInfo={mockTransactionInfo}
|
||||
selectedNetwork={mockEthMainnet}
|
||||
baseFeePerGas={mockBaseFeePerGas}
|
||||
onUpdateCustomNetworkFee={mockOnUpdateCustomNetworkFee}
|
||||
onBack={mockOnBack}
|
||||
onClose={mockOnClose}
|
||||
/>
|
||||
</BraveCoreThemeProvider>
|
||||
</Provider>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container).toBeVisible()
|
||||
|
||||
// Check for description text
|
||||
expect(
|
||||
screen.getByText('braveWalletEditGasDescription'),
|
||||
).toBeInTheDocument()
|
||||
|
||||
// Check for gas limit input
|
||||
expect(screen.getByText('braveWalletEditGasLimit')).toBeInTheDocument()
|
||||
|
||||
// Check for base fee display (for EIP1559 transactions)
|
||||
expect(screen.getByText('braveWalletEditGasBaseFee')).toBeInTheDocument()
|
||||
|
||||
// Check for gas tip limit (for EIP1559 transactions)
|
||||
expect(screen.getByText('braveWalletGasTipLimit')).toBeInTheDocument()
|
||||
|
||||
// Check for gas price limit (for EIP1559 transactions)
|
||||
expect(screen.getByText('braveWalletGasPriceLimit')).toBeInTheDocument()
|
||||
|
||||
// Check for update button
|
||||
expect(screen.getByText('braveWalletUpdate')).toBeInTheDocument()
|
||||
|
||||
// Check for use default button (for EIP1559 transactions)
|
||||
expect(screen.getByText('braveWalletUseDefault')).toBeInTheDocument()
|
||||
|
||||
// Check for gas limit error
|
||||
const gasLimitInput = screen.getByTestId('gas-limit-input')
|
||||
fireEvent.change(gasLimitInput, { target: { value: '0' } })
|
||||
expect(screen.getByText('braveWalletEditGasLimitError')).toBeVisible()
|
||||
|
||||
// Check for gas price limit error
|
||||
const gasPriceLimitInput = screen.getByTestId('gas-price-limit-input')
|
||||
fireEvent.change(gasPriceLimitInput, { target: { value: '0' } })
|
||||
expect(
|
||||
screen.getByText('braveWalletGasFeeLimitLowerThanBaseFeeWarning'),
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
it('should render non-EIP1559 transaction correctly', async () => {
|
||||
const nonEip1559TransactionInfo = {
|
||||
...mockTransactionInfo,
|
||||
txDataUnion: {
|
||||
ethTxData1559: undefined,
|
||||
ethTxData: {
|
||||
nonce: '0x1',
|
||||
gasPrice: '0x59682f00',
|
||||
gasLimit: '0x5208',
|
||||
to: '0x0987654321098765432109876543210987654321',
|
||||
value: '0x0',
|
||||
data: [],
|
||||
signOnly: false,
|
||||
signedTransaction: undefined,
|
||||
},
|
||||
solanaTxData: undefined,
|
||||
filTxData: undefined,
|
||||
btcTxData: undefined,
|
||||
zecTxData: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
const store = createMockStore({})
|
||||
const { container } = render(
|
||||
<Provider store={store}>
|
||||
<BraveCoreThemeProvider>
|
||||
<CustomNetworkFee
|
||||
transactionInfo={nonEip1559TransactionInfo}
|
||||
selectedNetwork={mockEthMainnet}
|
||||
baseFeePerGas={mockBaseFeePerGas}
|
||||
onUpdateCustomNetworkFee={mockOnUpdateCustomNetworkFee}
|
||||
onBack={mockOnBack}
|
||||
onClose={mockOnClose}
|
||||
/>
|
||||
</BraveCoreThemeProvider>
|
||||
</Provider>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container).toBeVisible()
|
||||
|
||||
// Check for gas price input (for non-EIP1559 transactions)
|
||||
expect(screen.getByText('braveWalletGasPrice')).toBeInTheDocument()
|
||||
|
||||
// Should not show EIP1559 specific fields
|
||||
expect(
|
||||
screen.queryByText('braveWalletEditGasBaseFee'),
|
||||
).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByText('braveWalletGasTipLimit'),
|
||||
).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByText('braveWalletGasPriceLimit'),
|
||||
).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByText('braveWalletUseDefault'),
|
||||
).not.toBeInTheDocument()
|
||||
|
||||
// Check for gas price error
|
||||
const gasPriceInput = screen.getByTestId('gas-price-input')
|
||||
fireEvent.change(gasPriceInput, { target: { value: '0' } })
|
||||
expect(
|
||||
screen.getByText('braveWalletEditGasZeroGasPriceWarning'),
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
+482
@@ -0,0 +1,482 @@
|
||||
// Copyright (c) 2025 The Brave Authors. All rights reserved.
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
import * as React from 'react'
|
||||
import { skipToken } from '@reduxjs/toolkit/query/react'
|
||||
import Button from '@brave/leo/react/button'
|
||||
|
||||
// Types
|
||||
import {
|
||||
BraveWallet,
|
||||
SerializableTransactionInfo,
|
||||
} from '../../../../constants/types'
|
||||
import {
|
||||
UpdateUnapprovedTransactionGasFieldsType, //
|
||||
} from '../../../../common/constants/action_types'
|
||||
|
||||
// Queries
|
||||
import {
|
||||
querySubscriptionOptions60s, //
|
||||
} from '../../../../common/slices/constants'
|
||||
import {
|
||||
useGetDefaultFiatCurrencyQuery,
|
||||
useGetTokenSpotPricesQuery,
|
||||
} from '../../../../common/slices/api.slice'
|
||||
|
||||
// Utils
|
||||
import { getLocale } from '../../../../../common/locale'
|
||||
import {
|
||||
parseTransactionFeesWithoutPrices, //
|
||||
} from '../../../../utils/tx-utils'
|
||||
import { makeNetworkAsset } from '../../../../options/asset-options'
|
||||
import {
|
||||
getPriceIdForToken,
|
||||
getTokenPriceAmountFromRegistry,
|
||||
} from '../../../../utils/pricing-utils'
|
||||
import Amount from '../../../../utils/amount'
|
||||
|
||||
// Styled Components
|
||||
import { Column, Row, VerticalDivider } from '../../../shared/style'
|
||||
import {
|
||||
StyledWrapper,
|
||||
Card,
|
||||
SectionLabel,
|
||||
Input,
|
||||
InputLabel,
|
||||
InputWrapper,
|
||||
Description,
|
||||
} from './custom_network_fee.styles'
|
||||
|
||||
interface Props {
|
||||
transactionInfo: SerializableTransactionInfo
|
||||
selectedNetwork: BraveWallet.NetworkInfo
|
||||
baseFeePerGas: string
|
||||
onUpdateCustomNetworkFee: (
|
||||
payload: UpdateUnapprovedTransactionGasFieldsType,
|
||||
) => void
|
||||
onBack: () => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function CustomNetworkFee(props: Props) {
|
||||
const {
|
||||
transactionInfo,
|
||||
baseFeePerGas,
|
||||
selectedNetwork,
|
||||
onUpdateCustomNetworkFee,
|
||||
onBack,
|
||||
onClose,
|
||||
} = props
|
||||
|
||||
// Memos
|
||||
const transactionFees = React.useMemo(
|
||||
() => parseTransactionFeesWithoutPrices(transactionInfo),
|
||||
[transactionInfo],
|
||||
)
|
||||
const { isEIP1559Transaction } = transactionFees
|
||||
|
||||
const networkAsset = React.useMemo(() => {
|
||||
return makeNetworkAsset(selectedNetwork)
|
||||
}, [selectedNetwork])
|
||||
|
||||
const networkTokenPriceIds = React.useMemo(
|
||||
() => (networkAsset ? [getPriceIdForToken(networkAsset)] : []),
|
||||
[networkAsset],
|
||||
)
|
||||
|
||||
// Queries
|
||||
const { data: defaultFiatCurrency } = useGetDefaultFiatCurrencyQuery()
|
||||
|
||||
const { data: spotPriceRegistry } = useGetTokenSpotPricesQuery(
|
||||
networkTokenPriceIds.length && defaultFiatCurrency
|
||||
? { ids: networkTokenPriceIds, toCurrency: defaultFiatCurrency }
|
||||
: skipToken,
|
||||
querySubscriptionOptions60s,
|
||||
)
|
||||
|
||||
// State
|
||||
const [gasLimit, setGasLimit] = React.useState<string>(
|
||||
transactionFees.gasLimit,
|
||||
)
|
||||
const [gasPrice, setGasPrice] = React.useState<string>(
|
||||
new Amount(transactionFees.gasPrice)
|
||||
.divideByDecimals(9) // Wei-per-gas → GWei-per-gas conversion
|
||||
.format(),
|
||||
)
|
||||
const [maxPriorityFeePerGas, setMaxPriorityFeePerGas] =
|
||||
React.useState<string>(
|
||||
new Amount(transactionFees.maxPriorityFeePerGas)
|
||||
.divideByDecimals(9) // Wei-per-gas → GWei-per-gas conversion
|
||||
.format(),
|
||||
)
|
||||
const [maxFeePerGas, setMaxFeePerGas] = React.useState<string>(
|
||||
new Amount(transactionFees.maxFeePerGas)
|
||||
.divideByDecimals(9) // Wei-per-gas → GWei-per-gas conversion
|
||||
.format(),
|
||||
)
|
||||
|
||||
// Methods
|
||||
const handleGasLimitInputChanged = (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
setGasLimit(event.target.value)
|
||||
}
|
||||
|
||||
const handleMaxPriorityFeePerGasInputChanged = (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const value = event.target.value
|
||||
setMaxPriorityFeePerGas(value)
|
||||
|
||||
// GWei-per-gas → Wei-per-gas conversion
|
||||
const maxPriorityFeePerGasWei = new Amount(value).multiplyByDecimals(9)
|
||||
|
||||
const computedMaxFeePerGasWei = new Amount(baseFeePerGas).plus(
|
||||
maxPriorityFeePerGasWei,
|
||||
)
|
||||
|
||||
const computedMaxFeePerGasGWei = computedMaxFeePerGasWei
|
||||
.divideByDecimals(9) // Wei-per-gas → GWei-per-gas conversion
|
||||
.format()
|
||||
|
||||
setMaxFeePerGas(computedMaxFeePerGasGWei)
|
||||
}
|
||||
|
||||
const handleMaxFeePerGasInputChanged = (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
setMaxFeePerGas(event.target.value)
|
||||
}
|
||||
|
||||
const handleGasPriceInputChanged = (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
setGasPrice(event.target.value)
|
||||
}
|
||||
|
||||
const onClickUpdate = React.useCallback(() => {
|
||||
if (!isEIP1559Transaction) {
|
||||
onUpdateCustomNetworkFee({
|
||||
chainId: transactionInfo.chainId,
|
||||
txMetaId: transactionInfo.id,
|
||||
gasPrice: new Amount(gasPrice).multiplyByDecimals(9).toHex(),
|
||||
gasLimit: new Amount(gasLimit).toHex(),
|
||||
})
|
||||
|
||||
onClose()
|
||||
return
|
||||
}
|
||||
|
||||
onUpdateCustomNetworkFee({
|
||||
chainId: transactionInfo.chainId,
|
||||
txMetaId: transactionInfo.id,
|
||||
maxPriorityFeePerGas: new Amount(maxPriorityFeePerGas)
|
||||
.multiplyByDecimals(9)
|
||||
.toHex(),
|
||||
maxFeePerGas: new Amount(maxFeePerGas).multiplyByDecimals(9).toHex(),
|
||||
gasLimit: new Amount(gasLimit).toHex(),
|
||||
})
|
||||
|
||||
onClose()
|
||||
}, [
|
||||
gasPrice,
|
||||
gasLimit,
|
||||
maxPriorityFeePerGas,
|
||||
maxFeePerGas,
|
||||
transactionInfo,
|
||||
onClose,
|
||||
onUpdateCustomNetworkFee,
|
||||
isEIP1559Transaction,
|
||||
])
|
||||
|
||||
// Computed / Memos
|
||||
const isCustomGasBelowBaseFee =
|
||||
isEIP1559Transaction
|
||||
&& new Amount(maxFeePerGas).multiplyByDecimals(9).lt(baseFeePerGas)
|
||||
|
||||
const customEIP1559GasFee = new Amount(maxFeePerGas)
|
||||
.multiplyByDecimals(9) // GWei-per-gas → Wei-per-gas conversion
|
||||
.times(gasLimit) // Wei-per-gas → Wei
|
||||
.divideByDecimals(selectedNetwork.decimals) // Wei → ETH conversion
|
||||
.format(6)
|
||||
|
||||
const customEIP1559FiatGasFee =
|
||||
customEIP1559GasFee
|
||||
&& spotPriceRegistry
|
||||
&& new Amount(customEIP1559GasFee)
|
||||
.times(getTokenPriceAmountFromRegistry(spotPriceRegistry, networkAsset))
|
||||
.formatAsFiat(defaultFiatCurrency)
|
||||
|
||||
const isUpdateButtonDisabled = React.useMemo(() => {
|
||||
if (gasLimit === '') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (new Amount(gasLimit).lte(0)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!isEIP1559Transaction && gasPrice === '') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (
|
||||
!isEIP1559Transaction
|
||||
&& new Amount(gasPrice).multiplyByDecimals(9).isNegative()
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (isEIP1559Transaction && maxFeePerGas === '') {
|
||||
return true
|
||||
}
|
||||
|
||||
return (
|
||||
isEIP1559Transaction
|
||||
&& new Amount(maxPriorityFeePerGas).multiplyByDecimals(9).isNegative()
|
||||
)
|
||||
}, [
|
||||
gasLimit,
|
||||
isEIP1559Transaction,
|
||||
gasPrice,
|
||||
maxFeePerGas,
|
||||
maxPriorityFeePerGas,
|
||||
])
|
||||
const isZeroGasPrice = React.useMemo(() => {
|
||||
return (
|
||||
!isEIP1559Transaction
|
||||
&& gasPrice !== ''
|
||||
&& new Amount(gasPrice).multiplyByDecimals(9).isZero()
|
||||
)
|
||||
}, [gasPrice, isEIP1559Transaction])
|
||||
|
||||
// Effects
|
||||
React.useEffect(() => {
|
||||
const maxPriorityFeePerGasWei = new Amount(
|
||||
maxPriorityFeePerGas,
|
||||
).multiplyByDecimals(9) // GWei-per-gas → Wei conversion
|
||||
|
||||
const maxFeePerGasWeiValue = new Amount(baseFeePerGas).plus(
|
||||
maxPriorityFeePerGasWei,
|
||||
)
|
||||
|
||||
setMaxFeePerGas(
|
||||
maxFeePerGasWeiValue
|
||||
.divideByDecimals(9) // Wei-per-gas → GWei-per-gas conversion
|
||||
.format(),
|
||||
)
|
||||
}, [maxPriorityFeePerGas, baseFeePerGas])
|
||||
|
||||
// render
|
||||
return (
|
||||
<StyledWrapper
|
||||
width='100%'
|
||||
height='100%'
|
||||
justifyContent='space-between'
|
||||
padding='16px'
|
||||
gap='16px'
|
||||
>
|
||||
<Description
|
||||
textColor='tertiary'
|
||||
textAlign='left'
|
||||
>
|
||||
{getLocale('braveWalletEditGasDescription')}
|
||||
</Description>
|
||||
<Card
|
||||
width='100%'
|
||||
padding={isEIP1559Transaction ? '16px 16px 8px 16px' : '16px'}
|
||||
gap='8px'
|
||||
>
|
||||
{isEIP1559Transaction && (
|
||||
<>
|
||||
<Row
|
||||
justifyContent='space-between'
|
||||
alignItems='flex-start'
|
||||
padding='8px 0px'
|
||||
>
|
||||
<SectionLabel textColor='secondary'>
|
||||
{getLocale('braveWalletEditGasBaseFee')}
|
||||
</SectionLabel>
|
||||
<SectionLabel textColor='primary'>
|
||||
{new Amount(baseFeePerGas).divideByDecimals(9).format()}{' '}
|
||||
{getLocale('braveWalletEditGasGwei')}
|
||||
</SectionLabel>
|
||||
</Row>
|
||||
<VerticalDivider />
|
||||
</>
|
||||
)}
|
||||
<Row justifyContent='space-between'>
|
||||
<SectionLabel textColor='secondary'>
|
||||
{getLocale('braveWalletEditGasLimit')}
|
||||
</SectionLabel>
|
||||
<InputWrapper
|
||||
width='140px'
|
||||
padding='8px 12px'
|
||||
hasError={gasLimit === '0'}
|
||||
>
|
||||
<Input
|
||||
placeholder='0'
|
||||
type='number'
|
||||
min={0}
|
||||
value={gasLimit}
|
||||
onChange={handleGasLimitInputChanged}
|
||||
data-testid='gas-limit-input'
|
||||
/>
|
||||
</InputWrapper>
|
||||
</Row>
|
||||
|
||||
{gasLimit === '0' && (
|
||||
<Row justifyContent='flex-start'>
|
||||
<InputLabel
|
||||
textColor='error'
|
||||
textAlign='left'
|
||||
>
|
||||
{getLocale('braveWalletEditGasLimitError')}
|
||||
</InputLabel>
|
||||
</Row>
|
||||
)}
|
||||
<VerticalDivider />
|
||||
{!isEIP1559Transaction && (
|
||||
<>
|
||||
<Row justifyContent='space-between'>
|
||||
<SectionLabel textColor='secondary'>
|
||||
{getLocale('braveWalletGasPrice')}
|
||||
</SectionLabel>
|
||||
<InputWrapper
|
||||
width='140px'
|
||||
gap='6px'
|
||||
padding='8px 12px'
|
||||
hasError={isZeroGasPrice}
|
||||
>
|
||||
<Input
|
||||
placeholder='0'
|
||||
type='number'
|
||||
value={gasPrice}
|
||||
onChange={handleGasPriceInputChanged}
|
||||
data-testid='gas-price-input'
|
||||
/>
|
||||
<InputLabel textColor='tertiary'>
|
||||
{getLocale('braveWalletEditGasGwei')}
|
||||
</InputLabel>
|
||||
</InputWrapper>
|
||||
</Row>
|
||||
{isZeroGasPrice && (
|
||||
<Row justifyContent='flex-start'>
|
||||
<InputLabel
|
||||
textColor='error'
|
||||
textAlign='left'
|
||||
>
|
||||
{getLocale('braveWalletEditGasZeroGasPriceWarning')}
|
||||
</InputLabel>
|
||||
</Row>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isEIP1559Transaction && (
|
||||
<>
|
||||
<Row justifyContent='space-between'>
|
||||
<SectionLabel textColor='secondary'>
|
||||
{getLocale('braveWalletGasTipLimit')}
|
||||
</SectionLabel>
|
||||
<InputWrapper
|
||||
width='140px'
|
||||
gap='6px'
|
||||
padding='8px 12px'
|
||||
hasError={isCustomGasBelowBaseFee}
|
||||
>
|
||||
<Input
|
||||
placeholder='0'
|
||||
type='number'
|
||||
min={0}
|
||||
value={maxPriorityFeePerGas}
|
||||
onChange={handleMaxPriorityFeePerGasInputChanged}
|
||||
/>
|
||||
<InputLabel textColor='tertiary'>
|
||||
{getLocale('braveWalletEditGasGwei')}
|
||||
</InputLabel>
|
||||
</InputWrapper>
|
||||
</Row>
|
||||
<VerticalDivider />
|
||||
<Row justifyContent='space-between'>
|
||||
<SectionLabel textColor='secondary'>
|
||||
{getLocale('braveWalletGasPriceLimit')}
|
||||
</SectionLabel>
|
||||
<InputWrapper
|
||||
width='140px'
|
||||
gap='6px'
|
||||
padding='8px 12px'
|
||||
hasError={isCustomGasBelowBaseFee}
|
||||
>
|
||||
<Input
|
||||
placeholder='0'
|
||||
min={0}
|
||||
type='number'
|
||||
value={maxFeePerGas}
|
||||
onChange={handleMaxFeePerGasInputChanged}
|
||||
data-testid='gas-price-limit-input'
|
||||
/>
|
||||
<InputLabel textColor='tertiary'>
|
||||
{getLocale('braveWalletEditGasGwei')}
|
||||
</InputLabel>
|
||||
</InputWrapper>
|
||||
</Row>
|
||||
{isCustomGasBelowBaseFee && (
|
||||
<Row justifyContent='flex-start'>
|
||||
<InputLabel
|
||||
textColor='error'
|
||||
textAlign='left'
|
||||
>
|
||||
{getLocale('braveWalletGasFeeLimitLowerThanBaseFeeWarning')}
|
||||
</InputLabel>
|
||||
</Row>
|
||||
)}
|
||||
<VerticalDivider />
|
||||
<Row
|
||||
justifyContent='space-between'
|
||||
alignItems='flex-start'
|
||||
padding='8px 0px'
|
||||
>
|
||||
<SectionLabel textColor='secondary'>
|
||||
{getLocale('braveWalletEditGasEstimatedNetworkFee')}
|
||||
</SectionLabel>
|
||||
<Column alignItems='flex-end'>
|
||||
<SectionLabel textColor='primary'>
|
||||
~{customEIP1559FiatGasFee}
|
||||
</SectionLabel>
|
||||
<SectionLabel textColor='primary'>
|
||||
~
|
||||
{new Amount(customEIP1559GasFee).formatAsAsset(
|
||||
6,
|
||||
networkAsset.symbol,
|
||||
)}
|
||||
</SectionLabel>
|
||||
</Column>
|
||||
</Row>
|
||||
<VerticalDivider />
|
||||
<Row width='unset'>
|
||||
<Button
|
||||
kind='plain-faint'
|
||||
size='tiny'
|
||||
onClick={onBack}
|
||||
>
|
||||
{getLocale('braveWalletUseDefault')}
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
<Row>
|
||||
<Button
|
||||
isDisabled={isUpdateButtonDisabled}
|
||||
onClick={onClickUpdate}
|
||||
>
|
||||
{getLocale('braveWalletUpdate')}
|
||||
</Button>
|
||||
</Row>
|
||||
</StyledWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomNetworkFee
|
||||
+26
-4
@@ -12,6 +12,12 @@ import {
|
||||
SuggestedMaxPriorityFeeSelector, //
|
||||
} from './suggested_max_priority_fee_selector'
|
||||
|
||||
// Utils
|
||||
import Amount from '../../../../utils/amount'
|
||||
import {
|
||||
parseTransactionFeesWithoutPrices, //
|
||||
} from '../../../../utils/tx-utils'
|
||||
|
||||
// Mocks
|
||||
import { mockEthMainnet } from '../../../../stories/mock-data/mock-networks'
|
||||
import {
|
||||
@@ -48,6 +54,16 @@ jest.mock('../../../../common/slices/api.slice', () => {
|
||||
})
|
||||
|
||||
describe('SuggestedMaxPriorityFeeSelector', () => {
|
||||
const baseFeePerGas = '0x59682f00' // 1500000000 wei (1.5 gwei)
|
||||
const transactionFees = parseTransactionFeesWithoutPrices(mockTransactionInfo)
|
||||
const suggestedFees = mockSuggestedMaxPriorityFeeOptions.map((option) =>
|
||||
new Amount(baseFeePerGas)
|
||||
.plus(option.fee)
|
||||
.times(transactionFees.gasLimit) // Wei-per-gas → Wei conversion
|
||||
.divideByDecimals(mockEthMainnet.decimals) // Wei → ETH conversion
|
||||
.format(4),
|
||||
)
|
||||
|
||||
const renderComponent = () => {
|
||||
const store = createMockStore({})
|
||||
return render(
|
||||
@@ -56,7 +72,7 @@ describe('SuggestedMaxPriorityFeeSelector', () => {
|
||||
<SuggestedMaxPriorityFeeSelector
|
||||
transactionInfo={mockTransactionInfo}
|
||||
selectedNetwork={mockEthMainnet}
|
||||
baseFeePerGas={'0x59682f00'} // 1500000000 wei (1.5 gwei)
|
||||
baseFeePerGas={baseFeePerGas}
|
||||
suggestedMaxPriorityFee='average'
|
||||
suggestedMaxPriorityFeeOptions={mockSuggestedMaxPriorityFeeOptions}
|
||||
setSuggestedMaxPriorityFee={() => {}}
|
||||
@@ -82,9 +98,15 @@ describe('SuggestedMaxPriorityFeeSelector', () => {
|
||||
expect(screen.getByText('1 min')).toBeInTheDocument()
|
||||
|
||||
// Check if gas fees are displayed
|
||||
expect(screen.getByText('0.003048 ETH')).toBeInTheDocument()
|
||||
expect(screen.getByText('0.01171 ETH')).toBeInTheDocument()
|
||||
expect(screen.getByText('0.02664 ETH')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText(suggestedFees[0] + ' ' + mockEthMainnet.symbol),
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText(suggestedFees[1] + ' ' + mockEthMainnet.symbol),
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText(suggestedFees[2] + ' ' + mockEthMainnet.symbol),
|
||||
).toBeInTheDocument()
|
||||
|
||||
// Check if custom button is rendered
|
||||
expect(screen.getByText('braveWalletCustom')).toBeInTheDocument()
|
||||
|
||||
@@ -826,6 +826,7 @@ provideStrings({
|
||||
braveWalletTransactionGasFeeCap: 'Gas Fee Cap',
|
||||
braveWalletNetworkFees: 'Network fees',
|
||||
braveWalletNetworkFee: 'Network fee',
|
||||
braveWalletCustomFeeAmount: 'Custom fee amount',
|
||||
braveWalletTransactionMayIncludeAccountCreationFee:
|
||||
'This transaction may include an account creation fee',
|
||||
braveWalletSystemProgramAssignWarningTitle:
|
||||
@@ -1052,6 +1053,11 @@ provideStrings({
|
||||
braveWalletGasFeeLimitLowerThanBaseFeeWarning:
|
||||
'Fee limit is set lower than the base fee. '
|
||||
+ 'Your transaction may take a long time or fail.',
|
||||
braveWalletGasTipLimit: 'Gas tip limit',
|
||||
braveWalletGasPriceLimit: 'Gas price limit',
|
||||
braveWalletGasPrice: 'Gas price',
|
||||
braveWalletEditGasEstimatedNetworkFee: 'Estimated network fee',
|
||||
braveWalletUseDefault: 'Use default',
|
||||
|
||||
// Advanced transaction settings
|
||||
braveWalletAdvancedTransactionSettings: 'Advanced settings',
|
||||
|
||||
@@ -37,8 +37,8 @@ export const mockTransactionInfo: SerializableTransactionInfo = {
|
||||
ethTxData1559: {
|
||||
baseData: {
|
||||
nonce: '0x1',
|
||||
gasPrice: '150',
|
||||
gasLimit: '21000',
|
||||
gasPrice: '100000000',
|
||||
gasLimit: '122665', // wei
|
||||
to: '2',
|
||||
value: '0x15ddf09c97b0000',
|
||||
data: Array.from(new Uint8Array(24)),
|
||||
@@ -46,8 +46,8 @@ export const mockTransactionInfo: SerializableTransactionInfo = {
|
||||
signedTransaction: undefined,
|
||||
},
|
||||
chainId: '0x0',
|
||||
maxPriorityFeePerGas: '1',
|
||||
maxFeePerGas: '1',
|
||||
maxPriorityFeePerGas: '80410000', // (0.08041 gwei)
|
||||
maxFeePerGas: '3600000000', // (3.6 gwei)
|
||||
gasEstimation: undefined,
|
||||
},
|
||||
ethTxData: undefined,
|
||||
|
||||
@@ -1088,6 +1088,12 @@
|
||||
<message name="IDS_BRAVE_WALLET_AUTO_DISCOVERY_EMPTY_STATE_REFRESH" desc="NFTs tab empty state refreshing text">Refreshing</message>
|
||||
<message name="IDS_BRAVE_WALLET_NETWORK_FEES" desc="Label for blockchain network fees">Network fees</message>
|
||||
<message name="IDS_BRAVE_WALLET_NETWORK_FEE" desc="Label for blockchain network fee">Network fee</message>
|
||||
<message name="IDS_BRAVE_WALLET_CUSTOM_FEE_AMOUNT" desc="Label for custom fee amount">Custom fee amount</message>
|
||||
<message name="IDS_BRAVE_WALLET_GAS_TIP_LIMIT" desc="Label for gas tip limit">Gas tip limit</message>
|
||||
<message name="IDS_BRAVE_WALLET_GAS_PRICE_LIMIT" desc="Label for gas price limit">Gas price limit</message>
|
||||
<message name="IDS_BRAVE_WALLET_GAS_PRICE" desc="Label for gas price">Gas price</message>
|
||||
<message name="IDS_BRAVE_WALLET_EDIT_GAS_ESTIMATED_NETWORK_FEE" desc="Label for estimated network fee">Estimated network fee</message>
|
||||
<message name="IDS_BRAVE_WALLET_USE_DEFAULT" desc="Label for use default">Use default</message>
|
||||
<message name="IDS_WALLET_EIP6963_PROVIDER_NAME" desc="Provider name which will be return when announce ethereum provider">Brave Wallet</message>
|
||||
<message name="IDS_BRAVE_WALLET_RECEIVE" desc="Receive asset event title">Receive</message>
|
||||
<message name="IDS_BRAVE_WALLET_FROM" desc="From address label">From</message>
|
||||
|
||||
Reference in New Issue
Block a user