Skip to content

Commit

Permalink
feat(suite): add dust section to coin selection
Browse files Browse the repository at this point in the history
  • Loading branch information
komret committed Aug 18, 2022
1 parent b808432 commit d5be092
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 39 deletions.
22 changes: 14 additions & 8 deletions packages/components/src/components/form/Checkbox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { FONT_SIZE } from '../../../config/variables';
import { useTheme } from '../../../utils';
import { Icon } from '../../Icon';

const Wrapper = styled.div`
const Wrapper = styled.div<Pick<CheckboxProps, 'isDisabled'>>`
display: flex;
cursor: pointer;
cursor: ${({ isDisabled }) => (isDisabled ? 'default' : 'pointer')};
align-items: center;
:hover,
Expand All @@ -17,7 +17,7 @@ const Wrapper = styled.div`
}
`;

const IconWrapper = styled.div<Pick<CheckboxProps, 'isChecked'>>`
const IconWrapper = styled.div<Pick<CheckboxProps, 'isChecked' | 'isDisabled'>>`
display: flex;
justify-content: center;
align-items: center;
Expand All @@ -32,8 +32,10 @@ const IconWrapper = styled.div<Pick<CheckboxProps, 'isChecked'>>`
:hover,
:focus {
border: ${({ theme, isChecked }) =>
!isChecked && `2px solid ${darken(theme.HOVER_DARKEN_FILTER, theme.STROKE_GREY)}`};
border: ${({ theme, isChecked, isDisabled }) =>
!isChecked &&
!isDisabled &&
`2px solid ${darken(theme.HOVER_DARKEN_FILTER, theme.STROKE_GREY)}`};
}
`;

Expand All @@ -60,19 +62,23 @@ export interface CheckboxProps extends React.HTMLAttributes<HTMLDivElement> {
event: React.KeyboardEvent<HTMLElement> | React.MouseEvent<HTMLElement> | null,
) => any;
isChecked?: boolean;
isDisabled?: boolean;
}

export const Checkbox = ({ isChecked, children, onClick, ...rest }: CheckboxProps) => {
export const Checkbox = ({ isChecked, isDisabled, children, onClick, ...rest }: CheckboxProps) => {
const theme = useTheme();

const handleClick = isDisabled ? undefined : onClick;

return (
<Wrapper
onClick={onClick}
isDisabled={isDisabled}
onClick={handleClick}
onKeyUp={event => handleKeyboard(event, onClick)}
tabIndex={0}
{...rest}
>
<IconWrapper isChecked={isChecked}>
<IconWrapper isChecked={isChecked} isDisabled={isDisabled}>
{isChecked && <Icon size={24} color={theme.TYPE_WHITE} icon="CHECK" />}
</IconWrapper>

Expand Down
43 changes: 43 additions & 0 deletions packages/suite/src/components/wallet/UtxoSelectionList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as React from 'react';

import { getAccountTransactions } from '@suite-common/wallet-utils';
import { useSelector } from '@suite-hooks';
import type { AccountUtxo, CardanoInput, PROTO } from '@trezor/connect';
import { UtxoSelection } from '@wallet-components/UtxoSelection';
import { useSendFormContext } from '@wallet-hooks';

interface Props {
composedInputs: PROTO.TxInputType[] | CardanoInput[];
utxos: AccountUtxo[];
}

export const UtxoSelectionList = ({ composedInputs, utxos }: Props) => {
const { transactions } = useSelector(state => ({
transactions: state.wallet.transactions,
}));

const { account, selectedUtxos } = useSendFormContext();

const accountTransactions = getAccountTransactions(account.key, transactions.transactions);

return (
<>
{utxos.map(utxo => (
<UtxoSelection
key={`${utxo.txid}-${utxo.vout}`}
isChecked={
selectedUtxos.length
? selectedUtxos.some(u => u.txid === utxo.txid && u.vout === utxo.vout)
: composedInputs.some(
u => u.prev_hash === utxo.txid && u.prev_index === utxo.vout,
)
}
transaction={accountTransactions.find(
transaction => transaction.txid === utxo.txid,
)}
utxo={utxo}
/>
))}
</>
);
};
18 changes: 17 additions & 1 deletion packages/suite/src/support/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5201,11 +5201,27 @@ export default defineMessages({
TR_SELECTED: {
id: 'TR_SELECTED',
defaultMessage: '{amount} selected',
description: 'Number of list items selected',
},
TR_NO_SPENDABLE_UTXOS: {
id: 'TR_NO_SPENDABLE_UTXOS',
defaultMessage: 'There are no spendable UTXOs in your account.',
description: 'Message showing in Coin control section',
},
TR_CHANGE_ADDRESS: {
id: 'TR_CHANGE_ADDRESS',
defaultMessage: 'change address',
description: 'Address type',
description: 'Address type showing in Coin control section',
},
TR_DUST: {
id: 'TR_DUST',
defaultMessage: 'Unspendable outpusts (dust)',
description: 'Heading in Coin control section',
},
TR_DUST_DESCRIPTION: {
id: 'TR_DUST_DESCRIPTION',
defaultMessage: 'These outputs are likely smaller than the fee required to spend them.',
description: 'Sub-heading in Coin control section',
},
TR_CONNECTED_TO_PROVIDER: {
defaultMessage: 'Connected to {provider} as {user}',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from 'react';
import styled from 'styled-components';

import { formatNetworkAmount, getAccountTransactions } from '@suite-common/wallet-utils';
import { formatNetworkAmount } from '@suite-common/wallet-utils';
import { FormattedCryptoAmount, Translation } from '@suite-components';
import { useSelector } from '@suite-hooks';
import { Checkbox, Icon, Switch, variables } from '@trezor/components';
import { UtxoSelection } from '@wallet-components/UtxoSelection';
import type { AccountUtxo } from '@trezor/connect';
import { UtxoSelectionList } from '@wallet-components/UtxoSelectionList';
import { useSendFormContext } from '@wallet-hooks';

const Row = styled.div`
Expand All @@ -20,6 +20,16 @@ const SecondRow = styled(Row)`
margin-top: 12px;
`;

const DustRow = styled.div`
font-weight: ${variables.FONT_WEIGHT.MEDIUM};
margin-top: 6px;
`;

const DustDescriptionRow = styled.div`
color: ${props => props.theme.TYPE_LIGHT_GREY};
margin: 6px 0 12px 0;
`;

const StyledSwitch = styled(Switch)`
margin: 0 12px 0 auto;
`;
Expand All @@ -39,19 +49,24 @@ interface Props {
close: () => void;
}

export const UtxoSelectionList = ({ close }: Props) => {
const { transactions } = useSelector(state => ({
transactions: state.wallet.transactions,
}));
const { account, composedLevels, composeTransaction, selectedUtxos, setValue, watch } =
export const CoinControl = ({ close }: Props) => {
const { account, composedLevels, composeTransaction, feeInfo, selectedUtxos, setValue, watch } =
useSendFormContext();

const [spendableUtxos, dustUtxos]: [AccountUtxo[], AccountUtxo[]] = account.utxo
? account.utxo.reduce(
([previousSpendable, previousDust]: [AccountUtxo[], AccountUtxo[]], current) =>
feeInfo.dustLimit && parseInt(current.amount, 10) >= feeInfo.dustLimit
? [[...previousSpendable, current], previousDust]
: [previousSpendable, [...previousDust, current]],
[[], []],
)
: [[], []];
const selectedFee = watch('selectedFee');
const composedLevel = composedLevels?.[selectedFee || 'normal'];
const composedInputs = composedLevel?.type === 'final' ? composedLevel.transaction.inputs : [];
const inputs = composedInputs.length ? composedInputs : selectedUtxos;
const allSelected = selectedUtxos.length === account.utxo?.length;
const accountTransactions = getAccountTransactions(account.key, transactions.transactions);
const allSelected = !!selectedUtxos.length && selectedUtxos.length === spendableUtxos.length;

// TypeScript does not allow Array.prototype.reduce here (https://github.com/microsoft/TypeScript/issues/36390)
let total = 0;
Expand All @@ -76,7 +91,7 @@ export const UtxoSelectionList = ({ close }: Props) => {
composeTransaction();
};
const handleCheckbox = () => {
setValue('selectedUtxos', allSelected ? [] : account.utxo);
setValue('selectedUtxos', allSelected ? [] : spendableUtxos);
composeTransaction();
};

Expand All @@ -92,27 +107,32 @@ export const UtxoSelectionList = ({ close }: Props) => {
<Icon size={20} icon="CROSS" onClick={close} />
</Row>
<SecondRow>
<Checkbox isChecked={allSelected} onClick={handleCheckbox} />
<Checkbox
isChecked={allSelected}
isDisabled={!spendableUtxos.length}
onClick={handleCheckbox}
/>
<Translation id="TR_SELECTED" values={{ amount: inputs.length }} />
{!!total && <StyledCryptoAmount value={formattedTotal} symbol={account.symbol} />}
</SecondRow>
<Line />
{account.utxo?.map(utxo => (
<UtxoSelection
key={`${utxo.txid}-${utxo.vout}`}
isChecked={
selectedUtxos.length
? selectedUtxos.some(u => u.txid === utxo.txid && u.vout === utxo.vout)
: composedInputs.some(
u => u.prev_hash === utxo.txid && u.prev_index === utxo.vout,
)
}
transaction={accountTransactions.find(
transaction => transaction.txid === utxo.txid,
)}
utxo={utxo}
/>
))}
{spendableUtxos.length ? (
<UtxoSelectionList utxos={spendableUtxos} composedInputs={composedInputs} />
) : (
<Translation id="TR_NO_SPENDABLE_UTXOS" />
)}
{!!dustUtxos.length && (
<>
<Line />
<DustRow>
<Translation id="TR_DUST" />
</DustRow>
<DustDescriptionRow>
<Translation id="TR_DUST_DESCRIPTION" />
</DustDescriptionRow>
<UtxoSelectionList utxos={dustUtxos} composedInputs={composedInputs} />
</>
)}
<Line />
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useSendFormContext } from '@wallet-hooks';
import { isEnabled as isFeatureEnabled } from '@suite-common/suite-utils';
import { OpenGuideFromTooltip } from '@guide-components';
import { Locktime } from './components/Locktime';
import { UtxoSelectionList } from './components/UtxoSelectionList';
import { CoinControl } from './components/CoinControl';

const Wrapper = styled.div`
display: flex;
Expand Down Expand Up @@ -61,7 +61,7 @@ export const BitcoinOptions = () => {
return (
<Wrapper>
{utxoSelectionEnabled && (
<UtxoSelectionList
<CoinControl
close={() => {
resetDefaultValue('utxoSelection');
toggleOption('utxoSelection');
Expand Down

0 comments on commit d5be092

Please sign in to comment.