Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[POC + WIP] feat(send): utxo selection #4281

Closed
wants to merge 1 commit into from
Closed
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 @@ -58,11 +58,14 @@ export const composeTransaction =
}

const baseFee = formValues.rbfParams ? formValues.rbfParams.baseFee : 0;
const selectedUtxos =
formValues.options.includes('utxoSelection') &&
formValues.selectedUtxos?.map(u => ({ ...u, required: true }));
const params = {
account: {
path: account.path,
addresses: account.addresses,
utxo: account.utxo,
utxo: selectedUtxos || account.utxo,
},
feeLevels: predefinedLevels,
baseFee,
Expand Down
64 changes: 64 additions & 0 deletions packages/suite/src/components/wallet/UtxoSelection/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as React from 'react';
import styled from 'styled-components';
import { RadioButton, variables } from '@trezor/components';
import { FormattedCryptoAmount } from '@suite-components';
import { formatNetworkAmount } from '@wallet-utils/accountUtils';
import type { AccountUtxo } from 'trezor-connect';
import type { Network } from '@wallet-types';

const Wrapper = styled.div`
margin-bottom: 25px;
`;

const Item = styled.div`
display: flex;
align-items: center;
padding: 8px 0px;
`;

const Details = styled.div`
display: flex;
flex-direction: column;
`;

const DetailsRow = styled.div`
font-weight: ${variables.FONT_WEIGHT.MEDIUM};
font-size: ${variables.FONT_SIZE.SMALL};
color: ${props => props.theme.TYPE_LIGHT_GREY};
`;

interface Props {
utxos: AccountUtxo[];
selectedUtxos: AccountUtxo[];
symbol: Network['symbol'];
toggleUtxoSelection: (utxo: AccountUtxo) => void;
}

const UtxoSelection = (props: Props) => (
<Wrapper>
{props.utxos.map(utxo => (
<Item>
<RadioButton
onClick={() => props.toggleUtxoSelection(utxo)}
isChecked={
!!props.selectedUtxos.find(
u => u.txid === utxo.txid && u.vout === utxo.vout,
)
}
/>
<Details>
<FormattedCryptoAmount
value={formatNetworkAmount(utxo.amount, props.symbol)}
symbol={props.symbol}
/>
<DetailsRow>Address: {utxo.address}</DetailsRow>
<DetailsRow>Path: {utxo.path}</DetailsRow>
<DetailsRow>TXID: {utxo.txid}</DetailsRow>
<DetailsRow>Confirmations: {utxo.confirmations}</DetailsRow>
</Details>
</Item>
))}
</Wrapper>
);

export default UtxoSelection;
34 changes: 34 additions & 0 deletions packages/suite/src/hooks/wallet/form/useUtxoSelection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useEffect } from 'react';
import { UseFormMethods } from 'react-hook-form';
import type { AccountUtxo } from 'trezor-connect';
import type { FormState } from '@wallet-types/sendForm';

type Props = UseFormMethods<{
selectedUtxos?: FormState['selectedUtxos'];
}> & {
composeRequest: (field?: string) => void;
};

// shareable sub-hook used in useRbfForm and useSendForm (TODO)

export const useUtxoSelection = ({ composeRequest, setValue, getValues, register }: Props) => {
// register custom form field (without HTMLElement)
useEffect(() => {
register({ name: 'selectedUtxos', type: 'custom' });
}, [register]);

const toggleUtxoSelection = (utxo: AccountUtxo) => {
const { selectedUtxos } = getValues();
const isSelected = selectedUtxos?.find(u => u.txid === utxo.txid && u.vout === utxo.vout);
if (isSelected) {
setValue('selectedUtxos', selectedUtxos?.filter(u => u !== isSelected) ?? []);
} else {
setValue('selectedUtxos', (selectedUtxos || []).concat([utxo]));
}
composeRequest();
};

return {
toggleUtxoSelection,
};
};
8 changes: 8 additions & 0 deletions packages/suite/src/hooks/wallet/useSendForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { useSendFormFields } from './useSendFormFields';
import { useSendFormCompose } from './useSendFormCompose';
import { useSendFormImport } from './useSendFormImport';
import { useFees } from './form/useFees';
import { useUtxoSelection } from './form/useUtxoSelection';

export const SendContext = createContext<SendContextValues | null>(null);
SendContext.displayName = 'SendContext';
Expand Down Expand Up @@ -182,6 +183,12 @@ export const useSendForm = (props: UseSendFormProps): SendContextValues => {
...useFormMethods,
});

// sub-hook
const { toggleUtxoSelection } = useUtxoSelection({
composeRequest,
...useFormMethods,
});

const resetContext = useCallback(() => {
setComposedLevels(undefined);
removeDraft(); // reset draft
Expand Down Expand Up @@ -280,6 +287,7 @@ export const useSendForm = (props: UseSendFormProps): SendContextValues => {
updateContext,
resetContext,
changeFeeLevel,
toggleUtxoSelection,
composeTransaction: composeRequest,
loadTransaction,
signTransaction: sign,
Expand Down
4 changes: 4 additions & 0 deletions packages/suite/src/types/wallet/sendForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
FeeLevel,
TokenInfo,
ComposeOutput,
AccountUtxo,
PrecomposedTransaction as PrecomposedTransactionBase,
} from 'trezor-connect';
import { AppState, ExtendedMessageDescriptor } from '@suite-types';
Expand All @@ -27,6 +28,7 @@ export type Output = {

export type FormOptions =
| 'broadcast'
| 'utxoSelection'
| 'bitcoinRBF'
| 'bitcoinLockTime'
| 'ethereumData'
Expand All @@ -49,6 +51,7 @@ export type FormState = {
ethereumDataHex?: string;
rippleDestinationTag?: string;
rbfParams?: RbfTransactionParams;
selectedUtxos?: AccountUtxo[];
};

export interface FeeInfo {
Expand Down Expand Up @@ -173,6 +176,7 @@ export type SendContextValues = Omit<UseFormMethods<FormState>, 'register'> &
calculateFiat: (outputIndex: number, amount?: string) => void;
setAmount: (outputIndex: number, amount: string) => void;
changeFeeLevel: (currentLevel: FeeLevel['label']) => void;
toggleUtxoSelection: (utxo: AccountUtxo) => void;
resetDefaultValue: (field: string) => void;
setMax: (index: number, active: boolean) => void;
getDefaultValue: GetDefaultValue;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';
import styled from 'styled-components';
import { useSendFormContext } from '@wallet-hooks';
import { Icon, variables } from '@trezor/components';
import UtxoSelectionBase from '@wallet-components/UtxoSelection';

const Wrapper = styled.div`
margin-bottom: 25px;
`;

const Title = styled.div`
font-weight: ${variables.FONT_WEIGHT.MEDIUM};
font-size: ${variables.FONT_SIZE.NORMAL};
color: ${props => props.theme.TYPE_DARK_GREY};
display: flex;
flex-direction: row;
justify-content: space-between;
`;

const Description = styled.div`
font-weight: ${variables.FONT_WEIGHT.MEDIUM};
font-size: ${variables.FONT_SIZE.SMALL};
color: ${props => props.theme.TYPE_LIGHT_GREY};
`;

interface Props {
close: () => void;
}

const UtxoSelection = ({ close }: Props) => {
const { account, getDefaultValue, toggleUtxoSelection } = useSendFormContext();

const selectedUtxos = getDefaultValue('selectedUtxos', []);

return (
<Wrapper>
<Title>
<span>Coin selection</span>
<Icon size={20} icon="CROSS" onClick={close} />
</Title>
<Description>Description how to use coins selection</Description>
<UtxoSelectionBase
utxos={account.utxo!}
selectedUtxos={selectedUtxos!}
symbol={account.symbol}
toggleUtxoSelection={toggleUtxoSelection}
/>
</Wrapper>
);
};

export default UtxoSelection;
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useSendFormContext } from '@wallet-hooks';
import { isEnabled as isFeatureEnabled } from '@suite-utils/features';
import { OpenGuideFromTooltip } from '@guide-views';
import Locktime from './components/Locktime';
import UtxoSelection from './components/UtxoSelection';

const Wrapper = styled.div`
display: flex;
Expand Down Expand Up @@ -53,10 +54,20 @@ const BitcoinOptions = () => {
const options = getDefaultValue('options', []);
const locktimeEnabled = options.includes('bitcoinLockTime');
const rbfEnabled = options.includes('bitcoinRBF');
const utxoSelectionEnabled = options.includes('utxoSelection');
const broadcastEnabled = options.includes('broadcast');

return (
<Wrapper>
{utxoSelectionEnabled && (
<UtxoSelection
close={() => {
resetDefaultValue('utxoSelection');
toggleOption('utxoSelection');
composeTransaction();
}}
/>
)}
{locktimeEnabled && (
<Locktime
close={() => {
Expand Down Expand Up @@ -118,6 +129,29 @@ const BitcoinOptions = () => {
</StyledButton>
</Tooltip>
)}
{!utxoSelectionEnabled && (
<Tooltip
openGuide={{
node: (
<OpenGuideFromTooltip id="/suite-basics/send/utxo-selection.md" />
),
}}
content={<span>Coin selection tooltip...</span>}
cursor="pointer"
>
<StyledButton
variant="tertiary"
icon="CREDIT_CARD"
onClick={() => {
// open additional form
toggleOption('utxoSelection');
composeTransaction();
}}
>
Coin selection
</StyledButton>
</Tooltip>
)}
<Tooltip content={<Translation id="BROADCAST_TOOLTIP" />} cursor="pointer">
<StyledButton
variant="tertiary"
Expand Down