diff --git a/libs/accounts/src/lib/TransferFee.graphql b/libs/accounts/src/lib/TransferFee.graphql new file mode 100644 index 000000000..49d5e9d84 --- /dev/null +++ b/libs/accounts/src/lib/TransferFee.graphql @@ -0,0 +1,18 @@ +query TransferFee( + $fromAccount: ID! + $fromAccountType: AccountType! + $toAccount: ID! + $amount: String! + $assetId: String! +) { + estimateTransferFee( + fromAccount: $fromAccount + fromAccountType: $fromAccountType + toAccount: $toAccount + amount: $amount + assetId: $assetId + ) { + fee + discount + } +} diff --git a/libs/accounts/src/lib/__generated__/TransferFee.ts b/libs/accounts/src/lib/__generated__/TransferFee.ts new file mode 100644 index 000000000..22ad02662 --- /dev/null +++ b/libs/accounts/src/lib/__generated__/TransferFee.ts @@ -0,0 +1,63 @@ +import * as Types from '@vegaprotocol/types'; + +import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; +const defaultOptions = {} as const; +export type TransferFeeQueryVariables = Types.Exact<{ + fromAccount: Types.Scalars['ID']; + fromAccountType: Types.AccountType; + toAccount: Types.Scalars['ID']; + amount: Types.Scalars['String']; + assetId: Types.Scalars['String']; +}>; + + +export type TransferFeeQuery = { __typename?: 'Query', estimateTransferFee?: { __typename?: 'EstimatedTransferFee', fee: string, discount: string } | null }; + + +export const TransferFeeDocument = gql` + query TransferFee($fromAccount: ID!, $fromAccountType: AccountType!, $toAccount: ID!, $amount: String!, $assetId: String!) { + estimateTransferFee( + fromAccount: $fromAccount + fromAccountType: $fromAccountType + toAccount: $toAccount + amount: $amount + assetId: $assetId + ) { + fee + discount + } +} + `; + +/** + * __useTransferFeeQuery__ + * + * To run a query within a React component, call `useTransferFeeQuery` and pass it any options that fit your needs. + * When your component renders, `useTransferFeeQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useTransferFeeQuery({ + * variables: { + * fromAccount: // value for 'fromAccount' + * fromAccountType: // value for 'fromAccountType' + * toAccount: // value for 'toAccount' + * amount: // value for 'amount' + * assetId: // value for 'assetId' + * }, + * }); + */ +export function useTransferFeeQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(TransferFeeDocument, options); + } +export function useTransferFeeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(TransferFeeDocument, options); + } +export type TransferFeeQueryHookResult = ReturnType; +export type TransferFeeLazyQueryHookResult = ReturnType; +export type TransferFeeQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/libs/accounts/src/lib/transfer-container.tsx b/libs/accounts/src/lib/transfer-container.tsx index a02f07f94..84ab0d1d1 100644 --- a/libs/accounts/src/lib/transfer-container.tsx +++ b/libs/accounts/src/lib/transfer-container.tsx @@ -25,7 +25,6 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => { const t = useT(); const { pubKey, pubKeys, isReadOnly } = useVegaWallet(); const { params } = useNetworkParams([ - NetworkParams.transfer_fee_factor, NetworkParams.transfer_minTransferQuantumMultiple, ]); @@ -72,7 +71,6 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => { pubKeys={pubKeys ? pubKeys?.map((pk) => pk.publicKey) : null} isReadOnly={isReadOnly} assetId={assetId} - feeFactor={params.transfer_fee_factor} minQuantumMultiple={params.transfer_minTransferQuantumMultiple} submitTransfer={transfer} accounts={sortedAccounts} diff --git a/libs/accounts/src/lib/transfer-form.spec.tsx b/libs/accounts/src/lib/transfer-form.spec.tsx index 369e52ced..d1eb6d738 100644 --- a/libs/accounts/src/lib/transfer-form.spec.tsx +++ b/libs/accounts/src/lib/transfer-form.spec.tsx @@ -15,6 +15,30 @@ import { } from './transfer-form'; import { AccountType, AccountTypeMapping } from '@vegaprotocol/types'; import { removeDecimal } from '@vegaprotocol/utils'; +import type { TransferFeeQuery } from './__generated__/TransferFee'; + +const feeFactor = 0.001; +const mockUseTransferFeeQuery = jest.fn( + ({ + variables: { amount }, + }: { + variables: { amount: string }; + }): { data: TransferFeeQuery } => { + return { + data: { + estimateTransferFee: { + discount: '0', + fee: (Number(amount) * feeFactor).toFixed(), + }, + }, + }; + } +); + +jest.mock('./__generated__/TransferFee', () => ({ + useTransferFeeQuery: (props: { variables: { amount: string } }) => + mockUseTransferFeeQuery(props), +})); describe('TransferForm', () => { const renderComponent = (props: TransferFormProps) => { @@ -56,7 +80,6 @@ describe('TransferForm', () => { const props = { pubKey, pubKeys: [pubKey, '2'.repeat(64)], - feeFactor: '0.001', submitTransfer: jest.fn(), accounts: [ { @@ -79,7 +102,6 @@ describe('TransferForm', () => { pubKey, 'a4b6e3de5d7ef4e31ae1b090be49d1a2ef7bcefff60cccf7658a0d4922651cce', ], - feeFactor: '0.001', submitTransfer: jest.fn(), accounts: [], minQuantumMultiple: '1', @@ -96,10 +118,6 @@ describe('TransferForm', () => { }); it.each([ - { - targetText: 'Transfer fee', - tooltipText: /transfer\.fee\.factor/, - }, { targetText: 'Amount to be transferred', tooltipText: /without the fee/, @@ -109,9 +127,6 @@ describe('TransferForm', () => { tooltipText: /total amount taken from your account/, }, ])('Tooltip for "$targetText" shows', async (o) => { - // 1003-TRAN-015 - // 1003-TRAN-016 - // 1003-TRAN-017 // 1003-TRAN-018 // 1003-TRAN-019 renderComponent(props); @@ -124,6 +139,10 @@ describe('TransferForm', () => { // Select asset await selectAsset(asset); + await userEvent.selectOptions( + screen.getByLabelText('From account'), + `${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}` + ); // set valid amount const amountInput = screen.getByLabelText('Amount'); await userEvent.type(amountInput, amount); @@ -214,9 +233,7 @@ describe('TransferForm', () => { // set valid amount await userEvent.clear(amountInput); await userEvent.type(amountInput, amount); - expect(screen.getByTestId('transfer-fee')).toHaveTextContent( - new BigNumber(props.feeFactor).times(amount).toFixed() - ); + expect(screen.getByTestId('transfer-fee')).toHaveTextContent('1'); await submit(); @@ -385,47 +402,44 @@ describe('TransferForm', () => { }); }); }); - describe('IncludeFeesCheckbox', () => { - it('validates fields when checkbox is not checked', async () => { - renderComponent(props); - // check current pubkey not shown - const keySelect: HTMLSelectElement = screen.getByLabelText('To Vega key'); - const pubKeyOptions = ['', pubKey, props.pubKeys[1]]; - expect(keySelect.children).toHaveLength(pubKeyOptions.length); - expect(Array.from(keySelect.options).map((o) => o.value)).toEqual( - pubKeyOptions - ); + it('validates fields', async () => { + renderComponent(props); - await submit(); - expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value + // check current pubkey not shown + const keySelect: HTMLSelectElement = screen.getByLabelText('To Vega key'); + const pubKeyOptions = ['', pubKey, props.pubKeys[1]]; + expect(keySelect.children).toHaveLength(pubKeyOptions.length); + expect(Array.from(keySelect.options).map((o) => o.value)).toEqual( + pubKeyOptions + ); - // Select a pubkey - await userEvent.selectOptions( - screen.getByLabelText('To Vega key'), - props.pubKeys[1] - ); + await submit(); + expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value - // Select asset - await selectAsset(asset); + // Select a pubkey + await userEvent.selectOptions( + screen.getByLabelText('To Vega key'), + props.pubKeys[1] + ); - await userEvent.selectOptions( - screen.getByLabelText('From account'), - `${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}` - ); + // Select asset + await selectAsset(asset); - const amountInput = screen.getByLabelText('Amount'); + await userEvent.selectOptions( + screen.getByLabelText('From account'), + `${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}` + ); - await userEvent.type(amountInput, amount); - const expectedFee = new BigNumber(amount) - .times(props.feeFactor) - .toFixed(); - const total = new BigNumber(amount).plus(expectedFee).toFixed(); - // 1003-TRAN-021 - expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee); - expect(screen.getByTestId('transfer-amount')).toHaveTextContent(amount); - expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total); - }); + const amountInput = screen.getByLabelText('Amount'); + + await userEvent.type(amountInput, amount); + const expectedFee = new BigNumber(amount).times(feeFactor).toFixed(); + const total = new BigNumber(amount).plus(expectedFee).toFixed(); + // 1003-TRAN-021 + expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee); + expect(screen.getByTestId('transfer-amount')).toHaveTextContent(amount); + expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total); }); describe('AddressField', () => { @@ -457,24 +471,29 @@ describe('TransferForm', () => { describe('TransferFee', () => { const props = { - amount: '200', - feeFactor: '0.001', - fee: '0.2', - transferAmount: '200', - decimals: 8, + amount: '20000', + discount: '0', + fee: '20', + decimals: 2, }; - it('calculates and renders the transfer fee', () => { + it('calculates and renders amounts and fee', () => { render(); - - const expected = new BigNumber(props.amount) - .times(props.feeFactor) - .toFixed(); - const total = new BigNumber(props.amount).plus(expected).toFixed(); - expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expected); - expect(screen.getByTestId('transfer-amount')).toHaveTextContent( - props.amount + expect(screen.queryByTestId('discount')).not.toBeInTheDocument(); + expect(screen.getByTestId('transfer-fee')).toHaveTextContent('0.2'); + expect(screen.getByTestId('transfer-amount')).toHaveTextContent('200.00'); + expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent( + '200.20' + ); + }); + + it('calculates and renders amounts, fee and discount', () => { + render(); + expect(screen.getByTestId('discount')).toHaveTextContent('0.1'); + expect(screen.getByTestId('transfer-fee')).toHaveTextContent('0.2'); + expect(screen.getByTestId('transfer-amount')).toHaveTextContent('200.00'); + expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent( + '200.10' ); - expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total); }); }); }); diff --git a/libs/accounts/src/lib/transfer-form.tsx b/libs/accounts/src/lib/transfer-form.tsx index d69f8dfde..a44697273 100644 --- a/libs/accounts/src/lib/transfer-form.tsx +++ b/libs/accounts/src/lib/transfer-form.tsx @@ -4,8 +4,9 @@ import { useRequired, useVegaPublicKey, addDecimal, - formatNumber, toBigNum, + removeDecimal, + addDecimalsFormatNumber, } from '@vegaprotocol/utils'; import { useT } from './use-t'; import { @@ -21,10 +22,11 @@ import type { Transfer } from '@vegaprotocol/wallet'; import { normalizeTransfer } from '@vegaprotocol/wallet'; import BigNumber from 'bignumber.js'; import type { ReactNode } from 'react'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { AssetOption, Balance } from '@vegaprotocol/assets'; import { AccountType, AccountTypeMapping } from '@vegaprotocol/types'; +import { useTransferFeeQuery } from './__generated__/TransferFee'; interface FormFields { toVegaKey: string; @@ -51,7 +53,6 @@ export interface TransferFormProps { asset: Asset; }>; assetId?: string; - feeFactor: string | null; minQuantumMultiple: string | null; submitTransfer: (transfer: Transfer) => void; } @@ -61,7 +62,6 @@ export const TransferForm = ({ pubKeys, isReadOnly, assetId: initialAssetId, - feeFactor, submitTransfer, accounts, minQuantumMultiple, @@ -136,11 +136,22 @@ export const TransferForm = ({ // Max amount given selected asset and from account const max = accountBalance ? new BigNumber(accountBalance) : new BigNumber(0); + const normalizedAmount = + (amount && asset && removeDecimal(amount, asset.decimals)) || '0'; - const fee = useMemo( - () => feeFactor && new BigNumber(feeFactor).times(amount).toString(), - [amount, feeFactor] - ); + const transferFeeQuery = useTransferFeeQuery({ + variables: { + fromAccount: pubKey || '', + fromAccountType: accountType || AccountType.ACCOUNT_TYPE_GENERAL, + amount: normalizedAmount, + assetId: asset?.id || '', + toAccount: selectedPubKey, + }, + skip: !pubKey || !amount || !asset || !selectedPubKey || fromVested, + }); + const transferFee = transferFeeQuery.loading + ? transferFeeQuery.data || transferFeeQuery.previousData + : transferFeeQuery.data; const onSubmit = useCallback( (fields: FormFields) => { @@ -432,12 +443,14 @@ export const TransferForm = ({ )} - {amount && fee && ( + {(transferFee?.estimateTransferFee || fromVested) && amount && asset && ( )} @@ -449,39 +462,44 @@ export const TransferForm = ({ export const TransferFee = ({ amount, - feeFactor, fee, + discount, decimals, }: { amount: string; - feeFactor: string | null; fee?: string; - decimals?: number; + discount?: string; + decimals: number; }) => { const t = useT(); - if (!feeFactor || !amount || !fee) return null; - if (isNaN(Number(feeFactor)) || isNaN(Number(amount)) || isNaN(Number(fee))) { + if (!amount || !fee) return null; + if (isNaN(Number(amount)) || isNaN(Number(fee))) { return null; } - const totalValue = new BigNumber(amount).plus(fee).toString(); + const totalValue = ( + BigInt(amount) + + BigInt(fee) - + BigInt(discount || '0') + ).toString(); return (
- -
{t('Transfer fee')}
-
- +
{t('Transfer fee')}
- {formatNumber(fee, decimals)} + {addDecimalsFormatNumber(fee, decimals)}
+ {discount && discount !== '0' && ( +
+
{t('Discount')}
+
+ {addDecimalsFormatNumber(discount, decimals)} +
+
+ )} +
- {formatNumber(amount, decimals)} + {addDecimalsFormatNumber(amount, decimals)}
@@ -505,7 +523,7 @@ export const TransferFee = ({
- {formatNumber(totalValue, decimals)} + {addDecimalsFormatNumber(totalValue, decimals)}
diff --git a/libs/i18n/src/locales/en/accounts.json b/libs/i18n/src/locales/en/accounts.json index cface53cb..8f5259e75 100644 --- a/libs/i18n/src/locales/en/accounts.json +++ b/libs/i18n/src/locales/en/accounts.json @@ -35,7 +35,6 @@ "The total amount of each asset on this key. Includes used and available collateral.": "The total amount of each asset on this key. Includes used and available collateral.", "The total amount taken from your account. The amount to be transferred plus the fee.": "The total amount taken from your account. The amount to be transferred plus the fee.", "The total amount to be transferred (without the fee)": "The total amount to be transferred (without the fee)", - "The transfer fee is set by the network parameter transfer.fee.factor, currently set to {{feeFactor}}": "The transfer fee is set by the network parameter transfer.fee.factor, currently set to {{feeFactor}}", "To account": "To account", "To Vega key": "To Vega key", "Total": "Total", diff --git a/specs/1003-TRAN-transfer.md b/specs/1003-TRAN-transfer.md index b6d08f33f..9f014c3de 100644 --- a/specs/1003-TRAN-transfer.md +++ b/specs/1003-TRAN-transfer.md @@ -40,8 +40,6 @@ ## Transfer -- **Must** display tooltip for "Transfer fee when hovered over.(1003-TRAN-017) - - **Must** display tooltip for "Amount to be transferred" when hovered over.(1003-TRAN-018) - **Must** display tooltip for "Total amount (with fee)" when hovered over.(1003-TRAN-019)