vega-frontend-monorepo/libs/accounts/src/lib/transfer-form.tsx
2023-02-06 11:35:40 -08:00

297 lines
7.8 KiB
TypeScript

import {
t,
minSafe,
maxSafe,
required,
vegaPublicKey,
addDecimal,
formatNumber,
} from '@vegaprotocol/react-helpers';
import {
Button,
FormGroup,
Input,
InputError,
Option,
RichSelect,
Select,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import type { Transfer } from '@vegaprotocol/wallet';
import { normalizeTransfer } from '@vegaprotocol/wallet';
import BigNumber from 'bignumber.js';
import type { ReactNode } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
interface FormFields {
toAddress: string;
asset: string;
amount: string;
}
interface TransferFormProps {
pubKey: string | null;
pubKeys: string[] | null;
assets: Array<{
id: string;
symbol: string;
name: string;
decimals: number;
balance: string;
}>;
feeFactor: string | null;
submitTransfer: (transfer: Transfer) => void;
}
export const TransferForm = ({
pubKey,
pubKeys,
assets,
feeFactor,
submitTransfer,
}: TransferFormProps) => {
const {
control,
register,
watch,
handleSubmit,
setValue,
formState: { errors },
} = useForm<FormFields>();
const amount = watch('amount');
const assetId = watch('asset');
const asset = useMemo(() => {
return assets.find((a) => a.id === assetId);
}, [assets, assetId]);
const onSubmit = useCallback(
(fields: FormFields) => {
if (!asset) {
throw new Error('Submitted transfer with no asset selected');
}
const transfer = normalizeTransfer(fields.toAddress, fields.amount, {
id: asset.id,
decimals: asset.decimals,
});
submitTransfer(transfer);
},
[asset, submitTransfer]
);
const min = useMemo(() => {
// Min viable amount given asset decimals EG for WEI 0.000000000000000001
const minViableAmount = asset
? new BigNumber(addDecimal('1', asset.decimals))
: new BigNumber(0);
return minViableAmount;
}, [asset]);
const max = useMemo(() => {
const maxAmount = asset ? new BigNumber(asset.balance) : new BigNumber(0);
return maxAmount;
}, [asset]);
return (
<form
onSubmit={handleSubmit(onSubmit)}
className="text-sm"
data-testid="transfer-form"
>
<FormGroup label="Vega key" labelFor="to-address">
<AddressField
pubKeys={pubKeys}
onChange={() => setValue('toAddress', '')}
select={
<Select {...register('toAddress')} id="to-address" defaultValue="">
<option value="" disabled={true}>
{t('Please select')}
</option>
{pubKeys?.length &&
pubKeys
.filter((pk) => pk !== pubKey) // remove currently selected pubkey
.map((pk) => (
<option key={pk} value={pk}>
{pk}
</option>
))}
</Select>
}
input={
<Input
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={true} // focus input immediately after is shown
id="to-address"
type="text"
{...register('toAddress', {
validate: {
required,
vegaPublicKey,
sameKey: (value) => {
if (value === pubKey) {
return t('Vega key is the same as current key');
}
return true;
},
},
})}
/>
}
/>
{errors.toAddress?.message && (
<InputError forInput="to-address">
{errors.toAddress.message}
</InputError>
)}
</FormGroup>
<FormGroup label="Asset" labelFor="asset">
<Controller
control={control}
name="asset"
rules={{
validate: {
required,
},
}}
render={({ field }) => (
<RichSelect
data-testid="select-asset"
id={field.name}
name={field.name}
onValueChange={(value) => {
field.onChange(value);
}}
placeholder={t('Please select')}
value={field.value}
>
{assets.map((a) => (
<Option key={a.id} value={a.id}>
<div className="text-left" data-testid={`asset-${a.id}`}>
<div>{a.name}</div>
<div className="text-xs">
<span className="font-mono" data-testid="asset-balance">
{formatNumber(a.balance, a.decimals)}
</span>{' '}
<span>{a.symbol}</span>
</div>
</div>
</Option>
))}
</RichSelect>
)}
/>
{errors.asset?.message && (
<InputError forInput="asset">{errors.asset.message}</InputError>
)}
</FormGroup>
<FormGroup label="Amount" labelFor="amount">
<Input
id="amount"
autoComplete="off"
appendElement={
asset && <span className="text-xs">{asset.symbol}</span>
}
{...register('amount', {
validate: {
required,
minSafe: (value) => minSafe(new BigNumber(min))(value),
maxSafe: (v) => {
const value = new BigNumber(v);
if (value.isGreaterThan(max)) {
return t(
'You cannot transfer more than your available collateral'
);
}
return maxSafe(max)(v);
},
},
})}
/>
{errors.amount?.message && (
<InputError forInput="amount">{errors.amount.message}</InputError>
)}
</FormGroup>
<TransferFee amount={amount} feeFactor={feeFactor} />
<Button type="submit" variant="primary" fill={true}>
{t('Confirm transfer')}
</Button>
</form>
);
};
export const TransferFee = ({
amount,
feeFactor,
}: {
amount: string;
feeFactor: string | null;
}) => {
if (!feeFactor || !amount) return null;
// using toFixed without an argument will always return a
// number in normal notation without rounding, formatting functions
// arent working in a way which won't round the decimal places
const value = new BigNumber(amount).times(feeFactor).toFixed();
return (
<div className="mb-4 flex justify-between items-center gap-4 flex-wrap">
<div>
<Tooltip
description={t(
`The transfer fee is set by the network parameter transfer.fee.factor, currently set to ${feeFactor}`
)}
>
<div>{t('Transfer fee')}</div>
</Tooltip>
</div>
<div
data-testid="transfer-fee"
className="text-neutral-500 dark:text-neutral-300"
>
{value}
</div>
</div>
);
};
interface AddressInputProps {
pubKeys: string[] | null;
select: ReactNode;
input: ReactNode;
onChange: () => void;
}
export const AddressField = ({
pubKeys,
select,
input,
onChange,
}: AddressInputProps) => {
const [isInput, setIsInput] = useState(() => {
if (pubKeys && pubKeys.length <= 1) {
return true;
}
return false;
});
return (
<>
{isInput ? input : select}
{pubKeys && pubKeys.length > 1 && (
<button
type="button"
onClick={() => {
setIsInput((curr) => !curr);
onChange();
}}
className="ml-auto text-sm absolute top-0 right-0 underline"
>
{isInput ? t('Select from wallet') : t('Enter manually')}
</button>
)}
</>
);
};