Compare commits

..
Author SHA1 Message Date
Matthew Russell 239f5c7c2a chore: fix lint 2023-11-02 16:56:10 -07:00
Matthew Russell 5819421daa test: fix tests 2023-11-02 14:59:07 -07:00
Matthew Russell 228f468f69 fix: amount validation should use selected account, use max button 2023-11-02 14:23:47 -07:00
Matthew Russell 8625ab22e9 chore: rename toAddress for clarity 2023-11-02 13:46:50 -07:00
Matthew Russell 7812bd8fa9 fix: always show the key dropdown even if only single key 2023-11-02 13:44:48 -07:00
Edd 06cfd79415 feat(explorer): add timestamp to tx list (#5174) 2023-11-02 13:22:37 +00:00
Edd 443220283c fix(explorer): format new network params correctly (#5172) 2023-11-02 13:17:11 +00:00
Matthew Russell a642bf8ce4 chore(trading): misc changes to trades and transfer (#5175) 2023-11-01 15:57:38 -07:00
Matthew Russell 20233db706 fix(deal-ticket): fees breakdown rendering issues (#5173) 2023-11-01 13:32:51 -07:00
m.ray d4f50eb70c fix(trading): pagination limit for candles (#5169) 2023-11-01 12:21:36 -07:00
Matthew Russell e5d3f90d45 feat(trading): transfer to and from accounts on same key (#5161) 2023-11-01 11:11:17 +00:00
Bartłomiej Głownia 673c896e2f feat(deal-ticket): get discounts for estimation from previous epoch (#5163) 2023-11-01 10:09:56 +00:00
Matthew Russell deea63fa5e fix(trading): empty items loaded text (#5158) 2023-11-01 10:08:16 +00:00
Matthew Russell d31333538b chore(trading): ignore .next from lint (#5162) 2023-11-01 10:06:53 +00:00
Matthew Russell 4af9979a21 chore(trading): referrals stats tweaks (#5160) 2023-11-01 10:06:36 +00:00
m.ray 6421cf87c6 feat(trading): liquidity provision current and pending on update (#5152) 2023-10-31 11:24:18 +00:00
Matthew Russell 0ebfab64ff fix(ui-toolkit): sparkline appearing to go up when the price moved down (#5137) 2023-10-31 11:20:48 +00:00
Edd 142f08343b feat(governance): add update benefit tiers table (#5146) 2023-10-31 10:20:38 +00:00
48 changed files with 1150 additions and 542 deletions
@@ -82,7 +82,7 @@ describe('Txs infinite list item', () => {
expect(screen.getByText('Missing vital data')).toBeInTheDocument();
});
it('renders data correctly', () => {
it('renders data even with missing time', () => {
render(
<MockedProvider>
<MemoryRouter>
@@ -105,5 +105,33 @@ describe('Txs infinite list item', () => {
expect(screen.getByTestId('pub-key')).toHaveTextContent('testPubKey');
expect(screen.getByTestId('tx-type')).toHaveTextContent('testType');
expect(screen.getByTestId('tx-block')).toHaveTextContent('1');
expect(screen.getByTestId('tx-time')).toHaveTextContent('-');
});
it('renders data correctly', () => {
render(
<MockedProvider>
<MemoryRouter>
<table>
<tbody>
<TxsInfiniteListItem
type="testType"
submitter="testPubKey"
hash="testTxHash"
block="1"
code={0}
command={{}}
createdAt="1970-11-01T18:07:15Z"
/>
</tbody>
</table>
</MemoryRouter>
</MockedProvider>
);
expect(screen.getByTestId('tx-hash')).toHaveTextContent('testTxHash');
expect(screen.getByTestId('pub-key')).toHaveTextContent('testPubKey');
expect(screen.getByTestId('tx-type')).toHaveTextContent('testType');
expect(screen.getByTestId('tx-block')).toHaveTextContent('1');
expect(screen.getByTestId('tx-time').textContent).toMatch(/years ago/);
});
});
@@ -9,6 +9,7 @@ import { PartyLink } from '../links';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import type { Screen } from '@vegaprotocol/react-helpers';
import { useMemo } from 'react';
import { TimeAgo } from '../time-ago';
const DEFAULT_TRUNCATE_LENGTH = 7;
@@ -32,6 +33,7 @@ export const TxsInfiniteListItem = ({
type,
block,
command,
createdAt,
}: Partial<BlockExplorerTransactionResult>) => {
const { screenSize } = useScreenDimensions();
const idTruncateLength = useMemo(
@@ -85,6 +87,11 @@ export const TxsInfiniteListItem = ({
endChars={5}
/>
</td>
{['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize) && (
<td className="text-sm items-center font-mono" data-testid="tx-time">
{createdAt ? <TimeAgo date={createdAt} /> : '-'}
</td>
)}
</tr>
);
};
@@ -3,6 +3,7 @@ import { TxsInfiniteListItem } from './txs-infinite-list-item';
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
import EmptyList from '../empty-list/empty-list';
import { Loader } from '@vegaprotocol/ui-toolkit';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
interface TxsInfiniteListProps {
hasMoreTxs: boolean;
@@ -19,7 +20,16 @@ interface ItemProps {
}
const Item = ({ tx }: ItemProps) => {
const { hash, submitter, type, command, block, code, index: blockIndex } = tx;
const {
hash,
submitter,
type,
command,
block,
code,
createdAt,
index: blockIndex,
} = tx;
return (
<TxsInfiniteListItem
type={type}
@@ -29,6 +39,7 @@ const Item = ({ tx }: ItemProps) => {
hash={hash}
block={block}
index={blockIndex}
createdAt={createdAt}
/>
);
};
@@ -39,6 +50,7 @@ export const TxsInfiniteList = ({
className,
hasFilters = false,
}: TxsInfiniteListProps) => {
const { screenSize } = useScreenDimensions();
if (!txs || txs.length === 0) {
if (!areTxsLoading) {
return (
@@ -66,6 +78,9 @@ export const TxsInfiniteList = ({
<th>{t('Type')}</th>
<th className="text-left">{t('From')}</th>
<th>{t('Block')}</th>
{['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize) && (
<th>{t('Time')}</th>
)}
</tr>
</thead>
<tbody>
@@ -36,30 +36,50 @@ const PERCENTAGE_PARAMS = [
'governance.proposal.updateNetParam.requiredMajority',
'governance.proposal.updateNetParam.requiredParticipation',
'governance.proposal.updateMarket.minProposerEquityLikeShare',
'governance.proposal.VolumeDiscountProgram.requiredMajority',
'governance.proposal.VolumeDiscountProgram.requiredParticipation',
'governance.proposal.referralProgram.requiredMajority',
'governance.proposal.transfer.requiredMajority',
'governance.proposal.updateAsset.requiredMajority',
'governance.proposal.updateAsset.requiredParticipation',
'governance.proposal.transfer.requiredParticipation',
'governance.proposal.referralProgram.requiredParticipation',
'network.validators.ersatz.rewardFactor',
'network.validators.ersatz.multipleOfTendermintValidators',
'validators.vote.required',
];
'referralProgram.maxReferralRewardFactor',
'referralProgram.maxReferralDiscountFactor',
'referralProgram.maxReferralRewardProportion',
].map((p) => p.toLowerCase());
const BIG_NUMBER_PARAMS = [
'spam.protection.delegation.min.tokens',
'validators.delegation.minAmount',
'governance.proposal.transfer.maxAmount',
'reward.staking.delegation.minimumValidatorStake',
'reward.staking.delegation.maxPayoutPerParticipant',
'reward.staking.delegation.maxPayoutPerEpoch',
'spam.protection.voting.min.tokens',
'spam.protection.proposal.min.tokens',
'governance.proposal.transfer.minVoterBalance',
'governance.proposal.freeform.minProposerBalance',
'governance.proposal.updateNetParam.minVoterBalance',
'governance.proposal.updateMarket.minVoterBalance',
'governance.proposal.asset.minVoterBalance',
'governance.proposal.updateNetParam.minProposerBalance',
'governance.proposal.freeform.minVoterBalance',
'spam.protection.proposal.min.tokens',
'governance.proposal.updateMarket.minProposerBalance',
'governance.proposal.asset.minProposerBalance',
'governance.proposal.transfer.minProposerBalance',
'governance.proposal.market.minProposerBalance',
'governance.proposal.market.minVoterBalance',
'governance.proposal.updateAsset.minProposerBalance',
'governance.proposal.updateAsset.minVoterBalance',
];
'governance.proposal.referralProgram.minProposerBalance',
'governance.proposal.referralProgram.minVoterBalance',
'governance.proposal.VolumeDiscountProgram.minProposerBalance',
'governance.proposal.VolumeDiscountProgram.minVoterBalance',
].map((p) => p.toLowerCase());
export const renderGroupedParams = (
group: GroupedParams,
@@ -131,12 +151,12 @@ export const NetworkParameterRow = ({
<div className="pb-2">
<SyntaxHighlighter data={JSON.parse(value)} />
</div>
) : BIG_NUMBER_PARAMS.includes(key.toLowerCase()) ? (
addDecimalsFormatNumber(Number(value), 18)
) : PERCENTAGE_PARAMS.includes(key.toLowerCase()) ? (
`${formatNumber(Number(value) * 100, 0)}%`
) : isNaN(Number(value)) ? (
value
) : BIG_NUMBER_PARAMS.includes(key) ? (
addDecimalsFormatNumber(Number(value), 18)
) : PERCENTAGE_PARAMS.includes(key) ? (
`${formatNumber(Number(value) * 100, 0)}%`
) : (
formatNumber(Number(value), 4)
)}
@@ -14,6 +14,10 @@ export interface BlockExplorerTransactionResult {
value: string;
};
error?: string;
// These aren't strictly optional but are new in 0.73.0 so we need to make them optional
createdAt?: string;
version?: string;
pow?: string;
}
export interface BlockExplorerTransactions {
@@ -909,6 +909,14 @@
"BenefitTierReferralDiscountFactorDescription": "The proportion of the referee's taker fees to be discounted",
"BenefitTierReferralRewardFactor": "Referral reward factor",
"BenefitTierReferralRewardFactorDescription": "The proportion of the referee's taker fees to be rewarded to the referrer",
"BenefitTierMinimumActivityStreak": "Minimum activity streak",
"BenefitTierMinimumActivityStreakDescription": "The minimum number of times the party needs to have completed the activity",
"BenefitTierMinimumQuantumBalance": "Minimum quantum balance",
"BenefitTierMinimumQuantumBalanceDescription": "The minimum amount of the vesting token to qualify",
"BenefitTierVestingMultiplier": "Vesting multiplier",
"BenefitTierVestingMultiplierDescription": "Vesting multiplier for the tier",
"BenefitTierRewardMultiplier": "Reward multiplier",
"BenefitTierRewardMultiplierDescription": "The multiplier",
"StakingTiers": "Staking tiers",
"StakingTierMinimumStakedTokens": "Minimum staked tokens",
"StakingTierMinimumStakedTokensDescription": "Required number of governance tokens ($VEGA) a referrer must have staked to receive the multiplier",
@@ -36,13 +36,13 @@ describe('ProposalReferralProgramDetails helper functions', () => {
it('should format referral discount factor correctly', () => {
const input = '0.05';
const formatted = formatReferralDiscountFactor(input);
expect(formatted).toBe('5.00%');
expect(formatted).toBe('5%');
});
it('should format referral reward factor correctly', () => {
const input = '0.1';
const formatted = formatReferralRewardFactor(input);
expect(formatted).toBe('10.00%');
expect(formatted).toBe('10%');
});
it('should format minimum staked tokens correctly', () => {
@@ -0,0 +1 @@
export * from './proposal-update-benefit-tiers-details';
@@ -0,0 +1,158 @@
import { render, screen } from '@testing-library/react';
import { ProposalUpdateBenefitTiers } from './proposal-update-benefit-tiers-details';
import { generateProposal } from '../../test-helpers/generate-proposals';
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
useAppState: () => ({
appState: {
decimals: 2,
},
}),
}));
const mockVestingBenefitTierProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateNetworkParameter',
networkParameter: {
key: 'blah.blah.benefitTiers',
value: JSON.stringify({
tiers: [
{
minimum_quantum_balance: '10000',
reward_multiplier: '0.05',
},
{
minimum_quantum_balance: '500000000000',
reward_multiplier: '0.1',
},
{
minimum_quantum_balance: '10000000000000',
reward_multiplier: '10',
},
],
}),
},
},
},
});
const mockActivityStreakBenefitTierProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateNetworkParameter',
networkParameter: {
key: 'blah.blah.benefitTiers',
value: JSON.stringify({
tiers: [
{
minimum_activity_streak: '10000',
vesting_multiplier: '5',
reward_multiplier: '0.1',
},
{
minimum_activity_streak: '10000000000000',
vesting_multiplier: '100',
reward_multiplier: '10',
},
],
}),
},
},
},
});
describe('ProposalUpdateBenefitTiers', () => {
it('should not render if proposal is null', () => {
render(<ProposalUpdateBenefitTiers proposal={null} />);
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
});
it('should not render if __typename is not UpdateNetworkParameter', () => {
const updateMarketProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateMarket',
},
},
});
render(<ProposalUpdateBenefitTiers proposal={updateMarketProposal} />);
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
});
it('should not render if there are no relevant fields', () => {
const incompleteProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateNetworkParameter',
},
},
});
render(<ProposalUpdateBenefitTiers proposal={incompleteProposal} />);
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
});
it('should not render if there are relevant fields that are empty', () => {
const incompleteProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateNetworkParameter',
networkParameter: {
key: 'blah.blah.benefitTiers',
value: JSON.stringify({}),
},
},
},
});
render(<ProposalUpdateBenefitTiers proposal={incompleteProposal} />);
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
});
it('should render a valid vesting benefit tier proposal', () => {
render(
<ProposalUpdateBenefitTiers proposal={mockVestingBenefitTierProposal} />
);
// 3 tiers in the sample data
expect(screen.getByText('Tier 1')).toBeInTheDocument();
expect(screen.getByText('Tier 2')).toBeInTheDocument();
expect(screen.getByText('Tier 3')).toBeInTheDocument();
expect(screen.getAllByText('Minimum quantum balance').length).toBe(3);
expect(screen.getAllByText('Reward multiplier').length).toBe(3);
expect(screen.getByText('0.00000000000001')).toBeInTheDocument();
expect(screen.getByText('0.05x')).toBeInTheDocument();
expect(screen.getByText('0.0000005')).toBeInTheDocument();
expect(screen.getByText('0.1x')).toBeInTheDocument();
expect(screen.getByText('0.00001')).toBeInTheDocument();
expect(screen.getByText('10x')).toBeInTheDocument();
});
it('should render a valid activity streak benefit tier proposal', () => {
render(
<ProposalUpdateBenefitTiers
proposal={mockActivityStreakBenefitTierProposal}
/>
);
// 3 tiers in the sample data
expect(screen.getByText('Tier 1')).toBeInTheDocument();
expect(screen.getByText('Tier 2')).toBeInTheDocument();
expect(screen.getAllByText('Minimum activity streak').length).toBe(2);
expect(screen.getAllByText('Vesting multiplier').length).toBe(2);
expect(screen.getAllByText('Reward multiplier').length).toBe(2);
expect(screen.getByText('10000')).toBeInTheDocument();
expect(screen.getByText('5x')).toBeInTheDocument();
expect(screen.getByText('0.1x')).toBeInTheDocument();
expect(screen.getByText('10000000000000')).toBeInTheDocument();
expect(screen.getByText('100x')).toBeInTheDocument();
expect(screen.getByText('10x')).toBeInTheDocument();
});
});
@@ -0,0 +1,164 @@
import { useTranslation } from 'react-i18next';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import {
KeyValueTable,
KeyValueTableRow,
RoundedWrapper,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import {
formatMinimumStakedTokens,
formatReferralRewardMultiplier,
} from '../proposal-referral-program-details';
import { formatNumberPercentage } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
// These types are not generated as it's not known how dynamic these are
type VestingBenefitTier = {
minimum_quantum_balance: string;
reward_multiplier: string;
};
type ActivityStreakBenefitTier = {
minimum_activity_streak: number;
reward_multiplier: string;
vesting_multiplier: string;
};
export type BenefitTiers =
| Array<ActivityStreakBenefitTier>
| Array<VestingBenefitTier>;
export function getBenefitTiers(json: string): BenefitTiers {
try {
const parsed = JSON.parse(json);
return parsed.tiers;
} catch (e) {
return [];
}
}
export const formatVolumeDiscountFactor = (value: string) => {
return formatNumberPercentage(new BigNumber(value).times(100));
};
interface ProposalReferralProgramDetailsProps {
proposal: ProposalQuery['proposal'];
}
/**
* Special rendered for network proposals that change any benefit tiers,
* which is detected by:
* 1) it being a network parameter change
* 2) the name of the field ending in `.benefitTiers`
*
* It only renders known fields so that they can be formatted correctly.
*/
export const ProposalUpdateBenefitTiers = ({
proposal,
}: ProposalReferralProgramDetailsProps) => {
const { t } = useTranslation();
if (
proposal?.terms?.change?.__typename !== 'UpdateNetworkParameter' ||
proposal?.terms?.change?.networkParameter.key.slice(-13) !== '.benefitTiers'
) {
return null;
}
const benefitTiersString = proposal?.terms?.change?.networkParameter.value;
const benefitTiers = getBenefitTiers(benefitTiersString);
if (!benefitTiers) {
return null;
}
return (
<div data-testid="proposal-update-benefit-tiers">
<RoundedWrapper paddingBottom={true}>
{benefitTiers && (
<div
className="mb-6"
data-testid="proposal-volume-discount-program-benefit-tiers"
>
<h3 className="mb-3 uppercase font-semibold text-lg">
{t('BenefitTiers')}
</h3>
<KeyValueTable>
{benefitTiers
.sort(
(a, b) =>
Number(a.reward_multiplier) - Number(b.reward_multiplier)
)
.map((benefitTier, index) => (
<div className="mb-4" key={index}>
<h4 className="font-semibold uppercase">
Tier {index + 1}
</h4>
{'minimum_activity_streak' in benefitTier && (
<KeyValueTableRow
data-testid={`mas-${benefitTier.reward_multiplier}`}
>
<Tooltip
description={t(
'BenefitTierMinimumActivityStreakDescription'
)}
>
<span>{t('BenefitTierMinimumActivityStreak')}</span>
</Tooltip>
{benefitTier.minimum_activity_streak}
</KeyValueTableRow>
)}
{'minimum_quantum_balance' in benefitTier && (
<KeyValueTableRow
data-testid={`mqb-${benefitTier.reward_multiplier}`}
>
<Tooltip
description={t(
'BenefitTierMinimumQuantumBalanceDescription'
)}
>
<span>{t('BenefitTierMinimumQuantumBalance')}</span>
</Tooltip>
{formatMinimumStakedTokens(
benefitTier.minimum_quantum_balance,
18
)}
</KeyValueTableRow>
)}
{'vesting_multiplier' in benefitTier && (
<KeyValueTableRow
data-testid={`vm-${benefitTier.reward_multiplier}`}
>
<Tooltip
description={t('BenefitTierVestingMultiplier')}
>
<span>{t('BenefitTierVestingMultiplier')}</span>
</Tooltip>
{formatReferralRewardMultiplier(
benefitTier.vesting_multiplier
)}
</KeyValueTableRow>
)}
{'reward_multiplier' in benefitTier && (
<KeyValueTableRow
data-testid={`rm-${benefitTier.reward_multiplier}`}
>
<Tooltip description={t('BenefitTierRewardMultiplier')}>
<span>{t('BenefitTierRewardMultiplier')}</span>
</Tooltip>
{formatReferralRewardMultiplier(
benefitTier.reward_multiplier
)}
</KeyValueTableRow>
)}
</div>
))}
</KeyValueTable>
</div>
)}
</RoundedWrapper>
</div>
);
};
@@ -27,6 +27,7 @@ import {
ProposalTransferDetails,
} from '../proposal-transfer';
import { FLAGS } from '@vegaprotocol/environment';
import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers';
export interface ProposalProps {
proposal: ProposalQuery['proposal'];
@@ -243,6 +244,14 @@ export const Proposal = ({
</div>
)}
{proposal.terms.change.__typename === 'UpdateNetworkParameter' &&
proposal.terms.change.networkParameter.key.slice(-13) ===
'.benefitTiers' && (
<div className="mb-4">
<ProposalUpdateBenefitTiers proposal={proposal} />
</div>
)}
{governanceTransferDetails}
<div className="mb-10">
+1 -1
View File
@@ -5,7 +5,7 @@
"next",
"next/core-web-vitals"
],
"ignorePatterns": ["!**/*", "__generated__"],
"ignorePatterns": ["!**/*", "__generated__", ".next"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
@@ -71,7 +71,7 @@ export const useReferral = (args: UseReferralArgs) => {
variables: {
code: referralSet?.id as string,
aggregationEpochs:
args.aggregationEpochs != null
args.aggregationEpochs !== null
? args.aggregationEpochs
: DEFAULT_AGGREGATION_DAYS,
},
@@ -3,6 +3,8 @@ import {
VegaIcon,
VegaIconNames,
truncateMiddle,
ExternalLink,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
@@ -28,6 +30,7 @@ import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
import BigNumber from 'bignumber.js';
import { t } from '@vegaprotocol/i18n';
import maxBy from 'lodash/maxBy';
import { DocsLinks } from '@vegaprotocol/environment';
export const ReferralStatistics = () => {
const { pubKey } = useVegaWallet();
@@ -201,7 +204,7 @@ export const Statistics = ({
'Total commission (last %s epochs)',
(details?.windowLength || DEFAULT_AGGREGATION_DAYS).toString()
)}
description={t('(qUSD)')}
description={<QUSDTooltip />}
>
{getNumberFormat(0).format(Number(totalCommissionValue))}
</StatTile>
@@ -233,7 +236,12 @@ export const Statistics = ({
<StatTile title={t('Discount')}>{discountFactorValue * 100}%</StatTile>
);
const runningVolumeTile = (
<StatTile title={t('Combined volume')}>
<StatTile
title={t(
'Combined volume (last %s epochs)',
details?.windowLength.toString()
)}
>
{compactNumFormat.format(runningVolumeValue)}
</StatTile>
);
@@ -241,24 +249,14 @@ export const Statistics = ({
<StatTile title={t('Epochs in set')}>{epochsValue}</StatTile>
);
const nextTierVolumeTile = (
<StatTile
title={t(
'Volume to next tier %s',
nextBenefitTierValue?.tier ? `(${nextBenefitTierValue.tier})` : ''
)}
>
<StatTile title={t('Volume to next tier')}>
{nextBenefitTierVolumeValue <= 0
? '0'
: compactNumFormat.format(nextBenefitTierVolumeValue)}
</StatTile>
);
const nextTierEpochsTile = (
<StatTile
title={t(
'Epochs to next tier %s',
nextBenefitTierValue?.tier ? `(${nextBenefitTierValue.tier})` : ''
)}
>
<StatTile title={t('Epochs to next tier')}>
{nextBenefitTierEpochsValue <= 0 ? '0' : nextBenefitTierEpochsValue}
</StatTile>
);
@@ -339,11 +337,16 @@ export const Statistics = ({
},
{
name: 'commission',
displayName: t(
'Commission earned (last %s epochs)',
(
details?.windowLength || DEFAULT_AGGREGATION_DAYS
).toString()
displayName: (
<>
{t('Commission earned in')} <QUSDTooltip />{' '}
{t(
'(last %s epochs)',
(
details?.windowLength || DEFAULT_AGGREGATION_DAYS
).toString()
)}
</>
),
},
]}
@@ -373,3 +376,25 @@ export const Statistics = ({
</>
);
};
export const QUSDTooltip = () => (
<Tooltip
description={
<>
<p className="mb-1">
{t(
'qUSD provides a rough USD equivalent of balances across all assets using the value of "Quantum" for that asset'
)}
</p>
{DocsLinks && (
<ExternalLink href={DocsLinks.QUANTUM}>
{t('Find out more')}
</ExternalLink>
)}
</>
}
underline={true}
>
<span>{t('qUSD')}</span>
</Tooltip>
);
+11 -14
View File
@@ -1,10 +1,10 @@
import { Tooltip, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import { forwardRef, type HTMLAttributes } from 'react';
import { forwardRef, type ReactNode, type HTMLAttributes } from 'react';
import { BORDER_COLOR, GRADIENT } from './constants';
type TableColumnDefinition = {
displayName?: string;
displayName?: ReactNode;
name: string;
tooltip?: string;
className?: string;
@@ -46,7 +46,7 @@ export const Table = forwardRef<
INNER_BORDER_STYLE
)}
>
<span className="flex flex-row gap-2 items-center">
<span className="flex flex-row items-center gap-2">
<span>{displayName}</span>
{tooltip ? (
<Tooltip description={tooltip}>
@@ -102,17 +102,14 @@ export const Table = forwardRef<
key={`${i}-${name}`}
>
{/** display column name in mobile view */}
{!noCollapse &&
!noHeader &&
displayName &&
displayName.length > 0 && (
<span
aria-hidden
className="md:hidden font-mono text-xs px-0 text-vega-clight-100 dark:text-vega-cdark-100"
>
{displayName}
</span>
)}
{!noCollapse && !noHeader && displayName && (
<span
aria-hidden
className="px-0 font-mono text-xs md:hidden text-vega-clight-100 dark:text-vega-cdark-100"
>
{displayName}
</span>
)}
<span>{d[name]}</span>
</td>
))}
+3 -3
View File
@@ -29,7 +29,7 @@ export const Tile = ({
type StatTileProps = {
title: string;
description?: string;
description?: ReactNode;
children?: ReactNode;
};
export const StatTile = ({ title, description, children }: StatTileProps) => {
@@ -67,11 +67,11 @@ export const CodeTile = ({
title={t('Your referral code')}
description={createdAt ? t('(Created at: %s)', createdAt) : undefined}
>
<div className="flex gap-2 items-center justify-between">
<div className="flex items-center justify-between gap-2">
<Tooltip
description={
<div className="break-all">
<span className="text-xl bg-rainbow bg-clip-text text-transparent">
<span className="text-xl text-transparent bg-rainbow bg-clip-text">
{code}
</span>
</div>
@@ -112,6 +112,8 @@ const USE_ACCOUNT_TYPES = [
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
AccountType.ACCOUNT_TYPE_FEES_MAKER,
AccountType.ACCOUNT_TYPE_PENDING_TRANSFERS,
AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
AccountType.ACCOUNT_TYPE_VESTING_REWARDS,
];
const getAssetIds = (data: Account[]) =>
+11 -18
View File
@@ -1,5 +1,5 @@
import * as Schema from '@vegaprotocol/types';
import { addDecimal, truncateByChars } from '@vegaprotocol/utils';
import { truncateByChars } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
NetworkParams,
@@ -9,12 +9,16 @@ import { useDataProvider } from '@vegaprotocol/data-provider';
import type { Transfer } from '@vegaprotocol/wallet';
import { useVegaTransactionStore } from '@vegaprotocol/web3';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useCallback, useMemo } from 'react';
import { useCallback } from 'react';
import { accountsDataProvider } from './accounts-data-provider';
import { TransferForm } from './transfer-form';
import sortBy from 'lodash/sortBy';
import { Lozenge } from '@vegaprotocol/ui-toolkit';
export const ALLOWED_ACCOUNTS = [
Schema.AccountType.ACCOUNT_TYPE_GENERAL,
Schema.AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
];
export const TransferContainer = ({ assetId }: { assetId?: string }) => {
const { pubKey, pubKeys } = useVegaWallet();
const { param } = useNetworkParam(NetworkParams.transfer_fee_factor);
@@ -33,20 +37,9 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
[create]
);
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]);
const accounts = data
? data.filter((account) => ALLOWED_ACCOUNTS.includes(account.type))
: [];
return (
<>
@@ -65,10 +58,10 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
<TransferForm
pubKey={pubKey}
pubKeys={pubKeys ? pubKeys?.map((pk) => pk.publicKey) : null}
assets={sortBy(assets, 'name')}
assetId={assetId}
feeFactor={param}
submitTransfer={transfer}
accounts={accounts}
/>
</>
);
+151 -180
View File
@@ -1,18 +1,34 @@
import {
act,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import BigNumber from 'bignumber.js';
import { AddressField, TransferFee, TransferForm } from './transfer-form';
import { AccountType } from '@vegaprotocol/types';
import { addDecimal, formatNumber, removeDecimal } from '@vegaprotocol/utils';
import userEvent from '@testing-library/user-event';
import { removeDecimal } from '@vegaprotocol/utils';
describe('TransferForm', () => {
const submit = () => fireEvent.submit(screen.getByTestId('transfer-form'));
const submit = async () => {
await userEvent.click(
screen.getByRole('button', { name: 'Confirm transfer' })
);
};
const selectAsset = async (asset: {
id: string;
name: string;
decimals: number;
}) => {
// Bypass RichSelect and target hidden native select
// eslint-disable-next-line
fireEvent.change(document.querySelector('select[name="asset"]')!, {
target: { value: asset.id },
});
// assert rich select as updated
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
asset.name
);
};
const amount = '100';
const pubKey =
'70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680';
@@ -21,7 +37,6 @@ describe('TransferForm', () => {
symbol: '€',
name: 'EUR',
decimals: 2,
balance: addDecimal(100000, 2), // 1000
};
const props = {
pubKey,
@@ -29,9 +44,20 @@ describe('TransferForm', () => {
pubKey,
'a4b6e3de5d7ef4e31ae1b090be49d1a2ef7bcefff60cccf7658a0d4922651cce',
],
assets: [asset],
feeFactor: '0.001',
submitTransfer: jest.fn(),
accounts: [
{
type: AccountType.ACCOUNT_TYPE_GENERAL,
asset,
balance: '100000',
},
{
type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
asset,
balance: '100000',
},
],
};
it('form tooltips correctly displayed', async () => {
@@ -42,49 +68,45 @@ describe('TransferForm', () => {
// 1003-TRAN-019
render(<TransferForm {...props} />);
// Select a pubkey
fireEvent.change(screen.getByLabelText('Vega key'), {
target: { value: props.pubKeys[1] },
});
await userEvent.selectOptions(
screen.getByLabelText('To Vega key'),
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 } }
);
await selectAsset(asset);
// set valid amount
fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: amount },
});
const amountInput = screen.getByLabelText('Amount');
await userEvent.type(amountInput, amount);
expect(amountInput).toHaveValue(amount);
userEvent.hover(screen.getByText('Include transfer fee'));
const includeTransferLabel = screen.getByText('Include transfer fee');
await userEvent.hover(includeTransferLabel);
expect(await screen.findByRole('tooltip')).toHaveTextContent(
'The fee will be taken from the amount you are transferring.'
);
await userEvent.unhover(screen.getByText('Include transfer fee'));
await waitFor(() => {
const tooltips = screen.getAllByTestId('tooltip-content');
expect(tooltips[0]).toBeVisible();
});
const transferFee = screen.getByText('Transfer fee');
await userEvent.hover(transferFee);
expect(await screen.findByRole('tooltip')).toHaveTextContent(
/transfer.fee.factor/
);
await userEvent.unhover(transferFee);
userEvent.hover(screen.getByText('Transfer fee'));
const amountToBeTransferred = screen.getByText('Amount to be transferred');
await userEvent.hover(amountToBeTransferred);
expect(await screen.findByRole('tooltip')).toHaveTextContent(
/without the fee/
);
await userEvent.unhover(amountToBeTransferred);
await waitFor(() => {
const tooltips = screen.getAllByTestId('tooltip-content');
expect(tooltips[0]).toBeVisible();
});
userEvent.hover(screen.getByText('Amount to be transferred'));
await waitFor(() => {
const tooltips = screen.getAllByTestId('tooltip-content');
expect(tooltips[0]).toBeVisible();
});
userEvent.hover(screen.getByText('Total amount (with fee)'));
await waitFor(() => {
const tooltips = screen.getAllByTestId('tooltip-content');
expect(tooltips[0]).toBeVisible();
});
const totalAmountWithFee = screen.getByText('Total amount (with fee)');
await userEvent.hover(totalAmountWithFee);
expect(await screen.findByRole('tooltip')).toHaveTextContent(
/total amount taken from your account/
);
});
it('validates a manually entered address', async () => {
@@ -92,30 +114,23 @@ describe('TransferForm', () => {
// 1003-TRAN-013
// 1003-TRAN-004
render(<TransferForm {...props} />);
submit();
expect(await screen.findAllByText('Required')).toHaveLength(3);
await submit();
expect(await screen.findAllByText('Required')).toHaveLength(3); // pubkey is set as default value
const toggle = screen.getByText('Enter manually');
fireEvent.click(toggle);
await userEvent.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');
});
expect(screen.getByLabelText('To Vega key')).toHaveAttribute(
'type',
'text'
);
await userEvent.type(
screen.getByLabelText('To Vega key'),
'invalid-address'
);
expect(screen.getAllByTestId('input-error-text')[0]).toHaveTextContent(
'Invalid Vega key'
);
});
it('validates fields and submits', async () => {
@@ -127,68 +142,58 @@ describe('TransferForm', () => {
render(<TransferForm {...props} />);
// check current pubkey not shown
const keySelect: HTMLSelectElement = screen.getByLabelText('Vega key');
expect(keySelect.children).toHaveLength(2);
const keySelect = screen.getByLabelText<HTMLSelectElement>('To Vega key');
expect(keySelect.children).toHaveLength(3);
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual([
'',
pubKey,
props.pubKeys[1],
]);
submit();
expect(await screen.findAllByText('Required')).toHaveLength(3);
await submit();
expect(await screen.findAllByText('Required')).toHaveLength(3); // pubkey is set as default value
// Select a pubkey
fireEvent.change(screen.getByLabelText('Vega key'), {
target: { value: props.pubKeys[1] },
});
await userEvent.selectOptions(
screen.getByLabelText('To Vega key'),
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 } }
);
await selectAsset(asset);
// assert rich select as updated
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
asset.name
);
expect(await screen.findByTestId('asset-balance')).toHaveTextContent(
formatNumber(asset.balance, asset.decimals)
await userEvent.selectOptions(
screen.getByLabelText('From account'),
AccountType.ACCOUNT_TYPE_VESTED_REWARDS
);
const amountInput = screen.getByLabelText('Amount');
// Test amount validation
fireEvent.change(amountInput, {
target: { value: '0.00000001' },
});
await userEvent.type(amountInput, '0.00000001');
expect(
await screen.findByText('Value is below minimum')
).toBeInTheDocument();
fireEvent.change(amountInput, {
target: { value: '9999999' },
});
await userEvent.clear(amountInput);
await userEvent.type(amountInput, '9999999');
expect(
await screen.findByText(/cannot transfer more/i)
).toBeInTheDocument();
// set valid amount
fireEvent.change(amountInput, {
target: { value: amount },
});
await userEvent.clear(amountInput);
await userEvent.type(amountInput, amount);
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(
new BigNumber(props.feeFactor).times(amount).toFixed()
);
submit();
await submit();
await waitFor(() => {
expect(props.submitTransfer).toHaveBeenCalledTimes(1);
expect(props.submitTransfer).toHaveBeenCalledWith({
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
to: props.pubKeys[1],
asset: asset.id,
@@ -200,59 +205,50 @@ describe('TransferForm', () => {
describe('IncludeFeesCheckbox', () => {
it('validates fields and submits when checkbox is checked', async () => {
render(<TransferForm {...props} />);
const mockSubmit = jest.fn();
render(<TransferForm {...props} submitTransfer={mockSubmit} />);
// 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],
]);
const keySelect = screen.getByLabelText<HTMLSelectElement>('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
);
submit();
expect(await screen.findAllByText('Required')).toHaveLength(3);
await submit();
expect(await screen.findAllByText('Required')).toHaveLength(3); // pubkey set as default value
// Select a pubkey
fireEvent.change(screen.getByLabelText('Vega key'), {
target: { value: props.pubKeys[1] },
});
await userEvent.selectOptions(
screen.getByLabelText('To Vega key'),
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 } }
);
await selectAsset(asset);
// assert rich select as updated
expect(await screen.findByTestId('select-asset')).toHaveTextContent(
asset.name
);
expect(await screen.findByTestId('asset-balance')).toHaveTextContent(
formatNumber(asset.balance, asset.decimals)
await userEvent.selectOptions(
screen.getByLabelText('From account'),
AccountType.ACCOUNT_TYPE_VESTED_REWARDS
);
const amountInput = screen.getByLabelText('Amount');
const checkbox = screen.getByTestId('include-transfer-fee');
// 1003-TRAN-022
expect(checkbox).not.toBeChecked();
act(() => {
/* fire events that update state */
// set valid amount
fireEvent.change(amountInput, {
target: { value: amount },
});
// check include fees checkbox
fireEvent.click(checkbox);
});
await userEvent.clear(amountInput);
await userEvent.type(amountInput, amount);
await userEvent.click(checkbox);
expect(checkbox).toBeChecked();
const expectedFee = new BigNumber(amount)
.times(props.feeFactor)
.toFixed();
const expectedAmount = new BigNumber(amount).minus(expectedFee).toFixed();
// 1003-TRAN-020
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(
@@ -262,18 +258,17 @@ describe('TransferForm', () => {
amount
);
submit();
await submit();
await waitFor(() => {
// 1003-TRAN-023
expect(props.submitTransfer).toHaveBeenCalledTimes(1);
expect(props.submitTransfer).toHaveBeenCalledWith({
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
expect(mockSubmit).toHaveBeenCalledTimes(1);
expect(mockSubmit).toHaveBeenCalledWith({
fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
to: props.pubKeys[1],
asset: asset.id,
amount: removeDecimal(amount, asset.decimals),
amount: removeDecimal(expectedAmount, asset.decimals),
oneOff: {},
});
});
@@ -283,47 +278,30 @@ describe('TransferForm', () => {
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],
]);
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
);
submit();
expect(await screen.findAllByText('Required')).toHaveLength(3);
await submit();
expect(await screen.findAllByText('Required')).toHaveLength(3); // pubkey set as default value
// Select a pubkey
fireEvent.change(screen.getByLabelText('Vega key'), {
target: { value: props.pubKeys[1] },
});
await userEvent.selectOptions(
screen.getByLabelText('To Vega key'),
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(await screen.findByTestId('asset-balance')).toHaveTextContent(
formatNumber(asset.balance, asset.decimals)
);
await selectAsset(asset);
const amountInput = screen.getByLabelText('Amount');
const checkbox = screen.getByTestId('include-transfer-fee');
expect(checkbox).not.toBeChecked();
act(() => {
/* fire events that update state */
// set valid amount
fireEvent.change(amountInput, {
target: { value: amount },
});
});
await userEvent.type(amountInput, amount);
expect(checkbox).not.toBeChecked();
const expectedFee = new BigNumber(amount)
.times(props.feeFactor)
@@ -338,7 +316,6 @@ describe('TransferForm', () => {
describe('AddressField', () => {
const props = {
pubKeys: ['pubkey-1', 'pubkey-2'],
select: <div>select</div>,
input: <div>input</div>,
onChange: jest.fn(),
@@ -348,24 +325,18 @@ describe('TransferForm', () => {
const mockOnChange = jest.fn();
render(<AddressField {...props} onChange={mockOnChange} />);
// select should be shown as multiple pubkeys provided
// select should be shown by default
expect(screen.getByText('select')).toBeInTheDocument();
expect(screen.queryByText('input')).not.toBeInTheDocument();
fireEvent.click(screen.getByText('Enter manually'));
await userEvent.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'));
await userEvent.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', () => {
+158 -79
View File
@@ -1,3 +1,4 @@
import sortBy from 'lodash/sortBy';
import {
minSafe,
maxSafe,
@@ -5,6 +6,7 @@ import {
vegaPublicKey,
addDecimal,
formatNumber,
addDecimalsFormatNumber,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
@@ -24,22 +26,22 @@ import type { ReactNode } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { AssetOption, Balance } from '@vegaprotocol/assets';
import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
interface FormFields {
toAddress: string;
toVegaKey: string;
asset: string;
amount: string;
fromAccount: AccountType;
}
interface TransferFormProps {
pubKey: string | null;
pubKeys: string[] | null;
assets: Array<{
id: string;
symbol: string;
name: string;
decimals: number;
accounts: Array<{
type: AccountType;
balance: string;
asset: { id: string; symbol: string; name: string; decimals: number };
}>;
assetId?: string;
feeFactor: string | null;
@@ -49,10 +51,10 @@ interface TransferFormProps {
export const TransferForm = ({
pubKey,
pubKeys,
assets,
assetId: initialAssetId,
feeFactor,
submitTransfer,
accounts,
}: TransferFormProps) => {
const {
control,
@@ -64,14 +66,50 @@ export const TransferForm = ({
} = useForm<FormFields>({
defaultValues: {
asset: initialAssetId,
toVegaKey: pubKey || '',
},
});
const assets = sortBy(
accounts
.filter((a) => a.type === AccountType.ACCOUNT_TYPE_GENERAL)
.map((account) => ({
...account.asset,
balance: addDecimal(account.balance, account.asset.decimals),
})),
'name'
);
const selectedPubKey = watch('toVegaKey');
const amount = watch('amount');
const fromAccount = watch('fromAccount');
const assetId = watch('asset');
const asset = assets.find((a) => a.id === assetId);
const account = accounts.find(
(a) => a.asset.id === assetId && a.type === fromAccount
);
const accountBalance =
account && addDecimal(account.balance, account.asset.decimals);
// General account for the selected asset
const generalAccount = accounts.find((a) => {
return (
a.asset.id === assetId && a.type === AccountType.ACCOUNT_TYPE_GENERAL
);
});
const [includeFee, setIncludeFee] = useState(false);
// Min viable amount given asset decimals EG for WEI 0.000000000000000001
const min = asset
? new BigNumber(addDecimal('1', asset.decimals))
: new BigNumber(0);
// Max amount given selected asset and from account
const max = accountBalance ? new BigNumber(accountBalance) : new BigNumber(0);
const transferAmount = useMemo(() => {
if (!amount) return undefined;
if (includeFee && feeFactor) {
@@ -90,10 +128,6 @@ export const TransferForm = ({
);
}, [amount, includeFee, transferAmount, feeFactor]);
const asset = useMemo(() => {
return assets.find((a) => a.id === assetId);
}, [assets, assetId]);
const onSubmit = useCallback(
(fields: FormFields) => {
if (!asset) {
@@ -102,28 +136,21 @@ export const TransferForm = ({
if (!transferAmount) {
throw new Error('Submitted transfer with no amount selected');
}
const transfer = normalizeTransfer(fields.toAddress, transferAmount, {
id: asset.id,
decimals: asset.decimals,
});
const transfer = normalizeTransfer(
fields.toVegaKey,
transferAmount,
fields.fromAccount,
AccountType.ACCOUNT_TYPE_GENERAL, // field is readonly in the form
{
id: asset.id,
decimals: asset.decimals,
}
);
submitTransfer(transfer);
},
[asset, submitTransfer, transferAmount]
);
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]);
// reset for placeholder workaround https://github.com/radix-ui/primitives/issues/1569
useEffect(() => {
if (!pubKey) {
@@ -137,57 +164,47 @@ export const TransferForm = ({
className="text-sm"
data-testid="transfer-form"
>
<TradingFormGroup label="Vega key" labelFor="to-address">
<TradingFormGroup label="To Vega key" labelFor="toVegaKey">
<AddressField
pubKeys={pubKeys}
onChange={() => setValue('toAddress', '')}
onChange={() => setValue('toVegaKey', '')}
select={
<TradingSelect
{...register('toAddress')}
id="to-address"
defaultValue=""
>
<TradingSelect {...register('toVegaKey')} id="toVegaKey">
<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>
))}
{pubKeys?.map((pk) => {
const text = pk === pubKey ? t('Current key: ') + pk : pk;
return (
<option key={pk} value={pk}>
{text}
</option>
);
})}
</TradingSelect>
}
input={
<TradingInput
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={true} // focus input immediately after is shown
id="to-address"
id="toVegaKey"
type="text"
{...register('toAddress', {
{...register('toVegaKey', {
validate: {
required,
vegaPublicKey,
sameKey: (value) => {
if (value === pubKey) {
return t('Vega key is the same as current key');
}
return true;
},
},
})}
/>
}
/>
{errors.toAddress?.message && (
<TradingInputError forInput="to-address">
{errors.toAddress.message}
{errors.toVegaKey?.message && (
<TradingInputError forInput="toVegaKey">
{errors.toVegaKey.message}
</TradingInputError>
)}
</TradingFormGroup>
<TradingFormGroup label="Asset" labelFor="asset">
<TradingFormGroup label={t('Asset')} labelFor="asset">
<Controller
control={control}
name="asset"
@@ -228,6 +245,68 @@ export const TransferForm = ({
</TradingInputError>
)}
</TradingFormGroup>
<TradingFormGroup label={t('From account')} labelFor="fromAccount">
<TradingSelect
id="fromAccount"
defaultValue=""
{...register('fromAccount', {
validate: {
required,
sameAccount: (value) => {
if (
pubKey === selectedPubKey &&
value === AccountType.ACCOUNT_TYPE_GENERAL
) {
return t(
'Cannot transfer to the same account type for the connected key'
);
}
return true;
},
},
})}
>
<option value="" disabled={true}>
{t('Please select')}
</option>
{accounts
.filter((a) => {
if (!assetId) return true;
return assetId === a.asset.id;
})
.map((a) => {
return (
<option value={a.type} key={`${a.type}-${a.asset.id}`}>
{AccountTypeMapping[a.type]} (
{addDecimalsFormatNumber(a.balance, a.asset.decimals)}{' '}
{a.asset.symbol})
</option>
);
})}
</TradingSelect>
{errors.fromAccount?.message && (
<TradingInputError forInput="fromAccount">
{errors.fromAccount.message}
</TradingInputError>
)}
</TradingFormGroup>
<TradingFormGroup label={t('To account')} labelFor="toAccount">
<TradingSelect
id="toAccount"
defaultValue={AccountType.ACCOUNT_TYPE_GENERAL}
>
<option value={AccountType.ACCOUNT_TYPE_GENERAL}>
{generalAccount
? `${
AccountTypeMapping[AccountType.ACCOUNT_TYPE_GENERAL]
} (${addDecimalsFormatNumber(
generalAccount.balance,
generalAccount.asset.decimals
)} ${generalAccount.asset.symbol})`
: AccountTypeMapping[AccountType.ACCOUNT_TYPE_GENERAL]}
</option>
</TradingSelect>
</TradingFormGroup>
<TradingFormGroup label="Amount" labelFor="amount">
<TradingInput
id="amount"
@@ -242,15 +321,24 @@ export const TransferForm = ({
maxSafe: (v) => {
const value = new BigNumber(v);
if (value.isGreaterThan(max)) {
return t(
'You cannot transfer more than your available collateral'
);
return t('You cannot transfer more than available');
}
return maxSafe(max)(v);
},
},
})}
/>
{accountBalance && (
<button
type="button"
className="absolute top-0 right-0 ml-auto text-xs underline"
onClick={() =>
setValue('amount', parseFloat(accountBalance).toString())
}
>
{t('Use max')}
</button>
)}
{errors.amount?.message && (
<TradingInputError forInput="amount">
{errors.amount.message}
@@ -362,40 +450,31 @@ export const TransferFee = ({
};
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;
});
const [isInput, setIsInput] = useState(false);
return (
<>
{isInput ? input : select}
{pubKeys && pubKeys.length > 1 && (
<button
type="button"
onClick={() => {
setIsInput((curr) => !curr);
onChange();
}}
className="absolute top-0 right-0 ml-auto text-sm underline"
>
{isInput ? t('Select from wallet') : t('Enter manually')}
</button>
)}
<button
type="button"
onClick={() => {
setIsInput((curr) => !curr);
onChange();
}}
className="absolute top-0 right-0 ml-auto text-xs underline"
>
{isInput ? t('Select from wallet') : t('Enter manually')}
</button>
</>
);
};
+5 -1
View File
@@ -20,7 +20,11 @@ query Candles($marketId: ID!, $interval: Interval!, $since: String!) {
code
}
}
candlesConnection(interval: $interval, since: $since) {
candlesConnection(
interval: $interval
since: $since
pagination: { last: 5000 }
) {
edges {
node {
...CandleFields
+1 -1
View File
@@ -46,7 +46,7 @@ export const CandlesDocument = gql`
code
}
}
candlesConnection(interval: $interval, since: $since) {
candlesConnection(interval: $interval, since: $since, pagination: {last: 5000}) {
edges {
node {
...CandleFields
+1
View File
@@ -23,3 +23,4 @@ export * from './lib/type-helpers';
export * from './lib/cells/grid-progress-bar';
export * from './lib/use-datagrid-events';
export * from './lib/pagination';
+51
View File
@@ -0,0 +1,51 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Pagination } from './pagination';
describe('Pagination', () => {
const props = {
pageInfo: {
hasNextPage: true,
},
count: 0,
onLoad: () => undefined,
showRetentionMessage: false,
hasDisplayedRows: false,
};
it('renders message for 0 rows', async () => {
const mockOnLoad = jest.fn();
render(<Pagination {...props} onLoad={mockOnLoad} />);
expect(screen.getByText('0 rows loaded')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: 'Load more' }));
expect(mockOnLoad).toHaveBeenCalled();
});
it('renders mesasge for multiple rows', async () => {
const mockOnLoad = jest.fn();
const count = 10;
render(<Pagination {...props} count={count} onLoad={mockOnLoad} />);
expect(screen.getByText(`${count} rows loaded`)).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: 'Load more' }));
expect(mockOnLoad).toHaveBeenCalled();
});
it('renders message for a single row', async () => {
const mockOnLoad = jest.fn();
const count = 1;
render(<Pagination {...props} count={count} onLoad={mockOnLoad} />);
expect(screen.getByText(`${count} row loaded`)).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: 'Load more' }));
expect(mockOnLoad).toHaveBeenCalled();
});
it('renders the data rentention message', () => {
render(<Pagination {...props} showRetentionMessage={true} />);
expect(screen.getByText(/data node retention/)).toBeInTheDocument();
});
it('renders the row filter message', () => {
render(<Pagination {...props} count={1} hasDisplayedRows={false} />);
expect(screen.getByText(/No rows matching/)).toBeInTheDocument();
});
});
+53
View File
@@ -0,0 +1,53 @@
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
export const Pagination = ({
count,
pageInfo,
onLoad,
hasDisplayedRows,
showRetentionMessage,
}: {
count: number;
pageInfo: { hasNextPage?: boolean } | null;
onLoad: () => void;
hasDisplayedRows: boolean;
showRetentionMessage: boolean;
}) => {
let rowMessage = '';
if (count && !pageInfo?.hasNextPage) {
rowMessage = t('all %s rows loaded', count.toString());
} else {
if (count === 1) {
rowMessage = t('%s row loaded', count.toString());
} else {
rowMessage = t('%s rows loaded', count.toString());
}
}
return (
<div className="flex items-center justify-between p-1 border-t border-default">
<div className="text-xs">
{false}
{showRetentionMessage &&
t(
'Depending on data node retention you may not be able see the full history'
)}
</div>
<div className="flex items-center text-xs">
<span>{rowMessage}</span>
{pageInfo?.hasNextPage ? (
<Button size="extra-small" className="ml-1" onClick={onLoad}>
{t('Load more')}
</Button>
) : null}
</div>
{count && hasDisplayedRows === false ? (
<div className="absolute text-xs top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
{t('No rows matching selected filters')}
</div>
) : null}
</div>
);
};
@@ -79,7 +79,7 @@ export const DealTicketFeeDetails = ({
}
formattedValue={
<>
{totalDiscountFactor && (
{totalDiscountFactor ? (
<Pill size="xxs" intent={Intent.Warning} className="mr-1">
-
{formatNumberPercentage(
@@ -87,14 +87,14 @@ export const DealTicketFeeDetails = ({
2
)}
</Pill>
)}
) : null}
{totalDiscountedFeeAmount &&
`~${formatValue(totalDiscountedFeeAmount, assetDecimals, quantum)}`}
</>
}
labelDescription={
<>
<p className="mb-2">
<div className="flex flex-col gap-2">
<p>
{t(
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}. Fees estimated are "taker" fees and will only be payable if the order trades aggressively. Rebate equal to the maker portion will be paid to the trader if the order trades passively.`
)}
@@ -108,7 +108,7 @@ export const DealTicketFeeDetails = ({
symbol={assetSymbol}
decimals={assetDecimals}
/>
</>
</div>
}
symbol={assetSymbol}
/>
@@ -0,0 +1,36 @@
import { render, screen } from '@testing-library/react';
import { FeesBreakdown } from './fees-breakdown';
describe('FeesBreakdown', () => {
it('formats fee factors correctly', () => {
const feeFactors = {
makerFee: '0.00005',
infrastructureFee: '0.001',
liquidityFee: '0.5',
};
const fees = {
makerFee: '100',
infrastructureFee: '100',
liquidityFee: '100',
};
const props = {
totalFeeAmount: '100',
fees,
feeFactors,
symbol: 'USD',
decimals: 2,
referralDiscountFactor: '0.01',
volumeDiscountFactor: '0.01',
};
render(<FeesBreakdown {...props} />);
expect(screen.getByText('Maker fee').nextElementSibling).toHaveTextContent(
'0.005%'
);
expect(
screen.getByText('Infrastructure fee').nextElementSibling
).toHaveTextContent('0.1%');
expect(
screen.getByText('Liquidity fee').nextElementSibling
).toHaveTextContent('50%');
});
});
@@ -33,7 +33,7 @@ const FeesBreakdownItem = ({
<dt className="col-span-2">{label}</dt>
{factor && (
<dd className="text-right col-span-1">
{formatNumberPercentage(new BigNumber(factor).times(100), 2)}
{formatNumberPercentage(new BigNumber(factor).times(100))}
</dd>
)}
<dd className="text-right col-span-3">
@@ -6,7 +6,7 @@ import type { EstimateFeesQuery } from './__generated__/EstimateOrder';
const data: EstimateFeesQuery = {
epoch: {
id: '2',
id: '3',
},
volumeDiscountStats: {
edges: [
@@ -117,7 +117,7 @@ describe('useEstimateFees', () => {
});
});
it('returns 0 discounts if discount stats are not at the current epoch', () => {
it('returns 0 discounts if discount stats are not at the current epoch -1', () => {
const { result } = renderHook(() =>
useEstimateFees(
{
@@ -136,7 +136,7 @@ describe('useEstimateFees', () => {
});
it('returns discounts', () => {
data.epoch.id = '1';
data.epoch.id = '2';
const { result } = renderHook(() =>
useEstimateFees(
{
@@ -34,14 +34,13 @@ export const useEstimateFees = (
skip: !pubKey || !order?.size || !order?.price || order.postOnly,
});
const data = loading ? currentData || previousData : currentData;
const atEpoch = (Number(data?.epoch.id) || 0) - 1;
const volumeDiscountFactor =
(data?.volumeDiscountStats.edges[0]?.node.atEpoch.toString() ===
data?.epoch.id &&
(data?.volumeDiscountStats.edges[0]?.node.atEpoch === atEpoch &&
data?.volumeDiscountStats.edges[0]?.node.discountFactor) ||
'0';
const referralDiscountFactor =
(data?.referralSetStats.edges[0]?.node.atEpoch.toString() ===
data?.epoch.id &&
(data?.referralSetStats.edges[0]?.node.atEpoch === atEpoch &&
data?.referralSetStats.edges[0]?.node.discountFactor) ||
'0';
if (order?.postOnly) {
+1
View File
@@ -84,6 +84,7 @@ export const DocsLinks = VEGA_DOCS_URL
ETH_DATA_SOURCES: `${VEGA_DOCS_URL}/concepts/trading-on-vega/data-sources#ethereum-data-sources`,
ICEBERG_ORDERS: `${VEGA_DOCS_URL}/concepts/trading-on-vega/orders#iceberg-order`,
POST_REDUCE_ONLY: `${VEGA_DOCS_URL}/concepts/trading-on-vega/orders#conditional-order-parameters`,
QUANTUM: `${VEGA_DOCS_URL}/concepts/assets/asset-framework#quantum`,
}
: undefined;
+8 -26
View File
@@ -3,10 +3,10 @@ import { useCallback, useRef, useState } from 'react';
import { t } from '@vegaprotocol/i18n';
import { FillsTable } from './fills-table';
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
import { Pagination } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type * as Schema from '@vegaprotocol/types';
import { fillsWithMarketProvider } from './fills-data-provider';
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
interface FillsManagerProps {
partyId: string;
@@ -52,31 +52,13 @@ export const FillsManager = ({
overlayNoRowsTemplate={error ? error.message : t('No fills')}
{...props}
/>
<div className="flex justify-between border-t border-default p-1 items-center">
<div className="text-xs">
{t(
'Depending on data node retention you may not be able see the "full" history'
)}
</div>
<div className="flex text-xs items-center">
{data?.length && !pageInfo?.hasNextPage
? t('all %s items loaded', [data.length.toString()])
: t('%s items loaded', [
data?.length ? data.length.toString() : ' ',
])}
{pageInfo?.hasNextPage ? (
<Button size="extra-small" className="ml-1" onClick={() => load()}>
{t('Load more')}
</Button>
) : null}
</div>
{data?.length && hasDisplayedRow === false ? (
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
{t('No fills matching selected filters')}
</div>
) : null}
</div>
<Pagination
count={data?.length || 0}
pageInfo={pageInfo}
showRetentionMessage={true}
onLoad={load}
hasDisplayedRows={hasDisplayedRow || false}
/>
</div>
);
};
@@ -2,10 +2,10 @@ import type { AgGridReact } from 'ag-grid-react';
import { useCallback, useRef, useState } from 'react';
import { t } from '@vegaprotocol/i18n';
import { FundingPaymentsTable } from './funding-payments-table';
import { Pagination } from '@vegaprotocol/datagrid';
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { fundingPaymentsWithMarketProvider } from './funding-payments-data-provider';
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
interface FundingPaymentsManagerProps {
partyId: string;
@@ -57,30 +57,13 @@ export const FundingPaymentsManager = ({
overlayNoRowsTemplate={error ? error.message : t('No funding payments')}
{...props}
/>
<div className="flex justify-between border-t border-default p-1 items-center">
<div className="text-xs">
{t(
'Depending on data node retention you may not be able see the "full" history'
)}
</div>
<div className="flex text-xs items-center">
{data?.length && !pageInfo?.hasNextPage
? t('all %s items loaded', [data.length.toString()])
: t('%s items loaded', [
data?.length ? data.length.toString() : ' ',
])}
{pageInfo?.hasNextPage ? (
<Button size="extra-small" className="ml-1" onClick={() => load()}>
{t('Load more')}
</Button>
) : null}
</div>
{data?.length && hasDisplayedRow === false ? (
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
{t('No funding payments matching selected filters')}
</div>
) : null}
</div>
<Pagination
count={data?.length || 0}
pageInfo={pageInfo}
onLoad={load}
hasDisplayedRows={hasDisplayedRow || false}
showRetentionMessage={true}
/>
</div>
);
};
@@ -21,7 +21,10 @@ import type {
} from './__generated__/MarketLiquidity';
export type LiquidityProvisionFields = LiquidityProvisionFieldsFragment &
Schema.LiquiditySLAParameters;
Schema.LiquiditySLAParameters & {
currentCommitmentAmount?: string;
currentFee?: string;
};
export const liquidityProvisionsDataProvider = makeDataProvider<
LiquidityProvisionsQuery,
@@ -34,10 +37,28 @@ export const liquidityProvisionsDataProvider = makeDataProvider<
getData: (responseData: LiquidityProvisionsQuery | null) => {
return (responseData?.market?.liquidityProvisions?.edges
?.filter((n) => !!n)
.map((e) => ({
...e?.node.current,
...responseData.market?.liquiditySLAParameters,
})) ?? []) as LiquidityProvisionFields[];
.map((e) => {
let node;
if (!e?.node.pending && e?.node.current) {
node = {
...e?.node.current,
...responseData.market?.liquiditySLAParameters,
};
} else if (!e?.node.current && e?.node.pending) {
node = {
...e?.node.pending,
...responseData.market?.liquiditySLAParameters,
};
} else {
node = {
...e?.node.pending,
currentCommitmentAmount: e?.node.current.commitmentAmount,
currentFee: e?.node.current.fee,
...responseData.market?.liquiditySLAParameters,
};
}
return node;
}) ?? []) as LiquidityProvisionFields[];
},
});
@@ -91,7 +112,8 @@ export const matchFilter = (filter: Filter, lp: LiquidityProvisionData) => {
}
if (
filter.active === true &&
lp.status !== Schema.LiquidityProvisionStatus.STATUS_ACTIVE
lp.status !== Schema.LiquidityProvisionStatus.STATUS_ACTIVE &&
lp.status !== Schema.LiquidityProvisionStatus.STATUS_PENDING
) {
return false;
}
@@ -105,7 +127,7 @@ export const matchFilter = (filter: Filter, lp: LiquidityProvisionData) => {
};
export interface LiquidityProvisionData
extends Omit<LiquidityProvisionFieldsFragment, '__typename'>,
extends Omit<LiquidityProvisionFields, '__typename'>,
Partial<LiquidityProviderFieldsFragment>,
Omit<Schema.LiquiditySLAParameters, '__typename'> {
assetDecimalPlaces?: number;
@@ -113,11 +135,12 @@ export interface LiquidityProvisionData
averageEntryValuation?: string;
equityLikeShare?: string;
earmarkedFees?: number;
status: Schema.LiquidityProvisionStatus;
}
export const getLiquidityProvision = (
liquidityProvisions: LiquidityProvisionFields[],
liquidityProvider: LiquidityProviderFieldsFragment[],
liquidityProviders: LiquidityProviderFieldsFragment[],
filter?: Filter
): LiquidityProvisionData[] => {
return liquidityProvisions
@@ -136,12 +159,14 @@ export const getLiquidityProvision = (
}
return true;
})
.map((lp) => {
const lpObj = liquidityProvider.find((f) => lp.party.id === f.partyId);
if (!lpObj) return lp;
const accounts = compact(lp.party.accountsConnection?.edges).map(
(e) => e.node
.map((liquidityProvision) => {
const liquidityProvider = liquidityProviders.find(
(f) => liquidityProvision.party.id === f.partyId
);
if (!liquidityProvider) return liquidityProvision;
const accounts = compact(
liquidityProvision.party.accountsConnection?.edges
).map((e) => e.node);
const bondAccounts = accounts?.filter(
(a) => a?.type === Schema.AccountType.ACCOUNT_TYPE_BOND
);
@@ -164,8 +189,8 @@ export const getLiquidityProvision = (
)
.toNumber() ?? 0;
return {
...lp,
...lpObj,
...liquidityProvision,
...liquidityProvider,
balance,
earmarkedFees,
__typename: undefined,
+57 -5
View File
@@ -6,7 +6,10 @@ import {
getDateTimeFormat,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type { TypedDataAgGrid } from '@vegaprotocol/datagrid';
import type {
TypedDataAgGrid,
VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import { AgGrid } from '@vegaprotocol/datagrid';
import {
CopyWithTooltip,
@@ -22,7 +25,7 @@ import type {
ValueFormatterParams,
} from 'ag-grid-community';
import BigNumber from 'bignumber.js';
import type { LiquidityProvisionStatus } from '@vegaprotocol/types';
import { LiquidityProvisionStatus } from '@vegaprotocol/types';
import { LiquidityProvisionStatusMapping } from '@vegaprotocol/types';
import type { LiquidityProvisionData } from './liquidity-data-provider';
@@ -187,7 +190,32 @@ export const LiquidityTable = ({
headerTooltip: t(
'The amount committed to the market by this liquidity provider.'
),
valueFormatter: assetDecimalsQuantumFormatter,
valueFormatter: ({
data,
value,
}: VegaValueFormatterParams<
LiquidityProvisionData,
'commitmentAmount'
>) => {
if (!value) return '-';
const formattedCommitmentAmount = addDecimalsFormatNumberQuantum(
value,
assetDecimalPlaces ?? 0,
quantum ?? 0
);
if (
data?.currentCommitmentAmount &&
data?.currentCommitmentAmount !== value
) {
return `${addDecimalsFormatNumberQuantum(
data.currentCommitmentAmount,
assetDecimalPlaces ?? 0,
quantum ?? 0
)}/${formattedCommitmentAmount}`;
} else {
return formattedCommitmentAmount;
}
},
tooltipValueGetter: assetDecimalsFormatter,
},
{
@@ -207,7 +235,22 @@ export const LiquidityTable = ({
),
field: 'fee',
type: 'rightAligned',
valueFormatter: percentageFormatter,
valueFormatter: ({
data,
value,
}: ValueFormatterParams<LiquidityProvisionData, 'fee'>) => {
if (!value) return '-';
const formattedValue =
formatNumberPercentage(new BigNumber(value).times(100), 2) ||
'-';
if (data?.currentFee && data?.currentFee !== value) {
return `${formatNumberPercentage(
new BigNumber(data.currentFee).times(100),
2
)}/${formattedValue}`;
}
return formattedValue;
},
},
{
headerName: t('Adjusted stake share'),
@@ -328,8 +371,17 @@ export const LiquidityTable = ({
headerName: t('Status'),
headerTooltip: t('The current status of this liquidity provision.'),
field: 'status',
valueFormatter: ({ value }) => {
valueFormatter: ({
data,
value,
}: ValueFormatterParams<LiquidityProvisionData, 'status'>) => {
if (!value) return value;
if (
data?.status === LiquidityProvisionStatus.STATUS_PENDING &&
(data?.currentCommitmentAmount || data?.currentFee)
) {
return t('Updating next epoch');
}
return LiquidityProvisionStatusMapping[
value as LiquidityProvisionStatus
];
+1 -1
View File
@@ -37,7 +37,7 @@ export const MarketCandlesDocument = gql`
marketsConnection(id: $marketId) {
edges {
node {
candlesConnection(interval: $interval, since: $since) {
candlesConnection(interval: $interval, since: $since, pagination: {last: 1000}) {
edges {
node {
...MarketCandlesFields
+1 -1
View File
@@ -19,7 +19,7 @@ export const MarketsCandlesDocument = gql`
edges {
node {
id
candlesConnection(interval: $interval, since: $since) {
candlesConnection(interval: $interval, since: $since, pagination: {last: 1000}) {
edges {
node {
...MarketCandlesFields
+5 -1
View File
@@ -11,7 +11,11 @@ query MarketCandles($interval: Interval!, $since: String!, $marketId: ID!) {
marketsConnection(id: $marketId) {
edges {
node {
candlesConnection(interval: $interval, since: $since) {
candlesConnection(
interval: $interval
since: $since
pagination: { last: 1000 }
) {
edges {
node {
...MarketCandlesFields
+4 -4
View File
@@ -72,10 +72,10 @@ describe('totalFeesFactorsPercentage', () => {
makerFee: f[2].toString(),
});
it.each([
{ i: createFee(0, 0, 1), o: '100.00%' },
{ i: createFee(0, 1, 0), o: '100.00%' },
{ i: createFee(1, 0, 0), o: '100.00%' },
{ i: createFee(0.01, 0.02, 0.003), o: '3.30%' },
{ i: createFee(0, 0, 1), o: '100%' },
{ i: createFee(0, 1, 0), o: '100%' },
{ i: createFee(1, 0, 0), o: '100%' },
{ i: createFee(0.01, 0.02, 0.003), o: '3.3%' },
{ i: createFee(0.01, 0.056782, 0.003), o: '6.9782%' },
{ i: createFee(0.01, 0.056782, 0), o: '6.6782%' },
])('adds fees correctly', ({ i, o }) => {
+5 -1
View File
@@ -12,7 +12,11 @@ query MarketsCandles($interval: Interval!, $since: String!) {
edges {
node {
id
candlesConnection(interval: $interval, since: $since) {
candlesConnection(
interval: $interval
since: $since
pagination: { last: 1000 }
) {
edges {
node {
...MarketCandlesFields
@@ -3,6 +3,7 @@ import { useCallback, useRef, useState, useEffect } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import { OrderListTable } from '../order-list';
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
import { Pagination } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { ordersWithMarketProvider } from '../order-data-provider/order-data-provider';
import { normalizeOrderAmendment } from '@vegaprotocol/wallet';
@@ -11,7 +12,7 @@ import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/web3';
import { OrderEditDialog } from '../order-list/order-edit-dialog';
import type { Order } from '../order-data-provider';
import { OrderViewDialog } from '../order-list/order-view-dialog';
import { Splash, TradingButton as Button } from '@vegaprotocol/ui-toolkit';
import { Splash } from '@vegaprotocol/ui-toolkit';
export enum Filter {
'Open' = 'Open',
@@ -90,61 +91,34 @@ export const OrderListManager = ({
return (
<>
<div className="h-full relative">
<div className="flex flex-col h-full">
<OrderListTable
rowData={data}
ref={gridRef}
filter={filter}
onCancel={cancel}
onEdit={setEditOrder}
onView={setViewOrder}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
onFilterChanged={(event) => {
onRowDataUpdated(event);
if (onFilterChanged) {
onFilterChanged(event);
}
}}
onRowDataUpdated={onRowDataUpdated}
isReadOnly={isReadOnly}
overlayNoRowsTemplate={noRowsMessage || t('No orders')}
{...props}
/>
<div className="flex justify-between border-t border-default p-1 items-center">
<div className="text-xs">
{variables.filter?.liveOnly
? null
: t(
'Depending on data node retention you may not be able see the "full" history'
)}
</div>
{data ? (
<div className="flex text-xs items-center">
{data?.length && !pageInfo?.hasNextPage
? t('all %s items loaded', [data.length.toString()])
: t('%s items loaded', [
data?.length ? data.length.toString() : ' ',
])}
{pageInfo?.hasNextPage ? (
<Button
size="extra-small"
className="ml-1"
onClick={() => load()}
>
{t('Load more')}
</Button>
) : null}
</div>
) : null}
{data?.length && hasDisplayedRow === false ? (
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
{t('No orders matching selected filters')}
</div>
) : null}
</div>
</div>
<div className="relative flex flex-col h-full">
<OrderListTable
rowData={data}
ref={gridRef}
filter={filter}
onCancel={cancel}
onEdit={setEditOrder}
onView={setViewOrder}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
onFilterChanged={(event) => {
onRowDataUpdated(event);
if (onFilterChanged) {
onFilterChanged(event);
}
}}
onRowDataUpdated={onRowDataUpdated}
isReadOnly={isReadOnly}
overlayNoRowsTemplate={noRowsMessage || t('No orders')}
{...props}
/>
<Pagination
count={data?.length || 0}
pageInfo={pageInfo}
onLoad={load}
hasDisplayedRows={hasDisplayedRow || false}
showRetentionMessage={variables.filter?.liveOnly || true}
/>
</div>
{editOrder && (
<OrderEditDialog
+9 -53
View File
@@ -4,9 +4,6 @@ import { TradesTable } from './trades-table';
import { useDealTicketFormValues } from '@vegaprotocol/deal-ticket';
import { t } from '@vegaprotocol/i18n';
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
import { useCallback, useState } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
interface TradesContainerProps {
marketId: string;
@@ -19,60 +16,19 @@ export const TradesManager = ({
}: TradesContainerProps) => {
const update = useDealTicketFormValues((state) => state.updateAll);
const { data, error, load, pageInfo } = useDataProvider({
const { data, error } = useDataProvider({
dataProvider: tradesWithMarketProvider,
variables: { marketId },
});
const [hasDisplayedRow, setHasDisplayedRow] = useState<boolean | undefined>(
undefined
);
const { onFilterChanged, ...props } = gridProps || {};
const onRowDataUpdated = useCallback(
({ api }: { api: AgGridReact['api'] }) => {
setHasDisplayedRow(!!api.getDisplayedRowCount());
},
[]
);
return (
<div className="flex flex-col h-full">
<TradesTable
rowData={data}
onClick={(price?: string) => {
update(marketId, { price });
}}
onFilterChanged={(event) => {
onRowDataUpdated(event);
onFilterChanged(event);
}}
onRowDataUpdated={onRowDataUpdated}
overlayNoRowsTemplate={error ? error.message : t('No trades')}
{...props}
/>
<div className="flex justify-between border-t border-default p-1 items-center">
<div className="text-xs">
{t(
'Depending on data node retention you may not be able see the "full" history'
)}
</div>
<div className="flex text-xs items-center">
{data?.length && !pageInfo?.hasNextPage
? t('all %s items loaded', [data.length.toString()])
: t('%s items loaded', [
data?.length ? data.length.toString() : ' ',
])}
{pageInfo?.hasNextPage ? (
<Button size="extra-small" className="ml-1" onClick={() => load()}>
{t('Load more')}
</Button>
) : null}
</div>
{data?.length && hasDisplayedRow === false ? (
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
{t('No trades matching selected filters')}
</div>
) : null}
</div>
</div>
<TradesTable
rowData={data}
onClick={(price?: string) => {
update(marketId, { price });
}}
overlayNoRowsTemplate={error ? error.message : t('No trades')}
{...gridProps}
/>
);
};
@@ -72,7 +72,7 @@ LessThan24HoursIncrease.args = {
export const LessThan24HoursDecrease = Template.bind({});
LessThan24HoursDecrease.args = {
data: [20, 21, 22, 23, 24, 6, 7, 9, 11, 13, 11, 9],
data: [20990000, 20939973, 20980130],
width: 110,
height: 30,
};
@@ -44,14 +44,12 @@ export const SparklineView = ({
return null;
}
const midValue = (min + max) / 2;
// Market may be less than 24hr old so padd the data array
// with values that is the mid value (avg of min and max).
// This will rendera horizontal line until the real data shifts the line
const padCount = data.length < points ? points - data.length : 0;
const padArr = new Array(padCount).fill(midValue);
const trimmedData = data.slice(-points);
const padCount = data.length < points ? points - data.length : 0;
const padArr = new Array(padCount).fill(trimmedData[0]);
// Get the last 24 values if data has more than needed
const lineData: [number, number][] = [...padArr, ...trimmedData].map(
+2 -2
View File
@@ -77,8 +77,8 @@ describe('number utils', () => {
{ v: new BigNumber(123.123), d: 3, o: '123.123%' },
{ v: new BigNumber(123.123), d: 6, o: '123.123%' },
{ v: new BigNumber(123.123), d: 0, o: '123%' },
{ v: new BigNumber(123), d: undefined, o: '123.00%' }, // it default to 2 decimal places
{ v: new BigNumber(30000), d: undefined, o: '30,000.00%' },
{ v: new BigNumber(123), d: undefined, o: '123%' }, // it default to 2 decimal places
{ v: new BigNumber(30000), d: undefined, o: '30,000%' },
{ v: new BigNumber(3.000001), d: undefined, o: '3.000001%' },
])('formats given number correctly', ({ v, d, o }) => {
expect(formatNumberPercentage(v, d)).toStrictEqual(o);
+1 -1
View File
@@ -158,7 +158,7 @@ export const addDecimalsFixedFormatNumber = (
export const formatNumberPercentage = (value: BigNumber, decimals?: number) => {
const decimalPlaces =
typeof decimals === 'undefined' ? Math.max(value.dp() || 0, 2) : decimals;
typeof decimals === 'undefined' ? value.dp() || 0 : decimals;
return `${formatNumber(value, decimalPlaces)}%`;
};
+5 -3
View File
@@ -1,6 +1,6 @@
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
import type { Market, Order } from '@vegaprotocol/types';
import { AccountType } from '@vegaprotocol/types';
import type { AccountType } from '@vegaprotocol/types';
import BigNumber from 'bignumber.js';
import { ethers } from 'ethers';
import { sha3_256 } from 'js-sha3';
@@ -47,15 +47,17 @@ export const normalizeOrderAmendment = <T extends Exact<OrderAmendment, T>>(
export const normalizeTransfer = <T extends Exact<Transfer, T>>(
address: string,
amount: string,
fromAccountType: AccountType,
toAccountType: AccountType,
asset: {
id: string;
decimals: number;
}
): Transfer => {
return {
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
to: address,
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
fromAccountType,
toAccountType,
asset: asset.id,
amount: removeDecimal(amount, asset.decimals),
// oneOff or recurring required otherwise wallet will error
+4 -2
View File
@@ -1,2 +1,4 @@
[functions]
included_files = ["!node_modules/@sentry/cli/sentry-cli"]
[[redirects]]
from = "/*"
to = "/index.html"
status = 200