feat(trading): key to key transfers (#2784)
This commit is contained in:
parent
3eb1359504
commit
8bcdaf4cda
@ -5,7 +5,7 @@ import { useWithdrawalDialog } from '@vegaprotocol/withdraws';
|
|||||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
import { AccountManager } from '@vegaprotocol/accounts';
|
import { AccountManager, useTransferDialog } from '@vegaprotocol/accounts';
|
||||||
import { useDepositDialog } from '@vegaprotocol/deposits';
|
import { useDepositDialog } from '@vegaprotocol/deposits';
|
||||||
|
|
||||||
export const AccountsContainer = () => {
|
export const AccountsContainer = () => {
|
||||||
@ -13,6 +13,7 @@ export const AccountsContainer = () => {
|
|||||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||||
const openWithdrawalDialog = useWithdrawalDialog((store) => store.open);
|
const openWithdrawalDialog = useWithdrawalDialog((store) => store.open);
|
||||||
const openDepositDialog = useDepositDialog((store) => store.open);
|
const openDepositDialog = useDepositDialog((store) => store.open);
|
||||||
|
const openTransferDialog = useTransferDialog((store) => store.open);
|
||||||
|
|
||||||
const onClickAsset = useCallback(
|
const onClickAsset = useCallback(
|
||||||
(assetId?: string) => {
|
(assetId?: string) => {
|
||||||
@ -41,7 +42,10 @@ export const AccountsContainer = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{!isReadOnly && (
|
{!isReadOnly && (
|
||||||
<div className="flex justify-end p-2 px-[11px]">
|
<div className="flex gap-2 justify-end p-2 px-[11px]">
|
||||||
|
<Button size="sm" onClick={() => openTransferDialog()}>
|
||||||
|
{t('Transfer')}
|
||||||
|
</Button>
|
||||||
<Button size="sm" onClick={() => openDepositDialog()}>
|
<Button size="sm" onClick={() => openDepositDialog()}>
|
||||||
{t('Deposit')}
|
{t('Deposit')}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -13,11 +13,13 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
Icon,
|
Icon,
|
||||||
Drawer,
|
Drawer,
|
||||||
|
DropdownMenuSeparator,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import type { PubKey } from '@vegaprotocol/wallet';
|
import type { PubKey } from '@vegaprotocol/wallet';
|
||||||
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||||
import { Networks, useEnvironment } from '@vegaprotocol/environment';
|
import { Networks, useEnvironment } from '@vegaprotocol/environment';
|
||||||
import { WalletIcon } from '../icons/wallet';
|
import { WalletIcon } from '../icons/wallet';
|
||||||
|
import { useTransferDialog } from '@vegaprotocol/accounts';
|
||||||
|
|
||||||
const MobileWalletButton = ({
|
const MobileWalletButton = ({
|
||||||
isConnected,
|
isConnected,
|
||||||
@ -30,6 +32,7 @@ const MobileWalletButton = ({
|
|||||||
const openVegaWalletDialog = useVegaWalletDialogStore(
|
const openVegaWalletDialog = useVegaWalletDialogStore(
|
||||||
(store) => store.openVegaWalletDialog
|
(store) => store.openVegaWalletDialog
|
||||||
);
|
);
|
||||||
|
const openTransferDialog = useTransferDialog((store) => store.open);
|
||||||
const { VEGA_ENV } = useEnvironment();
|
const { VEGA_ENV } = useEnvironment();
|
||||||
const isYellow = VEGA_ENV === Networks.TESTNET;
|
const isYellow = VEGA_ENV === Networks.TESTNET;
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
@ -115,7 +118,16 @@ const MobileWalletButton = ({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="m-4">
|
<div className="flex flex-col gap-2 m-4">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setDrawerOpen(false);
|
||||||
|
openTransferDialog(true);
|
||||||
|
}}
|
||||||
|
fill
|
||||||
|
>
|
||||||
|
{t('Transfer')}
|
||||||
|
</Button>
|
||||||
<Button onClick={mobileDisconnect} fill>
|
<Button onClick={mobileDisconnect} fill>
|
||||||
{t('Disconnect')}
|
{t('Disconnect')}
|
||||||
</Button>
|
</Button>
|
||||||
@ -131,6 +143,7 @@ export const VegaWalletConnectButton = () => {
|
|||||||
const openVegaWalletDialog = useVegaWalletDialogStore(
|
const openVegaWalletDialog = useVegaWalletDialogStore(
|
||||||
(store) => store.openVegaWalletDialog
|
(store) => store.openVegaWalletDialog
|
||||||
);
|
);
|
||||||
|
const openTransferDialog = useTransferDialog((store) => store.open);
|
||||||
const { pubKey, pubKeys, selectPubKey, disconnect } = useVegaWallet();
|
const { pubKey, pubKeys, selectPubKey, disconnect } = useVegaWallet();
|
||||||
const isConnected = pubKey !== null;
|
const isConnected = pubKey !== null;
|
||||||
|
|
||||||
@ -171,6 +184,10 @@ export const VegaWalletConnectButton = () => {
|
|||||||
<KeypairItem key={pk.publicKey} pk={pk} />
|
<KeypairItem key={pk.publicKey} pk={pk} />
|
||||||
))}
|
))}
|
||||||
</DropdownMenuRadioGroup>
|
</DropdownMenuRadioGroup>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onClick={() => openTransferDialog(true)}>
|
||||||
|
{t('Transfer')}
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem data-testid="disconnect" onClick={disconnect}>
|
<DropdownMenuItem data-testid="disconnect" onClick={disconnect}>
|
||||||
{t('Disconnect')}
|
{t('Disconnect')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import first from 'lodash/first';
|
||||||
import compact from 'lodash/compact';
|
import compact from 'lodash/compact';
|
||||||
import type {
|
import type {
|
||||||
BatchMarketInstructionSubmissionBody,
|
BatchMarketInstructionSubmissionBody,
|
||||||
@ -10,13 +11,12 @@ import type {
|
|||||||
VegaStoredTxState,
|
VegaStoredTxState,
|
||||||
WithdrawalBusEventFieldsFragment,
|
WithdrawalBusEventFieldsFragment,
|
||||||
} from '@vegaprotocol/wallet';
|
} from '@vegaprotocol/wallet';
|
||||||
import { isBatchMarketInstructionsTransaction } from '@vegaprotocol/wallet';
|
|
||||||
import {
|
import {
|
||||||
|
isTransferTransaction,
|
||||||
|
isBatchMarketInstructionsTransaction,
|
||||||
ClientErrors,
|
ClientErrors,
|
||||||
useReconnectVegaWallet,
|
useReconnectVegaWallet,
|
||||||
WalletError,
|
WalletError,
|
||||||
} from '@vegaprotocol/wallet';
|
|
||||||
import {
|
|
||||||
isOrderAmendmentTransaction,
|
isOrderAmendmentTransaction,
|
||||||
isOrderCancellationTransaction,
|
isOrderCancellationTransaction,
|
||||||
isOrderSubmissionTransaction,
|
isOrderSubmissionTransaction,
|
||||||
@ -32,13 +32,13 @@ import {
|
|||||||
Size,
|
Size,
|
||||||
t,
|
t,
|
||||||
toBigNum,
|
toBigNum,
|
||||||
|
truncateByChars,
|
||||||
} from '@vegaprotocol/react-helpers';
|
} from '@vegaprotocol/react-helpers';
|
||||||
import { useAssetsDataProvider } from '@vegaprotocol/assets';
|
import { useAssetsDataProvider } from '@vegaprotocol/assets';
|
||||||
import { useEthWithdrawApprovalsStore } from '@vegaprotocol/web3';
|
import { useEthWithdrawApprovalsStore } from '@vegaprotocol/web3';
|
||||||
import { DApp, EXPLORER_TX, useLinks } from '@vegaprotocol/environment';
|
import { DApp, EXPLORER_TX, useLinks } from '@vegaprotocol/environment';
|
||||||
import { getRejectionReason, useOrderByIdQuery } from '@vegaprotocol/orders';
|
import { getRejectionReason, useOrderByIdQuery } from '@vegaprotocol/orders';
|
||||||
import { useMarketList } from '@vegaprotocol/market-list';
|
import { useMarketList } from '@vegaprotocol/market-list';
|
||||||
import first from 'lodash/first';
|
|
||||||
import type { Side } from '@vegaprotocol/types';
|
import type { Side } from '@vegaprotocol/types';
|
||||||
import { OrderStatusMapping } from '@vegaprotocol/types';
|
import { OrderStatusMapping } from '@vegaprotocol/types';
|
||||||
|
|
||||||
@ -87,12 +87,14 @@ const isTransactionTypeSupported = (tx: VegaStoredTxState) => {
|
|||||||
const cancelOrder = isOrderCancellationTransaction(tx.body);
|
const cancelOrder = isOrderCancellationTransaction(tx.body);
|
||||||
const editOrder = isOrderAmendmentTransaction(tx.body);
|
const editOrder = isOrderAmendmentTransaction(tx.body);
|
||||||
const batchMarketInstructions = isBatchMarketInstructionsTransaction(tx.body);
|
const batchMarketInstructions = isBatchMarketInstructionsTransaction(tx.body);
|
||||||
|
const transfer = isTransferTransaction(tx.body);
|
||||||
return (
|
return (
|
||||||
withdraw ||
|
withdraw ||
|
||||||
submitOrder ||
|
submitOrder ||
|
||||||
cancelOrder ||
|
cancelOrder ||
|
||||||
editOrder ||
|
editOrder ||
|
||||||
batchMarketInstructions
|
batchMarketInstructions ||
|
||||||
|
transfer
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -388,6 +390,26 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
|
|||||||
return <Details>{t('Batch market instruction')}</Details>;
|
return <Details>{t('Batch market instruction')}</Details>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isTransferTransaction(tx.body)) {
|
||||||
|
const { amount, to, asset } = tx.body.transfer;
|
||||||
|
const transferAsset = assets?.find((a) => a.id === asset);
|
||||||
|
// only render if we have an asset to avoid unformatted amounts showing
|
||||||
|
if (transferAsset) {
|
||||||
|
const value = addDecimalsFormatNumber(amount, transferAsset.decimals);
|
||||||
|
return (
|
||||||
|
<Details>
|
||||||
|
<h4 className="font-bold">{t('Transfer')}</h4>
|
||||||
|
<p>
|
||||||
|
{t('To')} {truncateByChars(to)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{value} {transferAsset.symbol}
|
||||||
|
</p>
|
||||||
|
</Details>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -433,6 +455,7 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
const explorerLink = useLinks(DApp.Explorer);
|
const explorerLink = useLinks(DApp.Explorer);
|
||||||
|
|
||||||
if (isWithdrawTransaction(tx.body)) {
|
if (isWithdrawTransaction(tx.body)) {
|
||||||
const completeWithdrawalButton = tx.withdrawal && (
|
const completeWithdrawalButton = tx.withdrawal && (
|
||||||
<div className="mt-[10px]">
|
<div className="mt-[10px]">
|
||||||
@ -490,6 +513,16 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isTransferTransaction(tx.body)) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold">{t('Transfer complete')}</h3>
|
||||||
|
<p>{t('Your transaction has been confirmed ')}</p>
|
||||||
|
<VegaTransactionDetails tx={tx} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-bold">{t('Confirmed')}</h3>
|
<h3 className="font-bold">{t('Confirmed')}</h3>
|
||||||
|
@ -8,6 +8,7 @@ import { CreateWithdrawalDialog } from '@vegaprotocol/withdraws';
|
|||||||
import { DepositDialog } from '@vegaprotocol/deposits';
|
import { DepositDialog } from '@vegaprotocol/deposits';
|
||||||
import { Web3ConnectUncontrolledDialog } from '@vegaprotocol/web3';
|
import { Web3ConnectUncontrolledDialog } from '@vegaprotocol/web3';
|
||||||
import { WelcomeDialog } from '../components/welcome-dialog';
|
import { WelcomeDialog } from '../components/welcome-dialog';
|
||||||
|
import { TransferDialog } from '@vegaprotocol/accounts';
|
||||||
|
|
||||||
const DialogsContainer = () => {
|
const DialogsContainer = () => {
|
||||||
const { isOpen, id, trigger, setOpen } = useAssetDetailsDialogStore();
|
const { isOpen, id, trigger, setOpen } = useAssetDetailsDialogStore();
|
||||||
@ -25,6 +26,7 @@ const DialogsContainer = () => {
|
|||||||
<DepositDialog />
|
<DepositDialog />
|
||||||
<Web3ConnectUncontrolledDialog />
|
<Web3ConnectUncontrolledDialog />
|
||||||
<CreateWithdrawalDialog />
|
<CreateWithdrawalDialog />
|
||||||
|
<TransferDialog />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -7,3 +7,4 @@ export * from './breakdown-table';
|
|||||||
export * from './use-account-balance';
|
export * from './use-account-balance';
|
||||||
export * from './get-settlement-account';
|
export * from './get-settlement-account';
|
||||||
export * from './use-market-account-balance';
|
export * from './use-market-account-balance';
|
||||||
|
export * from './transfer-dialog';
|
||||||
|
68
libs/accounts/src/lib/transfer-container.tsx
Normal file
68
libs/accounts/src/lib/transfer-container.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import * as Schema from '@vegaprotocol/types';
|
||||||
|
import {
|
||||||
|
addDecimal,
|
||||||
|
NetworkParams,
|
||||||
|
t,
|
||||||
|
truncateByChars,
|
||||||
|
useDataProvider,
|
||||||
|
useNetworkParam,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
|
import type { Transfer } from '@vegaprotocol/wallet';
|
||||||
|
import { useVegaTransactionStore, useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { accountsDataProvider } from './accounts-data-provider';
|
||||||
|
import { TransferForm } from './transfer-form';
|
||||||
|
import { useTransferDialog } from './transfer-dialog';
|
||||||
|
import { Lozenge } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
|
export const TransferContainer = () => {
|
||||||
|
const { pubKey, pubKeys } = useVegaWallet();
|
||||||
|
const open = useTransferDialog((store) => store.open);
|
||||||
|
const { param } = useNetworkParam(NetworkParams.transfer_fee_factor);
|
||||||
|
const { data } = useDataProvider({
|
||||||
|
dataProvider: accountsDataProvider,
|
||||||
|
variables: { partyId: pubKey },
|
||||||
|
skip: !pubKey,
|
||||||
|
});
|
||||||
|
const create = useVegaTransactionStore((store) => store.create);
|
||||||
|
|
||||||
|
const transfer = useCallback(
|
||||||
|
(transfer: Transfer) => {
|
||||||
|
create({ transfer });
|
||||||
|
open(false);
|
||||||
|
},
|
||||||
|
[create, open]
|
||||||
|
);
|
||||||
|
|
||||||
|
const assets = useMemo(() => {
|
||||||
|
if (!data) return [];
|
||||||
|
return data
|
||||||
|
.filter(
|
||||||
|
(account) => account.type === Schema.AccountType.ACCOUNT_TYPE_GENERAL
|
||||||
|
)
|
||||||
|
.map((account) => ({
|
||||||
|
id: account.asset.id,
|
||||||
|
symbol: account.asset.symbol,
|
||||||
|
name: account.asset.name,
|
||||||
|
decimals: account.asset.decimals,
|
||||||
|
balance: addDecimal(account.balance, account.asset.decimals),
|
||||||
|
}));
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className="text-sm mb-4">
|
||||||
|
{t('Transfer funds to another Vega key from')}{' '}
|
||||||
|
<Lozenge className="font-mono">{truncateByChars(pubKey || '')}</Lozenge>{' '}
|
||||||
|
{t('If you are at all unsure, stop and seek advice.')}
|
||||||
|
</p>
|
||||||
|
<TransferForm
|
||||||
|
pubKey={pubKey}
|
||||||
|
pubKeys={pubKeys ? pubKeys?.map((pk) => pk.publicKey) : null}
|
||||||
|
assets={assets}
|
||||||
|
feeFactor={param}
|
||||||
|
submitTransfer={transfer}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
28
libs/accounts/src/lib/transfer-dialog.tsx
Normal file
28
libs/accounts/src/lib/transfer-dialog.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import { Dialog } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { TransferContainer } from './transfer-container';
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Actions {
|
||||||
|
open: (open?: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTransferDialog = create<State & Actions>((set) => ({
|
||||||
|
isOpen: false,
|
||||||
|
open: (open = true) => {
|
||||||
|
set(() => ({ isOpen: open }));
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const TransferDialog = () => {
|
||||||
|
const { isOpen, open } = useTransferDialog();
|
||||||
|
return (
|
||||||
|
<Dialog title={t('Transfer')} open={isOpen} onChange={open} size="small">
|
||||||
|
<TransferContainer />
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
177
libs/accounts/src/lib/transfer-form.spec.tsx
Normal file
177
libs/accounts/src/lib/transfer-form.spec.tsx
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
import { AddressField, TransferFee, TransferForm } from './transfer-form';
|
||||||
|
import { AccountType } from '@vegaprotocol/types';
|
||||||
|
import { formatNumber, removeDecimal } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
|
describe('TransferForm', () => {
|
||||||
|
const submit = () => fireEvent.submit(screen.getByTestId('transfer-form'));
|
||||||
|
const amount = '100';
|
||||||
|
const pubKey =
|
||||||
|
'70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680';
|
||||||
|
const asset = {
|
||||||
|
id: 'asset-0',
|
||||||
|
symbol: 'ASSET 0',
|
||||||
|
name: 'Asset 0',
|
||||||
|
decimals: 2,
|
||||||
|
balance: '1000',
|
||||||
|
};
|
||||||
|
const props = {
|
||||||
|
pubKey,
|
||||||
|
pubKeys: [
|
||||||
|
pubKey,
|
||||||
|
'a4b6e3de5d7ef4e31ae1b090be49d1a2ef7bcefff60cccf7658a0d4922651cce',
|
||||||
|
],
|
||||||
|
assets: [asset],
|
||||||
|
feeFactor: '0.001',
|
||||||
|
submitTransfer: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
it('validates fields and submits', async () => {
|
||||||
|
render(<TransferForm {...props} />);
|
||||||
|
|
||||||
|
// check current pubkey not shown
|
||||||
|
const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key');
|
||||||
|
expect(keySelect.children).toHaveLength(2);
|
||||||
|
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([
|
||||||
|
'',
|
||||||
|
props.pubKeys[1],
|
||||||
|
]);
|
||||||
|
|
||||||
|
submit();
|
||||||
|
expect(await screen.findAllByText('Required')).toHaveLength(3);
|
||||||
|
|
||||||
|
// Select a pubkey
|
||||||
|
fireEvent.change(screen.getByLabelText('Vega key'), {
|
||||||
|
target: { value: props.pubKeys[1] },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Select asset
|
||||||
|
fireEvent.change(
|
||||||
|
// Bypass RichSelect and target hidden native select
|
||||||
|
// eslint-disable-next-line
|
||||||
|
document.querySelector('select[name="asset"]')!,
|
||||||
|
{ target: { value: asset.id } }
|
||||||
|
);
|
||||||
|
|
||||||
|
// assert rich select as updated
|
||||||
|
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
|
||||||
|
asset.name
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('asset-balance')).toHaveTextContent(
|
||||||
|
formatNumber(asset.balance, asset.decimals)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test amount validation
|
||||||
|
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||||
|
target: { value: '0.00000001' },
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
await screen.findByText('Value is below minimum')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||||
|
target: { value: '9999999' },
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
await screen.findByText(/cannot transfer more/i)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
// set valid amount
|
||||||
|
fireEvent.change(screen.getByLabelText('Amount'), {
|
||||||
|
target: { value: amount },
|
||||||
|
});
|
||||||
|
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(
|
||||||
|
new BigNumber(props.feeFactor).times(amount).toFixed()
|
||||||
|
);
|
||||||
|
|
||||||
|
submit();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(props.submitTransfer).toHaveBeenCalledTimes(1);
|
||||||
|
expect(props.submitTransfer).toHaveBeenCalledWith({
|
||||||
|
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||||
|
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||||
|
to: props.pubKeys[1],
|
||||||
|
asset: asset.id,
|
||||||
|
amount: removeDecimal(amount, asset.decimals),
|
||||||
|
oneOff: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates a manually entered address', async () => {
|
||||||
|
render(<TransferForm {...props} />);
|
||||||
|
submit();
|
||||||
|
expect(await screen.findAllByText('Required')).toHaveLength(3);
|
||||||
|
const toggle = screen.getByText('Enter manually');
|
||||||
|
fireEvent.click(toggle);
|
||||||
|
// has switched to input
|
||||||
|
expect(toggle).toHaveTextContent('Select from wallet');
|
||||||
|
expect(screen.getByLabelText('Vega key')).toHaveAttribute('type', 'text');
|
||||||
|
fireEvent.change(screen.getByLabelText('Vega key'), {
|
||||||
|
target: { value: 'invalid-address' },
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
const errors = screen.getAllByTestId('input-error-text');
|
||||||
|
expect(errors[0]).toHaveTextContent('Invalid Vega key');
|
||||||
|
});
|
||||||
|
|
||||||
|
// same pubkey
|
||||||
|
fireEvent.change(screen.getByLabelText('Vega key'), {
|
||||||
|
target: { value: pubKey },
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const errors = screen.getAllByTestId('input-error-text');
|
||||||
|
expect(errors[0]).toHaveTextContent('Vega key is the same');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('AddressField', () => {
|
||||||
|
const props = {
|
||||||
|
pubKeys: ['pubkey-1', 'pubkey-2'],
|
||||||
|
select: <div>select</div>,
|
||||||
|
input: <div>input</div>,
|
||||||
|
onChange: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
it('toggles content and calls onChange', async () => {
|
||||||
|
const mockOnChange = jest.fn();
|
||||||
|
render(<AddressField {...props} onChange={mockOnChange} />);
|
||||||
|
|
||||||
|
// select should be shown as multiple pubkeys provided
|
||||||
|
expect(screen.getByText('select')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('input')).not.toBeInTheDocument();
|
||||||
|
fireEvent.click(screen.getByText('Enter manually'));
|
||||||
|
expect(screen.queryByText('select')).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByText('input')).toBeInTheDocument();
|
||||||
|
expect(mockOnChange).toHaveBeenCalledTimes(1);
|
||||||
|
fireEvent.click(screen.getByText('Select from wallet'));
|
||||||
|
expect(screen.getByText('select')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('input')).not.toBeInTheDocument();
|
||||||
|
expect(mockOnChange).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Does not provide select option if there is only a single key', () => {
|
||||||
|
render(<AddressField {...props} pubKeys={['single-pubKey']} />);
|
||||||
|
expect(screen.getByText('input')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Select from wallet')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('TransferFee', () => {
|
||||||
|
const props = {
|
||||||
|
amount: '200',
|
||||||
|
feeFactor: '0.001',
|
||||||
|
};
|
||||||
|
it('calculates and renders the transfer fee', () => {
|
||||||
|
render(<TransferFee {...props} />);
|
||||||
|
|
||||||
|
const expected = new BigNumber(props.amount)
|
||||||
|
.times(props.feeFactor)
|
||||||
|
.toFixed();
|
||||||
|
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expected);
|
||||||
|
});
|
||||||
|
});
|
296
libs/accounts/src/lib/transfer-form.tsx
Normal file
296
libs/accounts/src/lib/transfer-form.tsx
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -2,6 +2,7 @@ import { renderHook, waitFor } from '@testing-library/react';
|
|||||||
import type { MockedResponse } from '@apollo/client/testing';
|
import type { MockedResponse } from '@apollo/client/testing';
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
import type { NetworkParamsKey } from './use-network-params';
|
import type { NetworkParamsKey } from './use-network-params';
|
||||||
|
import { toRealKey } from './use-network-params';
|
||||||
import {
|
import {
|
||||||
NetworkParams,
|
NetworkParams,
|
||||||
useNetworkParam,
|
useNetworkParam,
|
||||||
@ -18,7 +19,7 @@ describe('useNetworkParam', () => {
|
|||||||
request: {
|
request: {
|
||||||
query: NetworkParamDocument,
|
query: NetworkParamDocument,
|
||||||
variables: {
|
variables: {
|
||||||
key: arg,
|
key: toRealKey(arg),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
result: {
|
result: {
|
||||||
|
@ -104,10 +104,11 @@ export const NetworkParams = {
|
|||||||
market_liquidity_stakeToCcyVolume: 'market_liquidity_stakeToCcyVolume',
|
market_liquidity_stakeToCcyVolume: 'market_liquidity_stakeToCcyVolume',
|
||||||
market_liquidity_targetstake_triggering_ratio:
|
market_liquidity_targetstake_triggering_ratio:
|
||||||
'market_liquidity_targetstake_triggering_ratio',
|
'market_liquidity_targetstake_triggering_ratio',
|
||||||
|
transfer_fee_factor: 'transfer_fee_factor',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type Params = typeof NetworkParams;
|
type Params = typeof NetworkParams;
|
||||||
export type NetworkParamsKey = Params[keyof Params];
|
export type NetworkParamsKey = keyof Params;
|
||||||
type Result = {
|
type Result = {
|
||||||
[key in keyof Params]: string;
|
[key in keyof Params]: string;
|
||||||
};
|
};
|
||||||
@ -120,7 +121,7 @@ export const useNetworkParams = <T extends NetworkParamsKey[]>(params?: T) => {
|
|||||||
return compact(data.networkParametersConnection.edges)
|
return compact(data.networkParametersConnection.edges)
|
||||||
.map((p) => ({
|
.map((p) => ({
|
||||||
...p.node,
|
...p.node,
|
||||||
key: p.node.key.split('.').join('_'),
|
key: toInternalKey(p.node.key),
|
||||||
}))
|
}))
|
||||||
.filter((p) => {
|
.filter((p) => {
|
||||||
if (params === undefined || params.length === 0) return true;
|
if (params === undefined || params.length === 0) return true;
|
||||||
@ -143,7 +144,7 @@ export const useNetworkParams = <T extends NetworkParamsKey[]>(params?: T) => {
|
|||||||
export const useNetworkParam = (param: NetworkParamsKey) => {
|
export const useNetworkParam = (param: NetworkParamsKey) => {
|
||||||
const { data, loading, error } = useNetworkParamQuery({
|
const { data, loading, error } = useNetworkParamQuery({
|
||||||
variables: {
|
variables: {
|
||||||
key: param,
|
key: toRealKey(param),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -153,3 +154,11 @@ export const useNetworkParam = (param: NetworkParamsKey) => {
|
|||||||
error,
|
error,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const toRealKey = (key: NetworkParamsKey) => {
|
||||||
|
return key.split('_').join('.');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toInternalKey = (key: string) => {
|
||||||
|
return key.split('.').join('_');
|
||||||
|
};
|
||||||
|
@ -151,7 +151,7 @@ export const DropdownMenuSeparator = forwardRef<
|
|||||||
{...separatorProps}
|
{...separatorProps}
|
||||||
ref={forwardedRef}
|
ref={forwardedRef}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'h-px my-1 mx-2 bg-neutral-700 dark:bg-black',
|
'h-px my-1 mx-2 bg-neutral-400 dark:bg-neutral-300',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -15,7 +15,7 @@ export interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
|
|||||||
|
|
||||||
export const Select = forwardRef<HTMLSelectElement, SelectProps>(
|
export const Select = forwardRef<HTMLSelectElement, SelectProps>(
|
||||||
({ className, hasError, ...props }, ref) => (
|
({ className, hasError, ...props }, ref) => (
|
||||||
<div className="flex items-center relative">
|
<div className="relative">
|
||||||
<select
|
<select
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
@ -25,7 +25,10 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(
|
|||||||
'appearance-none rounded-md'
|
'appearance-none rounded-md'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Icon name="chevron-down" className="absolute right-4 z-10" />
|
<Icon
|
||||||
|
name="chevron-down"
|
||||||
|
className="absolute top-3 right-4 z-10 pointer-events-none"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -18,6 +18,7 @@ export const getIntentBorder = (intent = Intent.None) => {
|
|||||||
|
|
||||||
export const getIntentBackground = (intent?: Intent) => {
|
export const getIntentBackground = (intent?: Intent) => {
|
||||||
return {
|
return {
|
||||||
|
'bg-neutral-200 dark:bg-neutral-800': intent === undefined,
|
||||||
'bg-black dark:bg-white': intent === Intent.None,
|
'bg-black dark:bg-white': intent === Intent.None,
|
||||||
'bg-vega-pink dark:bg-vega-yellow': intent === Intent.Primary,
|
'bg-vega-pink dark:bg-vega-yellow': intent === Intent.Primary,
|
||||||
'bg-danger': intent === Intent.Danger,
|
'bg-danger': intent === Intent.Danger,
|
||||||
|
@ -292,6 +292,40 @@ export interface BatchMarketInstructionSubmissionBody {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TransferBase {
|
||||||
|
fromAccountType: Schema.AccountType;
|
||||||
|
to: string;
|
||||||
|
toAccountType: Schema.AccountType;
|
||||||
|
asset: string;
|
||||||
|
amount: string;
|
||||||
|
reference?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OneOffTransfer extends TransferBase {
|
||||||
|
oneOff: {
|
||||||
|
deliverOn?: number; // omit for immediate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecurringTransfer extends TransferBase {
|
||||||
|
recurring: {
|
||||||
|
factor: string;
|
||||||
|
startEpoch: number;
|
||||||
|
endEpoch?: number;
|
||||||
|
dispatchStrategy?: {
|
||||||
|
assetForMetric: string;
|
||||||
|
metric: Schema.DispatchMetric;
|
||||||
|
markets?: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Transfer = OneOffTransfer | RecurringTransfer;
|
||||||
|
|
||||||
|
export interface TransferBody {
|
||||||
|
transfer: Transfer;
|
||||||
|
}
|
||||||
|
|
||||||
export type Transaction =
|
export type Transaction =
|
||||||
| OrderSubmissionBody
|
| OrderSubmissionBody
|
||||||
| OrderCancellationBody
|
| OrderCancellationBody
|
||||||
@ -301,7 +335,8 @@ export type Transaction =
|
|||||||
| UndelegateSubmissionBody
|
| UndelegateSubmissionBody
|
||||||
| OrderAmendmentBody
|
| OrderAmendmentBody
|
||||||
| ProposalSubmissionBody
|
| ProposalSubmissionBody
|
||||||
| BatchMarketInstructionSubmissionBody;
|
| BatchMarketInstructionSubmissionBody
|
||||||
|
| TransferBody;
|
||||||
|
|
||||||
export const isWithdrawTransaction = (
|
export const isWithdrawTransaction = (
|
||||||
transaction: Transaction
|
transaction: Transaction
|
||||||
@ -324,6 +359,10 @@ export const isBatchMarketInstructionsTransaction = (
|
|||||||
): transaction is BatchMarketInstructionSubmissionBody =>
|
): transaction is BatchMarketInstructionSubmissionBody =>
|
||||||
'batchMarketInstructions' in transaction;
|
'batchMarketInstructions' in transaction;
|
||||||
|
|
||||||
|
export const isTransferTransaction = (
|
||||||
|
transaction: Transaction
|
||||||
|
): transaction is TransferBody => 'transfer' in transaction;
|
||||||
|
|
||||||
export interface TransactionResponse {
|
export interface TransactionResponse {
|
||||||
transactionHash: string;
|
transactionHash: string;
|
||||||
signature: string; // still to be added by core
|
signature: string; // still to be added by core
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
isOrderCancellationTransaction,
|
isOrderCancellationTransaction,
|
||||||
isOrderAmendmentTransaction,
|
isOrderAmendmentTransaction,
|
||||||
isBatchMarketInstructionsTransaction,
|
isBatchMarketInstructionsTransaction,
|
||||||
|
isTransferTransaction,
|
||||||
} from './connectors';
|
} from './connectors';
|
||||||
import { determineId } from './utils';
|
import { determineId } from './utils';
|
||||||
|
|
||||||
@ -184,9 +185,14 @@ export const useVegaTransactionStore = create<VegaTransactionStore>(
|
|||||||
);
|
);
|
||||||
if (transaction) {
|
if (transaction) {
|
||||||
transaction.transactionResult = transactionResult;
|
transaction.transactionResult = transactionResult;
|
||||||
if (
|
|
||||||
|
const isConfirmedOrderCancellation =
|
||||||
isOrderCancellationTransaction(transaction.body) &&
|
isOrderCancellationTransaction(transaction.body) &&
|
||||||
!transaction.body.orderCancellation.orderId &&
|
!transaction.body.orderCancellation.orderId;
|
||||||
|
const isConfirmedTransfer = isTransferTransaction(transaction.body);
|
||||||
|
|
||||||
|
if (
|
||||||
|
(isConfirmedOrderCancellation || isConfirmedTransfer) &&
|
||||||
!transactionResult.error &&
|
!transactionResult.error &&
|
||||||
transactionResult.status
|
transactionResult.status
|
||||||
) {
|
) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/react-helpers';
|
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/react-helpers';
|
||||||
import type { Market, Order } from '@vegaprotocol/types';
|
import type { Market, Order } from '@vegaprotocol/types';
|
||||||
import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
|
import { OrderTimeInForce, OrderType, AccountType } from '@vegaprotocol/types';
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
import { ethers } from 'ethers';
|
import { ethers } from 'ethers';
|
||||||
import { sha3_256 } from 'js-sha3';
|
import { sha3_256 } from 'js-sha3';
|
||||||
@ -8,6 +8,7 @@ import type {
|
|||||||
OrderAmendmentBody,
|
OrderAmendmentBody,
|
||||||
OrderSubmissionBody,
|
OrderSubmissionBody,
|
||||||
Transaction,
|
Transaction,
|
||||||
|
Transfer,
|
||||||
} from './connectors';
|
} from './connectors';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -63,3 +64,23 @@ export const normalizeOrderAmendment = (
|
|||||||
? toNanoSeconds(order.expiresAt) // Wallet expects timestamp in nanoseconds
|
? toNanoSeconds(order.expiresAt) // Wallet expects timestamp in nanoseconds
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const normalizeTransfer = (
|
||||||
|
address: string,
|
||||||
|
amount: string,
|
||||||
|
asset: {
|
||||||
|
id: string;
|
||||||
|
decimals: number;
|
||||||
|
}
|
||||||
|
): Transfer => {
|
||||||
|
return {
|
||||||
|
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||||
|
to: address,
|
||||||
|
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||||
|
asset: asset.id,
|
||||||
|
amount: removeDecimal(amount, asset.decimals),
|
||||||
|
// oneOff or recurring required otherwise wallet will error
|
||||||
|
// default oneOff is immediate transfer
|
||||||
|
oneOff: {},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user