+
{
+ setDrawerOpen(false);
+ openTransferDialog(true);
+ }}
+ fill
+ >
+ {t('Transfer')}
+
{t('Disconnect')}
@@ -131,6 +143,7 @@ export const VegaWalletConnectButton = () => {
const openVegaWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
+ const openTransferDialog = useTransferDialog((store) => store.open);
const { pubKey, pubKeys, selectPubKey, disconnect } = useVegaWallet();
const isConnected = pubKey !== null;
@@ -171,6 +184,10 @@ export const VegaWalletConnectButton = () => {
))}
+
+
openTransferDialog(true)}>
+ {t('Transfer')}
+
{t('Disconnect')}
diff --git a/apps/trading/lib/hooks/use-vega-transaction-toasts.tsx b/apps/trading/lib/hooks/use-vega-transaction-toasts.tsx
index bd985164a..723b41ce1 100644
--- a/apps/trading/lib/hooks/use-vega-transaction-toasts.tsx
+++ b/apps/trading/lib/hooks/use-vega-transaction-toasts.tsx
@@ -1,5 +1,6 @@
import type { ReactNode } from 'react';
import { useCallback, useMemo } from 'react';
+import first from 'lodash/first';
import compact from 'lodash/compact';
import type {
BatchMarketInstructionSubmissionBody,
@@ -10,13 +11,12 @@ import type {
VegaStoredTxState,
WithdrawalBusEventFieldsFragment,
} from '@vegaprotocol/wallet';
-import { isBatchMarketInstructionsTransaction } from '@vegaprotocol/wallet';
import {
+ isTransferTransaction,
+ isBatchMarketInstructionsTransaction,
ClientErrors,
useReconnectVegaWallet,
WalletError,
-} from '@vegaprotocol/wallet';
-import {
isOrderAmendmentTransaction,
isOrderCancellationTransaction,
isOrderSubmissionTransaction,
@@ -32,13 +32,13 @@ import {
Size,
t,
toBigNum,
+ truncateByChars,
} from '@vegaprotocol/react-helpers';
import { useAssetsDataProvider } from '@vegaprotocol/assets';
import { useEthWithdrawApprovalsStore } from '@vegaprotocol/web3';
import { DApp, EXPLORER_TX, useLinks } from '@vegaprotocol/environment';
import { getRejectionReason, useOrderByIdQuery } from '@vegaprotocol/orders';
import { useMarketList } from '@vegaprotocol/market-list';
-import first from 'lodash/first';
import type { Side } from '@vegaprotocol/types';
import { OrderStatusMapping } from '@vegaprotocol/types';
@@ -87,12 +87,14 @@ const isTransactionTypeSupported = (tx: VegaStoredTxState) => {
const cancelOrder = isOrderCancellationTransaction(tx.body);
const editOrder = isOrderAmendmentTransaction(tx.body);
const batchMarketInstructions = isBatchMarketInstructionsTransaction(tx.body);
+ const transfer = isTransferTransaction(tx.body);
return (
withdraw ||
submitOrder ||
cancelOrder ||
editOrder ||
- batchMarketInstructions
+ batchMarketInstructions ||
+ transfer
);
};
@@ -388,6 +390,26 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
return
{t('Batch market instruction')} ;
}
+ 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 (
+
+ {t('Transfer')}
+
+ {t('To')} {truncateByChars(to)}
+
+
+ {value} {transferAsset.symbol}
+
+
+ );
+ }
+ }
+
return null;
};
@@ -433,6 +455,7 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
})
);
const explorerLink = useLinks(DApp.Explorer);
+
if (isWithdrawTransaction(tx.body)) {
const completeWithdrawalButton = tx.withdrawal && (
@@ -490,6 +513,16 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
);
}
+ if (isTransferTransaction(tx.body)) {
+ return (
+
+
{t('Transfer complete')}
+
{t('Your transaction has been confirmed ')}
+
+
+ );
+ }
+
return (
{t('Confirmed')}
diff --git a/apps/trading/pages/dialogs-container.tsx b/apps/trading/pages/dialogs-container.tsx
index c03576ac4..6f0bbeb77 100644
--- a/apps/trading/pages/dialogs-container.tsx
+++ b/apps/trading/pages/dialogs-container.tsx
@@ -8,6 +8,7 @@ import { CreateWithdrawalDialog } from '@vegaprotocol/withdraws';
import { DepositDialog } from '@vegaprotocol/deposits';
import { Web3ConnectUncontrolledDialog } from '@vegaprotocol/web3';
import { WelcomeDialog } from '../components/welcome-dialog';
+import { TransferDialog } from '@vegaprotocol/accounts';
const DialogsContainer = () => {
const { isOpen, id, trigger, setOpen } = useAssetDetailsDialogStore();
@@ -25,6 +26,7 @@ const DialogsContainer = () => {
+
>
);
};
diff --git a/libs/accounts/src/lib/index.ts b/libs/accounts/src/lib/index.ts
index d3b35a103..ce1402607 100644
--- a/libs/accounts/src/lib/index.ts
+++ b/libs/accounts/src/lib/index.ts
@@ -7,3 +7,4 @@ export * from './breakdown-table';
export * from './use-account-balance';
export * from './get-settlement-account';
export * from './use-market-account-balance';
+export * from './transfer-dialog';
diff --git a/libs/accounts/src/lib/transfer-container.tsx b/libs/accounts/src/lib/transfer-container.tsx
new file mode 100644
index 000000000..a8fe25e9e
--- /dev/null
+++ b/libs/accounts/src/lib/transfer-container.tsx
@@ -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 (
+ <>
+
+ {t('Transfer funds to another Vega key from')}{' '}
+ {truncateByChars(pubKey || '')} {' '}
+ {t('If you are at all unsure, stop and seek advice.')}
+
+
pk.publicKey) : null}
+ assets={assets}
+ feeFactor={param}
+ submitTransfer={transfer}
+ />
+ >
+ );
+};
diff --git a/libs/accounts/src/lib/transfer-dialog.tsx b/libs/accounts/src/lib/transfer-dialog.tsx
new file mode 100644
index 000000000..998eaf253
--- /dev/null
+++ b/libs/accounts/src/lib/transfer-dialog.tsx
@@ -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((set) => ({
+ isOpen: false,
+ open: (open = true) => {
+ set(() => ({ isOpen: open }));
+ },
+}));
+
+export const TransferDialog = () => {
+ const { isOpen, open } = useTransferDialog();
+ return (
+
+
+
+ );
+};
diff --git a/libs/accounts/src/lib/transfer-form.spec.tsx b/libs/accounts/src/lib/transfer-form.spec.tsx
new file mode 100644
index 000000000..eb977217a
--- /dev/null
+++ b/libs/accounts/src/lib/transfer-form.spec.tsx
@@ -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( );
+
+ // 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( );
+ 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: select
,
+ input: input
,
+ onChange: jest.fn(),
+ };
+
+ it('toggles content and calls onChange', async () => {
+ const mockOnChange = jest.fn();
+ render( );
+
+ // 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( );
+ 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( );
+
+ const expected = new BigNumber(props.amount)
+ .times(props.feeFactor)
+ .toFixed();
+ expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expected);
+ });
+});
diff --git a/libs/accounts/src/lib/transfer-form.tsx b/libs/accounts/src/lib/transfer-form.tsx
new file mode 100644
index 000000000..53d1bf020
--- /dev/null
+++ b/libs/accounts/src/lib/transfer-form.tsx
@@ -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();
+
+ 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 (
+
+ );
+};
+
+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 (
+
+
+
+ {t('Transfer fee')}
+
+
+
+ {value}
+
+
+ );
+};
+
+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 && (
+ {
+ setIsInput((curr) => !curr);
+ onChange();
+ }}
+ className="ml-auto text-sm absolute top-0 right-0 underline"
+ >
+ {isInput ? t('Select from wallet') : t('Enter manually')}
+
+ )}
+ >
+ );
+};
diff --git a/libs/react-helpers/src/hooks/use-network-params.spec.tsx b/libs/react-helpers/src/hooks/use-network-params.spec.tsx
index cac3f004e..a70320ff3 100644
--- a/libs/react-helpers/src/hooks/use-network-params.spec.tsx
+++ b/libs/react-helpers/src/hooks/use-network-params.spec.tsx
@@ -2,6 +2,7 @@ import { renderHook, waitFor } from '@testing-library/react';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import type { NetworkParamsKey } from './use-network-params';
+import { toRealKey } from './use-network-params';
import {
NetworkParams,
useNetworkParam,
@@ -18,7 +19,7 @@ describe('useNetworkParam', () => {
request: {
query: NetworkParamDocument,
variables: {
- key: arg,
+ key: toRealKey(arg),
},
},
result: {
diff --git a/libs/react-helpers/src/hooks/use-network-params.ts b/libs/react-helpers/src/hooks/use-network-params.ts
index 8f8081ae0..bd598c85e 100644
--- a/libs/react-helpers/src/hooks/use-network-params.ts
+++ b/libs/react-helpers/src/hooks/use-network-params.ts
@@ -104,10 +104,11 @@ export const NetworkParams = {
market_liquidity_stakeToCcyVolume: 'market_liquidity_stakeToCcyVolume',
market_liquidity_targetstake_triggering_ratio:
'market_liquidity_targetstake_triggering_ratio',
+ transfer_fee_factor: 'transfer_fee_factor',
} as const;
type Params = typeof NetworkParams;
-export type NetworkParamsKey = Params[keyof Params];
+export type NetworkParamsKey = keyof Params;
type Result = {
[key in keyof Params]: string;
};
@@ -120,7 +121,7 @@ export const useNetworkParams = (params?: T) => {
return compact(data.networkParametersConnection.edges)
.map((p) => ({
...p.node,
- key: p.node.key.split('.').join('_'),
+ key: toInternalKey(p.node.key),
}))
.filter((p) => {
if (params === undefined || params.length === 0) return true;
@@ -143,7 +144,7 @@ export const useNetworkParams = (params?: T) => {
export const useNetworkParam = (param: NetworkParamsKey) => {
const { data, loading, error } = useNetworkParamQuery({
variables: {
- key: param,
+ key: toRealKey(param),
},
});
@@ -153,3 +154,11 @@ export const useNetworkParam = (param: NetworkParamsKey) => {
error,
};
};
+
+export const toRealKey = (key: NetworkParamsKey) => {
+ return key.split('_').join('.');
+};
+
+export const toInternalKey = (key: string) => {
+ return key.split('.').join('_');
+};
diff --git a/libs/ui-toolkit/src/components/dropdown-menu/dropdown-menu.tsx b/libs/ui-toolkit/src/components/dropdown-menu/dropdown-menu.tsx
index 8c000a171..3c9a4f533 100644
--- a/libs/ui-toolkit/src/components/dropdown-menu/dropdown-menu.tsx
+++ b/libs/ui-toolkit/src/components/dropdown-menu/dropdown-menu.tsx
@@ -151,7 +151,7 @@ export const DropdownMenuSeparator = forwardRef<
{...separatorProps}
ref={forwardedRef}
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
)}
/>
diff --git a/libs/ui-toolkit/src/components/select/select.tsx b/libs/ui-toolkit/src/components/select/select.tsx
index 13691624f..8fb5f0fae 100644
--- a/libs/ui-toolkit/src/components/select/select.tsx
+++ b/libs/ui-toolkit/src/components/select/select.tsx
@@ -15,7 +15,7 @@ export interface SelectProps extends SelectHTMLAttributes {
export const Select = forwardRef(
({ className, hasError, ...props }, ref) => (
-
+
(
'appearance-none rounded-md'
)}
/>
-
+
)
);
diff --git a/libs/ui-toolkit/src/utils/intent.tsx b/libs/ui-toolkit/src/utils/intent.tsx
index 3dbe353d8..dfcdcc788 100644
--- a/libs/ui-toolkit/src/utils/intent.tsx
+++ b/libs/ui-toolkit/src/utils/intent.tsx
@@ -18,6 +18,7 @@ export const getIntentBorder = (intent = Intent.None) => {
export const getIntentBackground = (intent?: Intent) => {
return {
+ 'bg-neutral-200 dark:bg-neutral-800': intent === undefined,
'bg-black dark:bg-white': intent === Intent.None,
'bg-vega-pink dark:bg-vega-yellow': intent === Intent.Primary,
'bg-danger': intent === Intent.Danger,
diff --git a/libs/wallet/src/connectors/vega-connector.ts b/libs/wallet/src/connectors/vega-connector.ts
index 7128b5a8f..4b394df8f 100644
--- a/libs/wallet/src/connectors/vega-connector.ts
+++ b/libs/wallet/src/connectors/vega-connector.ts
@@ -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 =
| OrderSubmissionBody
| OrderCancellationBody
@@ -301,7 +335,8 @@ export type Transaction =
| UndelegateSubmissionBody
| OrderAmendmentBody
| ProposalSubmissionBody
- | BatchMarketInstructionSubmissionBody;
+ | BatchMarketInstructionSubmissionBody
+ | TransferBody;
export const isWithdrawTransaction = (
transaction: Transaction
@@ -324,6 +359,10 @@ export const isBatchMarketInstructionsTransaction = (
): transaction is BatchMarketInstructionSubmissionBody =>
'batchMarketInstructions' in transaction;
+export const isTransferTransaction = (
+ transaction: Transaction
+): transaction is TransferBody => 'transfer' in transaction;
+
export interface TransactionResponse {
transactionHash: string;
signature: string; // still to be added by core
diff --git a/libs/wallet/src/use-vega-transaction-store.tsx b/libs/wallet/src/use-vega-transaction-store.tsx
index 0a1d1e545..644e833df 100644
--- a/libs/wallet/src/use-vega-transaction-store.tsx
+++ b/libs/wallet/src/use-vega-transaction-store.tsx
@@ -6,6 +6,7 @@ import {
isOrderCancellationTransaction,
isOrderAmendmentTransaction,
isBatchMarketInstructionsTransaction,
+ isTransferTransaction,
} from './connectors';
import { determineId } from './utils';
@@ -184,9 +185,14 @@ export const useVegaTransactionStore = create
(
);
if (transaction) {
transaction.transactionResult = transactionResult;
- if (
+
+ const isConfirmedOrderCancellation =
isOrderCancellationTransaction(transaction.body) &&
- !transaction.body.orderCancellation.orderId &&
+ !transaction.body.orderCancellation.orderId;
+ const isConfirmedTransfer = isTransferTransaction(transaction.body);
+
+ if (
+ (isConfirmedOrderCancellation || isConfirmedTransfer) &&
!transactionResult.error &&
transactionResult.status
) {
diff --git a/libs/wallet/src/utils.ts b/libs/wallet/src/utils.ts
index fcc032b5d..af5f4fd50 100644
--- a/libs/wallet/src/utils.ts
+++ b/libs/wallet/src/utils.ts
@@ -1,6 +1,6 @@
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/react-helpers';
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 { ethers } from 'ethers';
import { sha3_256 } from 'js-sha3';
@@ -8,6 +8,7 @@ import type {
OrderAmendmentBody,
OrderSubmissionBody,
Transaction,
+ Transfer,
} from './connectors';
/**
@@ -63,3 +64,23 @@ export const normalizeOrderAmendment = (
? toNanoSeconds(order.expiresAt) // Wallet expects timestamp in nanoseconds
: 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: {},
+ };
+};