297 lines
7.8 KiB
TypeScript
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>
|
|
)}
|
|
</>
|
|
);
|
|
};
|