Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ const PERCENTAGE_BUTTONS = [
value: 50,
},
{
label: '90%',
value: 90,
label: 'Max',
value: 100,
},
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,23 @@ import { useTransactionPayToken } from '../pay/useTransactionPayToken';
import { useUpdateTokenAmount } from './useUpdateTokenAmount';
import { TransactionMeta } from '@metamask/transaction-controller';
import { useParams } from '../../../../../util/navigation/navUtils';
import {
TransactionToken,
useTransactionRequiredTokens,
} from '../pay/useTransactionRequiredTokens';
import { NATIVE_TOKEN_ADDRESS } from '../../constants/tokens';
import { Hex } from '@metamask/utils';

jest.mock('../tokens/useTokenFiatRates');
jest.mock('../transactions/useUpdateTokenAmount');
jest.mock('../pay/useTransactionPayToken');
jest.mock('../useTokenAmount');
jest.mock('../../../../../util/navigation/navUtils');
jest.mock('../pay/useTransactionRequiredTokens');

jest.useFakeTimers();

const TOKEN_ADDRESS_MOCK = '0x1234567890123456789012345678901234567890' as Hex;
const TOKEN_TRANSFER_DATA =
'0xa9059cbb0000000000000000000000005a52e96bacdabb82fd05763e25335261b270efcb0000000000000000000000000000000000000000000000004563918244f40000';

Expand Down Expand Up @@ -51,6 +59,9 @@ describe('useTransactionCustomAmount', () => {
const useUpdateTokenAmountMock = jest.mocked(useUpdateTokenAmount);
const useTransactionPayTokenMock = jest.mocked(useTransactionPayToken);
const useParamsMock = jest.mocked(useParams);
const useTransactionRequiredTokensMock = jest.mocked(
useTransactionRequiredTokens,
);

const updateTokenAmountMock: ReturnType<
typeof useUpdateTokenAmount
Expand All @@ -66,10 +77,15 @@ describe('useTransactionCustomAmount', () => {
} as ReturnType<typeof useUpdateTokenAmountMock>);

useTransactionPayTokenMock.mockReturnValue({
payToken: { tokenFiatAmount: 1234.56 },
payToken: {
address: TOKEN_ADDRESS_MOCK,
chainId: '0x1' as Hex,
tokenFiatAmount: 1234.56,
},
} as ReturnType<typeof useTransactionPayToken>);

useParamsMock.mockReturnValue({});
useTransactionRequiredTokensMock.mockReturnValue([]);
});

it('returns pending amount provided by updatePendingAmount', async () => {
Expand Down Expand Up @@ -177,16 +193,6 @@ describe('useTransactionCustomAmount', () => {
expect(updateTokenAmountMock).toHaveBeenCalledWith('61.725');
});

it('updatePendingAmountPercentage updates amount fiat to percentage of token balance', async () => {
const { result } = runHook();

await act(async () => {
result.current.updatePendingAmountPercentage(43);
});

expect(result.current.amountFiat).toBe('530.86');
});

it('returns default amount from params if available', async () => {
useParamsMock.mockReturnValue({ amount: '43.21' });

Expand Down Expand Up @@ -242,4 +248,87 @@ describe('useTransactionCustomAmount', () => {

expect(result.current.hasInput).toBe(false);
});

describe('updatePendingAmountPercentage updates amount fiat', () => {
it('to percentage of token balance', async () => {
const { result } = runHook();

await act(async () => {
result.current.updatePendingAmountPercentage(43);
});

expect(result.current.amountFiat).toBe('530.86');
});

it('minus buffers if 100', async () => {
useTransactionRequiredTokensMock.mockReturnValue([
{},
{},
] as TransactionToken[]);

const { result } = runHook();

await act(async () => {
result.current.updatePendingAmountPercentage(100);
});

expect(result.current.amountFiat).toBe('1141.96');
});

it('minus additional buffer if 100 and pay token is native', async () => {
useTransactionRequiredTokensMock.mockReturnValue([
{},
{},
] as TransactionToken[]);

useTransactionPayTokenMock.mockReturnValue({
payToken: {
address: NATIVE_TOKEN_ADDRESS as Hex,
tokenFiatAmount: 1234.56,
},
} as ReturnType<typeof useTransactionPayToken>);

const { result } = runHook();

await act(async () => {
result.current.updatePendingAmountPercentage(100);
});

expect(result.current.amountFiat).toBe('1111.1');
});

it('minus no buffer if 100 but pay token matches required token', async () => {
useTransactionRequiredTokensMock.mockReturnValue([
{
address: TOKEN_ADDRESS_MOCK,
},
{},
] as TransactionToken[]);

useParamsMock.mockReturnValue({ amount: '43.21' });

const { result } = runHook();

await act(async () => {
result.current.updatePendingAmountPercentage(100);
});

expect(result.current.amountFiat).toBe('1234.56');
});

it('minus no buffer if 100 but all required tokens have sufficient balance and skipIfBalance', async () => {
useTransactionRequiredTokensMock.mockReturnValue([
{ skipIfBalance: true, amountRaw: '1', balanceRaw: '1' },
{ skipIfBalance: true, amountRaw: '1', balanceRaw: '1' },
] as TransactionToken[]);

const { result } = runHook();

await act(async () => {
result.current.updatePendingAmountPercentage(100);
});

expect(result.current.amountFiat).toBe('1234.56');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import { useUpdateTokenAmount } from './useUpdateTokenAmount';
import { getTokenTransferData } from '../../utils/transaction-pay';
import { useParams } from '../../../../../util/navigation/navUtils';
import { debounce } from 'lodash';
import { useSelector } from 'react-redux';
import { selectMetaMaskPayFlags } from '../../../../../selectors/featureFlagController/confirmations';
import { useTransactionRequiredTokens } from '../pay/useTransactionRequiredTokens';
import { getNativeTokenAddress } from '@metamask/assets-controllers';

export const MAX_LENGTH = 28;
const DEBOUNCE_DELAY = 500;
Expand All @@ -19,6 +23,7 @@ export function useTransactionCustomAmount() {
const [isInputChanged, setInputChanged] = useState(false);
const [hasInput, setHasInput] = useState(false);
const [amountHumanDebounced, setAmountHumanDebounced] = useState('0');
const maxPercentage = useMaxPercentage();

const debounceSetAmountDelayed = useMemo(
() =>
Expand Down Expand Up @@ -81,15 +86,17 @@ export function useTransactionCustomAmount() {
return;
}

const newAmount = new BigNumber(percentage)
const finalPercentage = percentage === 100 ? maxPercentage : percentage;

const newAmount = new BigNumber(finalPercentage)
.dividedBy(100)
.multipliedBy(tokenFiatAmount)
.decimalPlaces(2, BigNumber.ROUND_HALF_UP)
.decimalPlaces(2, BigNumber.ROUND_DOWN)
.toString(10);

updatePendingAmount(newAmount);
},
[tokenFiatAmount, updatePendingAmount],
[maxPercentage, tokenFiatAmount, updatePendingAmount],
);

const updateTokenAmount = useCallback(() => {
Expand All @@ -108,6 +115,47 @@ export function useTransactionCustomAmount() {
};
}

function useMaxPercentage() {
const featureFlags = useSelector(selectMetaMaskPayFlags);
const requiredTokens = useTransactionRequiredTokens();
const { payToken } = useTransactionPayToken();
const { chainId } = useTransactionMetadataRequest() ?? { chainId: '0x0' };

return useMemo(() => {
// Assumes we're not targetting native tokens.
if (
payToken?.chainId === chainId &&
payToken?.address.toLowerCase() ===
requiredTokens[0]?.address?.toLowerCase()
) {
return 100;
}

const requiredQuoteCount = requiredTokens.filter(
(token) =>
!token.skipIfBalance ||
new BigNumber(token.balanceRaw).lt(token.amountRaw),
).length;

let bufferPercentage =
requiredQuoteCount > 0 ? featureFlags.bufferInitial : 0;

if (requiredQuoteCount > 1) {
bufferPercentage +=
featureFlags.bufferSubsequent * (requiredQuoteCount - 1);
}

if (
payToken?.address === getNativeTokenAddress(payToken?.chainId ?? '0x0')
) {
// Cannot calculate gas cost yet so just add an additional buffer if pay token is native
bufferPercentage += featureFlags.bufferInitial;
}

return 100 - bufferPercentage * 100;
}, [chainId, featureFlags, payToken, requiredTokens]);
}

function getTokenAddress(transactionMeta: TransactionMeta | undefined): Hex {
const nestedCall = transactionMeta && getTokenTransferData(transactionMeta);

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@
"@metamask/swappable-obj-proxy": "^2.1.0",
"@metamask/swaps-controller": "^14.0.0",
"@metamask/token-search-discovery-controller": "^3.1.0",
"@metamask/transaction-controller": "^60.6.0",
"@metamask/transaction-controller": "^60.9.0",
"@metamask/tron-wallet-snap": "^1.4.0",
"@metamask/utils": "^11.8.1",
"@ngraveio/bc-ur": "^1.1.6",
Expand Down
10 changes: 5 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8834,9 +8834,9 @@ __metadata:
languageName: node
linkType: hard

"@metamask/transaction-controller@npm:^60.6.0, @metamask/transaction-controller@npm:^60.7.0":
version: 60.7.0
resolution: "@metamask/transaction-controller@npm:60.7.0"
"@metamask/transaction-controller@npm:^60.7.0, @metamask/transaction-controller@npm:^60.9.0":
version: 60.9.0
resolution: "@metamask/transaction-controller@npm:60.9.0"
dependencies:
"@ethereumjs/common": ^4.4.0
"@ethereumjs/tx": ^5.4.0
Expand Down Expand Up @@ -8866,7 +8866,7 @@ __metadata:
"@metamask/gas-fee-controller": ^24.0.0
"@metamask/network-controller": ^24.0.0
"@metamask/remote-feature-flag-controller": ^1.5.0
checksum: b68795b84ba99e46ea97a6e5e65ff1cd98b57f275f69bcc8cb419af950d434e7fe0eaa77515e5ece1fc65c9af1224fc22fe358619c85c254f75018e7b8a81c6d
checksum: 0cf4105e5e4fcce8518a0535c013fc4036d3709c25179b8708ba908fb9cd7998f259645e53ea80ecd5df4b0f631f8606972b652f199b79a803c2575ecdaba0c5
languageName: node
linkType: hard

Expand Down Expand Up @@ -34175,7 +34175,7 @@ __metadata:
"@metamask/test-dapp-multichain": ^0.17.1
"@metamask/test-dapp-solana": ^0.3.0
"@metamask/token-search-discovery-controller": ^3.1.0
"@metamask/transaction-controller": ^60.6.0
"@metamask/transaction-controller": ^60.9.0
"@metamask/tron-wallet-snap": ^1.4.0
"@metamask/utils": ^11.8.1
"@ngraveio/bc-ur": ^1.1.6
Expand Down
Loading