Compare commits

..
Author SHA1 Message Date
asiaznik cdbef93ab5 fix(environment): missing block height in node switcher 2023-11-07 17:18:14 +01:00
79 changed files with 1514 additions and 2107 deletions
-1
View File
@@ -52,4 +52,3 @@ cypress.env.json
/apps/**/cypress/reports/
/apps/**/cypress/downloads/
/apps/**/fixtures/wallet/node**
.nx/
@@ -133,7 +133,7 @@ export const proposalsData = {
instrument: {
name: 'UNIDAI Monthly (Dec 2022)',
code: 'UNIDAI.MF21',
product: {
futureProduct: {
settlementAsset: { symbol: 'tDAI', __typename: 'Asset' },
__typename: 'FutureProduct',
},
@@ -240,7 +240,7 @@ export const proposalsData = {
instrument: {
name: 'ETHBTC Quarterly (Feb 2023)',
code: 'ETHBTC.QM21',
product: {
futureProduct: {
settlementAsset: { symbol: 'tBTC', __typename: 'Asset' },
__typename: 'FutureProduct',
},
+2 -1
View File
@@ -35,5 +35,6 @@ NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
NX_PRODUCT_PERPETUALS=false
NX_UPDATE_MARKET_STATE=false
NX_REFERRALS=true
NX_REFERRALS=false
NX_GOVERNANCE_TRANSFERS=false
NX_VOLUME_DISCOUNTS=false
+2 -1
View File
@@ -31,8 +31,9 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
CYPRESS_FAIRGROUND=false
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_SUCCESSOR_MARKETS=false
NX_METAMASK_SNAPS=false
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
NX_VOLUME_DISCOUNTS=true
+1
View File
@@ -28,3 +28,4 @@ NX_METAMASK_SNAPS=true
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
NX_VOLUME_DISCOUNTS=true
+5 -4
View File
@@ -22,8 +22,9 @@ NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_SUCCESSOR_MARKETS=false
NX_METAMASK_SNAPS=false
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
NX_PRODUCT_PERPETUALS=false
NX_UPDATE_MARKET_STATE=false
NX_REFERRALS=false
NX_VOLUME_DISCOUNTS=false
+5 -4
View File
@@ -21,8 +21,9 @@ NX_TENDERMINT_URL=https://be.mainnet-mirror.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_SUCCESSOR_MARKETS=false
NX_METAMASK_SNAPS=false
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
NX_PRODUCT_PERPETUALS=false
NX_UPDATE_MARKET_STATE=false
NX_REFERRALS=false
NX_VOLUME_DISCOUNTS=false
+1
View File
@@ -25,3 +25,4 @@ NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
NX_GOVERNANCE_TRANSFERS=true
NX_VOLUME_DISCOUNTS=true
+1
View File
@@ -29,3 +29,4 @@ NX_METAMASK_SNAPS=true
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
NX_VOLUME_DISCOUNTS=true
+5 -4
View File
@@ -20,8 +20,9 @@ NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_SUCCESSOR_MARKETS=false
NX_METAMASK_SNAPS=false
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
NX_PRODUCT_PERPETUALS=false
NX_UPDATE_MARKET_STATE=false
NX_REFERRALS=false
NX_VOLUME_DISCOUNTS=false
@@ -189,6 +189,7 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
includeNewMarketProductFields: !!FLAGS.PRODUCT_PERPETUALS,
includeUpdateMarketStates: !!FLAGS.UPDATE_MARKET_STATE,
includeUpdateReferralPrograms: !!FLAGS.REFERRALS,
includeUpdateVolumeDiscountPrograms: !!FLAGS.VOLUME_DISCOUNTS,
},
});
@@ -77,7 +77,7 @@ describe('Proposal header', () => {
__typename: 'InstrumentConfiguration',
name: 'Some market',
code: 'FX:BTCUSD/DEC99',
product: {
futureProduct: {
__typename: 'FutureProduct',
settlementAsset: {
__typename: 'Asset',
@@ -39,18 +39,6 @@ export const ProposalHeader = ({
const titleContent = shorten(title ?? '', 100);
const getAsset = (proposal: ProposalQuery['proposal']) => {
const terms = proposal?.terms;
if (
terms?.change.__typename === 'NewMarket' &&
(terms.change.instrument.product?.__typename === 'FutureProduct' ||
terms.change.instrument.product?.__typename === 'PerpetualProduct')
) {
return terms.change.instrument.product.settlementAsset;
}
return undefined;
};
switch (change?.__typename) {
case 'NewMarket': {
proposalType =
@@ -66,10 +54,10 @@ export const ProposalHeader = ({
<span>
{t('Code')}: {change.instrument.code}.
</span>{' '}
{proposal?.terms && getAsset(proposal)?.symbol ? (
{change.instrument.futureProduct?.settlementAsset.symbol ? (
<>
<span className="font-semibold">
{getAsset(proposal)?.symbol}
{change.instrument.futureProduct.settlementAsset.symbol}
</span>{' '}
{t('settled future')}.
</>
@@ -78,7 +78,7 @@ export const ProposalVolumeDiscountProgramDetails = ({
{t('BenefitTiers')}
</h3>
<KeyValueTable>
{[...benefitTiers]
{benefitTiers
.sort(
(a, b) =>
Number(a.minimumRunningNotionalTakerVolume) -
@@ -8,7 +8,7 @@ import {
networkParamsQueryMock,
nextWeek,
} from '../../test-helpers/mocks';
import { CompactVotes, VoteBreakdown } from './vote-breakdown';
import { VoteBreakdown } from './vote-breakdown';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MockedResponse } from '@apollo/client/testing';
import {
@@ -346,22 +346,3 @@ describe('VoteBreakdown', () => {
expect(style.width).toBe(`${expectedProgress}%`);
});
});
describe('CompactVotes', () => {
it.each([
[0, '0'],
[1, '1'],
[12, '12'],
[123, '123'],
[1234, '1.2K'],
[12345, '12.3K'],
[123456, '123.5K'],
[1234567, '1.2M'],
[12345678, '12.3M'],
[123456789, '123.5M'],
[1234567890, '1.2B'],
])('compacts %s to %s', (input, output) => {
const { getByTestId } = render(<CompactVotes number={BigNumber(input)} />);
expect(getByTestId('compact-number').textContent).toBe(output);
});
});
@@ -3,21 +3,11 @@ import BigNumber from 'bignumber.js';
import { useTranslation } from 'react-i18next';
import { useVoteInformation } from '../../hooks';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '@vegaprotocol/utils';
import { formatNumber, toBigNum } from '@vegaprotocol/utils';
import { ProposalState } from '@vegaprotocol/types';
import type { ReactNode } from 'react';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { CompactNumber } from '@vegaprotocol/react-helpers';
export const CompactVotes = ({ number }: { number: BigNumber }) => (
<CompactNumber
number={number}
decimals={number.isGreaterThan(1000) ? 1 : 0}
compactAbove={1000}
compactDisplay="short"
/>
);
interface VoteBreakdownProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
@@ -208,7 +198,10 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
)}
>
<button>
<CompactVotes number={yesEquityLikeShareWeight} />
{yesEquityLikeShareWeight
.dividedBy(toBigNum(10 ** 6, 0))
.toFixed(1)}
M
</button>
</Tooltip>
<span>
@@ -233,7 +226,10 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
)}
>
<button>
<CompactVotes number={noEquityLikeShareWeight} />
{noEquityLikeShareWeight
.dividedBy(toBigNum(10 ** 6, 0))
.toFixed(1)}
M
</button>
</Tooltip>
<span>
@@ -283,7 +279,10 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
)}
>
<button>
<CompactVotes number={totalEquityLikeShareWeight} />
{totalEquityLikeShareWeight
.dividedBy(toBigNum(10 ** 6, 0))
.toFixed(1)}
M
</button>
</Tooltip>
<span>
@@ -322,7 +321,7 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
<span>{t('tokenVotesFor')}:</span>
<Tooltip description={formatNumber(yesTokens, defaultDP)}>
<button data-testid="num-votes-for">
<CompactVotes number={yesTokens} />
{yesTokens.dividedBy(toBigNum(10 ** 6, 0)).toFixed(1)}M
</button>
</Tooltip>
<span>
@@ -342,7 +341,7 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
<span>{t('tokenVotesAgainst')}:</span>
<Tooltip description={formatNumber(noTokens, defaultDP)}>
<button data-testid="num-votes-against">
<CompactVotes number={noTokens} />
{noTokens.dividedBy(toBigNum(10 ** 6, 0)).toFixed(1)}M
</button>
</Tooltip>
<span>
@@ -385,7 +384,7 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
<span>{t('totalTokensVoted')}:</span>
<Tooltip description={formatNumber(totalTokensVoted, defaultDP)}>
<button data-testid="total-voted">
<CompactVotes number={totalTokensVoted} />
{totalTokensVoted.dividedBy(toBigNum(10 ** 6, 0)).toFixed(1)}M
</button>
</Tooltip>
<span data-testid="total-voted-percentage">
@@ -84,6 +84,7 @@ query Proposal(
$includeNewMarketProductField: Boolean!
$includeUpdateMarketState: Boolean!
$includeUpdateReferralProgram: Boolean!
$includeUpdateVolumeDiscountProgram: Boolean!
) {
proposal(id: $proposalId) {
id
@@ -103,6 +104,7 @@ query Proposal(
...UpdateMarketState @include(if: $includeUpdateMarketState)
...UpdateReferralProgram @include(if: $includeUpdateReferralProgram)
...UpdateVolumeDiscountProgram
@include(if: $includeUpdateVolumeDiscountProgram)
terms {
closingDatetime
enactmentDatetime
@@ -130,25 +132,46 @@ query Proposal(
instrument {
name
code
product {
... on FutureProduct {
settlementAsset {
id
name
symbol
decimals
quantum
}
quoteName
dataSourceSpecBinding {
settlementDataProperty
tradingTerminationProperty
}
dataSourceSpecForSettlementData {
sourceType {
... on DataSourceDefinitionInternal {
sourceType {
... on DataSourceSpecConfigurationTime {
futureProduct {
settlementAsset {
id
name
symbol
decimals
quantum
}
quoteName
dataSourceSpecForSettlementData {
sourceType {
... on DataSourceDefinitionInternal {
sourceType {
... on DataSourceSpecConfigurationTime {
conditions {
operator
value
}
}
}
}
... on DataSourceDefinitionExternal {
sourceType {
... on DataSourceSpecConfiguration {
signers {
signer {
... on PubKey {
key
}
... on ETHAddress {
address
}
}
}
filters {
key {
name
type
}
conditions {
operator
value
@@ -156,44 +179,52 @@ query Proposal(
}
}
}
... on DataSourceDefinitionExternal {
sourceType {
... on DataSourceSpecConfiguration {
signers {
signer {
... on PubKey {
key
}
... on ETHAddress {
address
}
}
}
filters {
key {
name
type
}
conditions {
operator
value
}
}
}
}
}
}
}
}
... on PerpetualProduct {
settlementAsset {
id
name
symbol
decimals
quantum
}
quoteName
# dataSourceSpecForTradingTermination {
# sourceType {
# ... on DataSourceDefinitionInternal {
# sourceType {
# ... on DataSourceSpecConfigurationTime {
# conditions {
# operator
# value
# }
# }
# }
# }
# ... on DataSourceDefinitionExternal {
# sourceType {
# ... on DataSourceSpecConfiguration {
# signers {
# signer {
# ... on PubKey {
# key
# }
# ... on ETHAddress {
# address
# }
# }
# }
# filters {
# key {
# name
# type
# }
# conditions {
# operator
# value
# }
# }
# }
# }
# }
# }
# }
dataSourceSpecBinding {
settlementDataProperty
tradingTerminationProperty
}
}
}
File diff suppressed because one or more lines are too long
@@ -62,6 +62,7 @@ export const ProposalContainer = () => {
includeNewMarketProductField: !!FLAGS.PRODUCT_PERPETUALS,
includeUpdateMarketState: !!FLAGS.UPDATE_MARKET_STATE,
includeUpdateReferralProgram: !!FLAGS.REFERRALS,
includeUpdateVolumeDiscountProgram: !!FLAGS.VOLUME_DISCOUNTS,
},
skip: !params.proposalId,
});
@@ -101,16 +101,9 @@ fragment ProposalFields on Proposal {
instrument {
name
code
product {
... on FutureProduct {
settlementAsset {
symbol
}
}
... on PerpetualProduct {
settlementAsset {
symbol
}
futureProduct {
settlementAsset {
symbol
}
}
}
@@ -171,6 +164,7 @@ query Proposals(
$includeNewMarketProductFields: Boolean!
$includeUpdateMarketStates: Boolean!
$includeUpdateReferralPrograms: Boolean!
$includeUpdateVolumeDiscountPrograms: Boolean!
) {
proposalsConnection {
edges {
@@ -180,6 +174,7 @@ query Proposals(
...UpdateMarketStates @include(if: $includeUpdateMarketStates)
...UpdateReferralPrograms @include(if: $includeUpdateReferralPrograms)
...UpdateVolumeDiscountPrograms
@include(if: $includeUpdateVolumeDiscountPrograms)
}
}
}
@@ -11,16 +11,17 @@ export type UpdateReferralProgramsFragment = { __typename?: 'Proposal', terms: {
export type UpdateVolumeDiscountProgramsFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram', endOfProgramTimestamp: any, windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } } };
export type ProposalFieldsFragment = { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, product?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | { __typename?: 'PerpetualProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | { __typename?: 'SpotProduct' } | null } } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } };
export type ProposalFieldsFragment = { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | null } } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } };
export type ProposalsQueryVariables = Types.Exact<{
includeNewMarketProductFields: Types.Scalars['Boolean'];
includeUpdateMarketStates: Types.Scalars['Boolean'];
includeUpdateReferralPrograms: Types.Scalars['Boolean'];
includeUpdateVolumeDiscountPrograms: Types.Scalars['Boolean'];
}>;
export type ProposalsQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, product?: { __typename: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | { __typename: 'PerpetualProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | { __typename: 'SpotProduct' } | null } } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', decimalPlaces: number, id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename: 'Future', quoteName: string } | { __typename: 'Perpetual', quoteName: string } | { __typename: 'Spot' } } } } } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram', windowLength: number, endOfProgram: any, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram', endOfProgramTimestamp: any, windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } } } | null> | null } | null };
export type ProposalsQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | null, product?: { __typename: 'FutureProduct' } | { __typename: 'PerpetualProduct' } | { __typename: 'SpotProduct' } | null } } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', decimalPlaces: number, id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename: 'Future', quoteName: string } | { __typename: 'Perpetual', quoteName: string } | { __typename: 'Spot' } } } } } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram', windowLength: number, endOfProgram: any, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram', endOfProgramTimestamp: any, windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } } } | null> | null } | null };
export const NewMarketProductFieldsFragmentDoc = gql`
fragment NewMarketProductFields on Proposal {
@@ -130,16 +131,9 @@ export const ProposalFieldsFragmentDoc = gql`
instrument {
name
code
product {
... on FutureProduct {
settlementAsset {
symbol
}
}
... on PerpetualProduct {
settlementAsset {
symbol
}
futureProduct {
settlementAsset {
symbol
}
}
}
@@ -197,7 +191,7 @@ export const ProposalFieldsFragmentDoc = gql`
}
`;
export const ProposalsDocument = gql`
query Proposals($includeNewMarketProductFields: Boolean!, $includeUpdateMarketStates: Boolean!, $includeUpdateReferralPrograms: Boolean!) {
query Proposals($includeNewMarketProductFields: Boolean!, $includeUpdateMarketStates: Boolean!, $includeUpdateReferralPrograms: Boolean!, $includeUpdateVolumeDiscountPrograms: Boolean!) {
proposalsConnection {
edges {
node {
@@ -205,7 +199,7 @@ export const ProposalsDocument = gql`
...NewMarketProductFields @include(if: $includeNewMarketProductFields)
...UpdateMarketStates @include(if: $includeUpdateMarketStates)
...UpdateReferralPrograms @include(if: $includeUpdateReferralPrograms)
...UpdateVolumeDiscountPrograms
...UpdateVolumeDiscountPrograms @include(if: $includeUpdateVolumeDiscountPrograms)
}
}
}
@@ -231,6 +225,7 @@ ${UpdateVolumeDiscountProgramsFragmentDoc}`;
* includeNewMarketProductFields: // value for 'includeNewMarketProductFields'
* includeUpdateMarketStates: // value for 'includeUpdateMarketStates'
* includeUpdateReferralPrograms: // value for 'includeUpdateReferralPrograms'
* includeUpdateVolumeDiscountPrograms: // value for 'includeUpdateVolumeDiscountPrograms'
* },
* });
*/
@@ -50,6 +50,7 @@ export const ProposalsContainer = () => {
includeNewMarketProductFields: !!FLAGS.PRODUCT_PERPETUALS,
includeUpdateMarketStates: !!FLAGS.UPDATE_MARKET_STATE,
includeUpdateReferralPrograms: !!FLAGS.REFERRALS,
includeUpdateVolumeDiscountPrograms: !!FLAGS.VOLUME_DISCOUNTS,
},
});
@@ -42,6 +42,7 @@ export const RejectedProposalsContainer = () => {
includeNewMarketProductFields: !!FLAGS.PRODUCT_PERPETUALS,
includeUpdateMarketStates: !!FLAGS.UPDATE_MARKET_STATE,
includeUpdateReferralPrograms: !!FLAGS.REFERRALS,
includeUpdateVolumeDiscountPrograms: !!FLAGS.VOLUME_DISCOUNTS,
},
});
@@ -2,6 +2,8 @@ import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { proposalListQuery, marketUpdateProposal } from '@vegaprotocol/mock';
import * as Schema from '@vegaprotocol/types';
const marketSummaryBlock = 'header-summary';
describe('Market proposal notification', { tags: '@smoke' }, () => {
before(() => {
cy.setVegaWallet();
@@ -23,13 +25,12 @@ describe('Market proposal notification', { tags: '@smoke' }, () => {
});
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.getByTestId('Info').click();
cy.wait('@MarketData');
cy.getByTestId('sidebar-content').should('be.visible');
cy.getByTestId(marketSummaryBlock).should('be.visible');
});
it('should display market proposal notification if proposal found', () => {
cy.getByTestId('sidebar-content').within(() => {
cy.getByTestId(marketSummaryBlock).within(() => {
cy.getByTestId('market-proposal-notification').should(
'contain.text',
'Changes have been proposed for this market'
@@ -1,6 +1,7 @@
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { DocsLinks, useEnvironment } from '@vegaprotocol/environment';
import { ButtonLink, ExternalLink, Link } from '@vegaprotocol/ui-toolkit';
import { MarketProposalNotification } from '@vegaprotocol/proposals';
import type { Market } from '@vegaprotocol/markets';
import {
addDecimalsFormatNumber,
@@ -143,6 +144,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
/>
</HeaderStat>
)}
<MarketProposalNotification marketId={market.id} />
</>
);
};
@@ -1,5 +1,7 @@
import { Route, Routes } from 'react-router-dom';
import { Route, Routes, useParams } from 'react-router-dom';
import { MarketState } from '@vegaprotocol/types';
import { t } from '@vegaprotocol/i18n';
import { useMarket } from '@vegaprotocol/markets';
import { VegaIconNames } from '@vegaprotocol/ui-toolkit';
import {
SidebarButton,
@@ -9,7 +11,12 @@ import {
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
export const MarketsSidebar = () => {
const { marketId } = useParams();
const currentRouteId = useGetCurrentRouteId();
const { data } = useMarket(marketId);
const active =
data &&
[MarketState.STATE_ACTIVE, MarketState.STATE_PENDING].includes(data.state);
return (
<>
@@ -37,12 +44,14 @@ export const MarketsSidebar = () => {
element={
<>
<SidebarDivider />
<SidebarButton
view={ViewType.Order}
icon={VegaIconNames.TICKET}
tooltip={t('Order')}
routeId={currentRouteId}
/>
{active && (
<SidebarButton
view={ViewType.Order}
icon={VegaIconNames.TICKET}
tooltip={t('Order')}
routeId={currentRouteId}
/>
)}
<SidebarButton
view={ViewType.Info}
icon={VegaIconNames.BREAKDOWN}
@@ -75,20 +75,22 @@ export const useReferralProgram = () => {
const benefitTiers = sortBy(data.currentReferralProgram.benefitTiers, (t) =>
Number(t.referralRewardFactor)
).map((t, i) => {
return {
tier: i + 1, // sorted in asc order, hence first is the lowest tier
rewardFactor: Number(t.referralRewardFactor),
commission: Number(t.referralRewardFactor) * 100 + '%',
discountFactor: Number(t.referralDiscountFactor),
discount: Number(t.referralDiscountFactor) * 100 + '%',
minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
volume: getNumberFormat(0).format(
Number(t.minimumRunningNotionalTakerVolume)
),
epochs: Number(t.minimumEpochs),
};
});
)
.reverse()
.map((t, i) => {
return {
tier: i + 1,
rewardFactor: Number(t.referralRewardFactor),
commission: Number(t.referralRewardFactor) * 100 + '%',
discountFactor: Number(t.referralDiscountFactor),
discount: Number(t.referralDiscountFactor) * 100 + '%',
minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
volume: getNumberFormat(0).format(
Number(t.minimumRunningNotionalTakerVolume)
),
epochs: Number(t.minimumEpochs),
};
});
const stakingTiers = sortBy(
data.currentReferralProgram.stakingTiers,
@@ -29,8 +29,8 @@ import { useLayoutEffect, useRef, useState } from 'react';
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';
import minBy from 'lodash/minBy';
export const ReferralStatistics = () => {
const { pubKey } = useVegaWallet();
@@ -123,8 +123,8 @@ export const Statistics = ({
t.discountFactor === discountFactorValue
);
const nextBenefitTierValue = currentBenefitTierValue
? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier + 1)
: minBy(benefitTiers, (bt) => bt.tier); // min tier number is lowest tier
? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier - 1)
: maxBy(benefitTiers, (bt) => bt.tier); // max tier number is lowest tier
const epochsValue =
!isNaN(currentEpoch) && refereeInfo?.atEpoch
? currentEpoch - refereeInfo?.atEpoch
@@ -199,13 +199,13 @@ const TiersTable = ({
...d,
className: classNames({
'from-vega-pink-400 dark:from-vega-pink-600 to-20% bg-highlight':
d.tier >= 3,
d.tier === 1,
'from-vega-purple-400 dark:from-vega-purple-600 to-20% bg-highlight':
d.tier === 2,
'from-vega-blue-400 dark:from-vega-blue-600 to-20% bg-highlight':
d.tier === 1,
d.tier === 3,
'from-vega-orange-400 dark:from-vega-orange-600 to-20% bg-highlight':
d.tier == 0,
d.tier > 3,
}),
}))}
/>
@@ -8,7 +8,7 @@ jest.mock('@vegaprotocol/accounts', () => ({
),
}));
jest.mock('../../components/welcome-dialog/get-started', () => ({
jest.mock('../../components/welcome-dialog/get-started.ts', () => ({
GetStarted: () => <div>GetStarted</div>,
}));
@@ -8,7 +8,7 @@ jest.mock('../../components/withdraw-container', () => ({
),
}));
jest.mock('../../components/welcome-dialog/get-started', () => ({
jest.mock('../../components/welcome-dialog/get-started.ts', () => ({
GetStarted: () => <div>GetStarted</div>,
}));
@@ -16,11 +16,18 @@ query DiscountPrograms {
}
}
query Fees($partyId: ID!) {
query Fees(
$partyId: ID!
$volumeDiscountEpochs: Int!
$referralDiscountEpochs: Int!
) {
epoch {
id
}
volumeDiscountStats(partyId: $partyId, pagination: { last: 1 }) {
volumeDiscountStats(
partyId: $partyId
pagination: { last: $volumeDiscountEpochs }
) {
edges {
node {
atEpoch
@@ -29,22 +36,6 @@ query Fees($partyId: ID!) {
}
}
}
referrer: referralSets(referrer: $partyId) {
edges {
node {
id
referrer
}
}
}
referee: referralSets(referee: $partyId) {
edges {
node {
id
referrer
}
}
}
referralSetReferees(referee: $partyId) {
edges {
node {
@@ -52,7 +43,10 @@ query Fees($partyId: ID!) {
}
}
}
referralSetStats(partyId: $partyId, pagination: { last: 1 }) {
referralSetStats(
partyId: $partyId
pagination: { last: $referralDiscountEpochs }
) {
edges {
node {
atEpoch
+11 -20
View File
@@ -10,10 +10,12 @@ export type DiscountProgramsQuery = { __typename?: 'Query', currentReferralProgr
export type FeesQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
volumeDiscountEpochs: Types.Scalars['Int'];
referralDiscountEpochs: Types.Scalars['Int'];
}>;
export type FeesQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string }, volumeDiscountStats: { __typename?: 'VolumeDiscountStatsConnection', edges: Array<{ __typename?: 'VolumeDiscountStatsEdge', node: { __typename?: 'VolumeDiscountStats', atEpoch: number, discountFactor: string, runningVolume: string } } | null> }, referrer: { __typename?: 'ReferralSetConnection', edges: Array<{ __typename?: 'ReferralSetEdge', node: { __typename?: 'ReferralSet', id: string, referrer: string } } | null> }, referee: { __typename?: 'ReferralSetConnection', edges: Array<{ __typename?: 'ReferralSetEdge', node: { __typename?: 'ReferralSet', id: string, referrer: string } } | null> }, referralSetReferees: { __typename?: 'ReferralSetRefereeConnection', edges: Array<{ __typename?: 'ReferralSetRefereeEdge', node: { __typename?: 'ReferralSetReferee', atEpoch: number } } | null> }, referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, discountFactor: string, referralSetRunningNotionalTakerVolume: string } } | null> } };
export type FeesQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string }, volumeDiscountStats: { __typename?: 'VolumeDiscountStatsConnection', edges: Array<{ __typename?: 'VolumeDiscountStatsEdge', node: { __typename?: 'VolumeDiscountStats', atEpoch: number, discountFactor: string, runningVolume: string } } | null> }, referralSetReferees: { __typename?: 'ReferralSetRefereeConnection', edges: Array<{ __typename?: 'ReferralSetRefereeEdge', node: { __typename?: 'ReferralSetReferee', atEpoch: number } } | null> }, referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, discountFactor: string, referralSetRunningNotionalTakerVolume: string } } | null> } };
export const DiscountProgramsDocument = gql`
@@ -63,11 +65,14 @@ export type DiscountProgramsQueryHookResult = ReturnType<typeof useDiscountProgr
export type DiscountProgramsLazyQueryHookResult = ReturnType<typeof useDiscountProgramsLazyQuery>;
export type DiscountProgramsQueryResult = Apollo.QueryResult<DiscountProgramsQuery, DiscountProgramsQueryVariables>;
export const FeesDocument = gql`
query Fees($partyId: ID!) {
query Fees($partyId: ID!, $volumeDiscountEpochs: Int!, $referralDiscountEpochs: Int!) {
epoch {
id
}
volumeDiscountStats(partyId: $partyId, pagination: {last: 1}) {
volumeDiscountStats(
partyId: $partyId
pagination: {last: $volumeDiscountEpochs}
) {
edges {
node {
atEpoch
@@ -76,22 +81,6 @@ export const FeesDocument = gql`
}
}
}
referrer: referralSets(referrer: $partyId) {
edges {
node {
id
referrer
}
}
}
referee: referralSets(referee: $partyId) {
edges {
node {
id
referrer
}
}
}
referralSetReferees(referee: $partyId) {
edges {
node {
@@ -99,7 +88,7 @@ export const FeesDocument = gql`
}
}
}
referralSetStats(partyId: $partyId, pagination: {last: 1}) {
referralSetStats(partyId: $partyId, pagination: {last: $referralDiscountEpochs}) {
edges {
node {
atEpoch
@@ -124,6 +113,8 @@ export const FeesDocument = gql`
* const { data, loading, error } = useFeesQuery({
* variables: {
* partyId: // value for 'partyId'
* volumeDiscountEpochs: // value for 'volumeDiscountEpochs'
* referralDiscountEpochs: // value for 'referralDiscountEpochs'
* },
* });
*/
@@ -17,14 +17,6 @@ import { useReferralStats } from './use-referral-stats';
import { formatPercentage, getAdjustedFee } from './utils';
import { Table, Td, Th, THead, Tr } from './table';
import BigNumber from 'bignumber.js';
import { Links } from '../../lib/links';
import { Link } from 'react-router-dom';
import {
Tooltip,
VegaIcon,
VegaIconNames,
truncateMiddle,
} from '@vegaprotocol/ui-toolkit';
export const FeesContainer = () => {
const { pubKey } = useVegaWallet();
@@ -36,25 +28,25 @@ export const FeesContainer = () => {
const { data: markets, loading: marketsLoading } = useMarketList();
const { data: programData, loading: programLoading } =
useDiscountProgramsQuery({ errorPolicy: 'ignore' });
useDiscountProgramsQuery();
const volumeDiscountWindowLength =
programData?.currentVolumeDiscountProgram?.windowLength || 1;
const referralDiscountWindowLength =
programData?.currentReferralProgram?.windowLength || 1;
const { data: feesData, loading: feesLoading } = useFeesQuery({
variables: {
partyId: pubKey || '',
volumeDiscountEpochs: volumeDiscountWindowLength,
referralDiscountEpochs: referralDiscountWindowLength,
},
skip: !pubKey,
skip: !pubKey || !programData,
});
const previousEpoch = (Number(feesData?.epoch.id) || 0) - 1;
const { volumeDiscount, volumeTierIndex, volumeInWindow, volumeTiers } =
useVolumeStats(
previousEpoch,
feesData?.volumeDiscountStats.edges?.[0]?.node,
feesData?.volumeDiscountStats,
programData?.currentVolumeDiscountProgram
);
@@ -64,25 +56,16 @@ export const FeesContainer = () => {
referralTierIndex,
referralTiers,
epochsInSet,
code,
isReferrer,
} = useReferralStats(
previousEpoch,
feesData?.referralSetStats.edges?.[0]?.node,
feesData?.referralSetReferees.edges?.[0]?.node,
feesData?.referralSetStats,
feesData?.referralSetReferees,
programData?.currentReferralProgram,
feesData?.referrer.edges?.[0]?.node,
feesData?.referee.edges?.[0]?.node
feesData?.epoch
);
const loading = paramsLoading || feesLoading || programLoading;
const isConnected = Boolean(pubKey);
const isReferralProgramRunning = Boolean(programData?.currentReferralProgram);
const isVolumeDiscountProgramRunning = Boolean(
programData?.currentVolumeDiscountProgram
);
return (
<div className="grid auto-rows-min grid-cols-4 gap-3">
{isConnected && (
@@ -107,8 +90,6 @@ export const FeesContainer = () => {
<TotalDiscount
referralDiscount={referralDiscount}
volumeDiscount={volumeDiscount}
isReferralProgramRunning={isReferralProgramRunning}
isVolumeDiscountProgramRunning={isVolumeDiscountProgramRunning}
/>
</FeeCard>
<FeeCard
@@ -116,37 +97,23 @@ export const FeesContainer = () => {
className="sm:col-span-2"
loading={loading}
>
{isVolumeDiscountProgramRunning ? (
<CurrentVolume
tiers={volumeTiers}
tierIndex={volumeTierIndex}
windowLengthVolume={volumeInWindow}
windowLength={volumeDiscountWindowLength}
/>
) : (
<p className="pt-3 text-sm text-muted">
{t('No volume discount program active')}
</p>
)}
<CurrentVolume
tiers={volumeTiers}
tierIndex={volumeTierIndex}
windowLengthVolume={volumeInWindow}
windowLength={volumeDiscountWindowLength}
/>
</FeeCard>
<FeeCard
title={t('Referral benefits')}
className="sm:col-span-2"
loading={loading}
>
{isReferrer ? (
<ReferrerInfo code={code} />
) : isReferralProgramRunning ? (
<ReferralBenefits
setRunningNotionalTakerVolume={referralVolumeInWindow}
epochsInSet={epochsInSet}
epochs={referralDiscountWindowLength}
/>
) : (
<p className="pt-3 text-sm text-muted">
{t('No referral program active')}
</p>
)}
<ReferralBenefits
setRunningNotionalTakerVolume={referralVolumeInWindow}
epochsInSet={epochsInSet}
epochs={referralDiscountWindowLength}
/>
</FeeCard>
</>
)}
@@ -175,7 +142,7 @@ export const FeesContainer = () => {
/>
</FeeCard>
<FeeCard
title={t('Fees by market')}
title={t('Liquidity fees')}
className="lg:col-span-full"
loading={marketsLoading}
>
@@ -358,64 +325,26 @@ const ReferralBenefits = ({
const TotalDiscount = ({
referralDiscount,
volumeDiscount,
isReferralProgramRunning,
isVolumeDiscountProgramRunning,
}: {
referralDiscount: number;
volumeDiscount: number;
isReferralProgramRunning: boolean;
isVolumeDiscountProgramRunning: boolean;
}) => {
const totalDiscount = 1 - (1 - volumeDiscount) * (1 - referralDiscount);
const totalDiscountDescription = t(
'The total discount is calculated according to the following formula: '
);
const formula = (
<span className="italic">
1 - (1 - d<sub>volume</sub>) (1 - d<sub>referral</sub>)
</span>
);
return (
<div>
<Stat
description={
<>
{totalDiscountDescription}
{formula}
</>
}
value={formatPercentage(totalDiscount) + '%'}
value={formatPercentage(referralDiscount + volumeDiscount) + '%'}
highlight={true}
/>
<table className="w-full mt-0.5 text-xs text-muted">
<tbody>
<tr>
<th className="font-normal text-left">{t('Volume discount')}</th>
<td className="text-right">
{formatPercentage(volumeDiscount)}%
{!isVolumeDiscountProgramRunning && (
<Tooltip description={t('No active volume discount programme')}>
<span className="cursor-help">
{' '}
<VegaIcon name={VegaIconNames.INFO} size={12} />
</span>
</Tooltip>
)}
</td>
<td className="text-right">{formatPercentage(volumeDiscount)}%</td>
</tr>
<tr>
<th className="font-normal text-left ">{t('Referral discount')}</th>
<td className="text-right">
{formatPercentage(referralDiscount)}%
{!isReferralProgramRunning && (
<Tooltip description={t('No active referral programme')}>
<span className="cursor-help">
{' '}
<VegaIcon name={VegaIconNames.INFO} size={12} />
</span>
</Tooltip>
)}
</td>
</tr>
</tbody>
@@ -459,19 +388,25 @@ const VolumeTiers = ({
</tr>
</THead>
<tbody>
{Array.from(tiers).map((tier, i) => {
const isUserTier = tierIndex === i;
{Array.from(tiers)
.reverse()
.map((tier, i) => {
const isUserTier = tiers.length - 1 - tierIndex === i;
return (
<Tr key={i}>
<Td>{i + 1}</Td>
<Td>{formatPercentage(Number(tier.volumeDiscountFactor))}%</Td>
<Td>{formatNumber(tier.minimumRunningNotionalTakerVolume)}</Td>
<Td>{isUserTier ? formatNumber(lastEpochVolume) : ''}</Td>
<Td>{isUserTier ? <YourTier /> : null}</Td>
</Tr>
);
})}
return (
<Tr key={i}>
<Td>{i + 1}</Td>
<Td>
{formatPercentage(Number(tier.volumeDiscountFactor))}%
</Td>
<Td>
{formatNumber(tier.minimumRunningNotionalTakerVolume)}
</Td>
<Td>{isUserTier ? formatNumber(lastEpochVolume) : ''}</Td>
<Td>{isUserTier ? <YourTier /> : null}</Td>
</Tr>
);
})}
</tbody>
</Table>
</div>
@@ -512,33 +447,37 @@ const ReferralTiers = ({
</tr>
</THead>
<tbody>
{Array.from(tiers).map((t, i) => {
const isUserTier = tierIndex === i;
{Array.from(tiers)
.reverse()
.map((t, i) => {
const isUserTier = tiers.length - 1 - tierIndex === i;
const requiredVolume = Number(t.minimumRunningNotionalTakerVolume);
let unlocksIn = null;
if (
referralVolumeInWindow >= requiredVolume &&
epochsInSet < t.minimumEpochs
) {
unlocksIn = (
<span className="text-muted">
Unlocks in {t.minimumEpochs - epochsInSet} epochs
</span>
const requiredVolume = Number(
t.minimumRunningNotionalTakerVolume
);
}
let unlocksIn = null;
return (
<Tr key={i}>
<Td>{i + 1}</Td>
<Td>{formatPercentage(Number(t.referralDiscountFactor))}%</Td>
<Td>{formatNumber(t.minimumRunningNotionalTakerVolume)}</Td>
<Td>{t.minimumEpochs}</Td>
<Td>{isUserTier ? <YourTier /> : unlocksIn}</Td>
</Tr>
);
})}
if (
referralVolumeInWindow >= requiredVolume &&
epochsInSet < t.minimumEpochs
) {
unlocksIn = (
<span className="text-muted">
Unlocks in {t.minimumEpochs - epochsInSet} epochs
</span>
);
}
return (
<Tr key={i}>
<Td>{i + 1}</Td>
<Td>{formatPercentage(Number(t.referralDiscountFactor))}%</Td>
<Td>{formatNumber(t.minimumRunningNotionalTakerVolume)}</Td>
<Td>{t.minimumEpochs}</Td>
<Td>{isUserTier ? <YourTier /> : unlocksIn}</Td>
</Tr>
);
})}
</tbody>
</Table>
</div>
@@ -552,31 +491,3 @@ const YourTier = () => {
</span>
);
};
const ReferrerInfo = ({ code }: { code?: string }) => (
<div className="pt-3 text-sm text-vega-clight-200 dark:vega-cdark-200">
<p className="mb-1">
{t('Connected key is owner of the referral set')}
{code && (
<>
{' '}
<span className="text-transparent bg-rainbow bg-clip-text">
{truncateMiddle(code)}
</span>
</>
)}
{'. '}
{t('As owner, it is eligible for commission not fee discounts.')}
</p>
<p>
{t('See')}{' '}
<Link
className="underline text-black dark:text-white"
to={Links.REFERRALS()}
>
{t('Referrals')}
</Link>{' '}
{t('for more information.')}
</p>
</div>
);
@@ -1,31 +1,23 @@
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import type { ReactNode } from 'react';
export const Stat = ({
value,
text,
highlight,
description,
}: {
value: string | number;
text?: string;
highlight?: boolean;
description?: ReactNode;
}) => {
const val = (
<span
className={classNames('inline-block text-3xl leading-none', {
'text-transparent bg-rainbow bg-clip-text': highlight,
'cursor-help': description,
})}
>
{value}
</span>
);
return (
<p className="pt-3 leading-none first:pt-6">
{description ? <Tooltip description={description}>{val}</Tooltip> : val}
<span
className={classNames('inline-block text-3xl leading-none', {
'text-transparent bg-rainbow bg-clip-text': highlight,
})}
>
{value}
</span>
{text && (
<small className="block mt-0.5 text-xs text-muted">{text}</small>
)}
@@ -2,15 +2,46 @@ import { renderHook } from '@testing-library/react';
import { useReferralStats } from './use-referral-stats';
describe('useReferralStats', () => {
const stat = {
__typename: 'ReferralSetStats' as const,
atEpoch: 9,
discountFactor: '0.01',
referralSetRunningNotionalTakerVolume: '100',
const setStats = {
edges: [
{
__typename: 'ReferralSetStatsEdge' as const,
node: {
__typename: 'ReferralSetStats' as const,
atEpoch: 9,
discountFactor: '0.2',
referralSetRunningNotionalTakerVolume: '100',
},
},
{
__typename: 'ReferralSetStatsEdge' as const,
node: {
__typename: 'ReferralSetStats' as const,
atEpoch: 10,
discountFactor: '0.3',
referralSetRunningNotionalTakerVolume: '200',
},
},
],
};
const set = {
atEpoch: 4,
const sets = {
edges: [
{
node: {
atEpoch: 3,
},
},
{
node: {
atEpoch: 4,
},
},
],
};
const epoch = {
id: '10',
};
const program = {
@@ -42,41 +73,103 @@ describe('useReferralStats', () => {
referralTierIndex: -1,
referralTiers: [],
epochsInSet: 0,
code: undefined,
isReferrer: false,
});
});
it('returns default values if set is not from previous epoch', () => {
const { result } = renderHook(() =>
useReferralStats(10, stat, set, program)
);
expect(result.current).toEqual({
referralDiscount: 0,
referralVolumeInWindow: 0,
referralTierIndex: -1,
referralTiers: program.benefitTiers,
epochsInSet: 0,
code: undefined,
isReferrer: false,
});
});
it('returns formatted data and tiers', () => {
const { result } = renderHook(() =>
useReferralStats(9, stat, set, program)
useReferralStats(setStats, sets, program, epoch)
);
// should use stats from latest epoch
const stats = setStats.edges[1].node;
const set = sets.edges[1].node;
expect(result.current).toEqual({
referralDiscount: Number(stat.discountFactor),
referralDiscount: Number(stats.discountFactor),
referralVolumeInWindow: Number(
stat.referralSetRunningNotionalTakerVolume
stats.referralSetRunningNotionalTakerVolume
),
referralTierIndex: 0,
referralTierIndex: 1,
referralTiers: program.benefitTiers,
epochsInSet: stat.atEpoch - set.atEpoch,
code: undefined,
isReferrer: false,
epochsInSet: Number(epoch.id) - set.atEpoch,
});
});
it.each([
{ joinedAt: 2, index: -1 },
{ joinedAt: 3, index: -1 },
{ joinedAt: 4, index: 0 },
{ joinedAt: 5, index: 0 },
{ joinedAt: 6, index: 1 },
{ joinedAt: 7, index: 1 },
{ joinedAt: 8, index: 2 },
{ joinedAt: 9, index: 2 },
])('joined at epoch: $joinedAt should be index: $index', (obj) => {
const statsA = {
edges: [
{
__typename: 'ReferralSetStatsEdge' as const,
node: {
__typename: 'ReferralSetStats' as const,
atEpoch: 10,
discountFactor: '0.3',
referralSetRunningNotionalTakerVolume: '100000',
},
},
],
};
const setsA = {
edges: [
{
node: {
atEpoch: Number(epoch.id) - obj.joinedAt,
},
},
],
};
const { result } = renderHook(() =>
useReferralStats(statsA, setsA, program, epoch)
);
expect(result.current.referralTierIndex).toEqual(obj.index);
});
it.each([
{ volume: '50', index: -1 },
{ volume: '100', index: 0 },
{ volume: '150', index: 0 },
{ volume: '200', index: 1 },
{ volume: '250', index: 1 },
{ volume: '300', index: 2 },
{ volume: '999', index: 2 },
])('volume: $volume should be index: $index', (obj) => {
const statsA = {
edges: [
{
__typename: 'ReferralSetStatsEdge' as const,
node: {
__typename: 'ReferralSetStats' as const,
atEpoch: 10,
discountFactor: '0.3',
referralSetRunningNotionalTakerVolume: obj.volume,
},
},
],
};
const setsA = {
edges: [
{
node: {
atEpoch: 1,
},
},
],
};
const { result } = renderHook(() =>
useReferralStats(statsA, setsA, program, epoch)
);
expect(result.current.referralTierIndex).toEqual(obj.index);
});
});
@@ -1,51 +1,52 @@
import compact from 'lodash/compact';
import maxBy from 'lodash/maxBy';
import { getReferralBenefitTier } from './utils';
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
export const useReferralStats = (
previousEpoch?: number,
referralStats?: NonNullable<
FeesQuery['referralSetStats']['edges']['0']
>['node'],
setReferees?: NonNullable<
FeesQuery['referralSetReferees']['edges']['0']
>['node'],
setStats?: FeesQuery['referralSetStats'],
setReferees?: FeesQuery['referralSetReferees'],
program?: DiscountProgramsQuery['currentReferralProgram'],
setIfReferrer?: NonNullable<FeesQuery['referrer']['edges']['0']>['node'],
setIfReferee?: NonNullable<FeesQuery['referee']['edges']['0']>['node']
epoch?: FeesQuery['epoch']
) => {
const referralTiers = program?.benefitTiers || [];
if (
!previousEpoch ||
referralStats?.atEpoch !== previousEpoch ||
!program ||
!setReferees
) {
if (!setStats || !setReferees || !program || !epoch) {
return {
referralDiscount: 0,
referralVolumeInWindow: 0,
referralTierIndex: -1,
referralTiers,
epochsInSet: 0,
code: undefined,
isReferrer: false,
};
}
const referralSetsStats = compact(setStats.edges).map((e) => e.node);
const referralSets = compact(setReferees.edges).map((e) => e.node);
const referralSet = maxBy(referralSets, (s) => s.atEpoch);
const referralStats = maxBy(referralSetsStats, (s) => s.atEpoch);
const epochsInSet = referralSet ? Number(epoch.id) - referralSet.atEpoch : 0;
const referralDiscount = Number(referralStats?.discountFactor || 0);
const referralVolumeInWindow = Number(
referralStats?.referralSetRunningNotionalTakerVolume || 0
);
const referralTierIndex = referralTiers.findIndex(
(tier) => tier.referralDiscountFactor === referralStats?.discountFactor
);
const referralTierIndex = referralStats
? getReferralBenefitTier(
epochsInSet,
Number(referralStats.referralSetRunningNotionalTakerVolume),
referralTiers
)
: -1;
return {
referralDiscount,
referralVolumeInWindow,
referralTierIndex,
referralTiers,
epochsInSet: referralStats.atEpoch - setReferees.atEpoch,
code: (setIfReferrer || setIfReferee)?.id,
isReferrer: Boolean(setIfReferrer),
epochsInSet,
};
};
@@ -2,11 +2,27 @@ import { renderHook } from '@testing-library/react';
import { useVolumeStats } from './use-volume-stats';
describe('useReferralStats', () => {
const stats = {
__typename: 'VolumeDiscountStats' as const,
atEpoch: 10,
discountFactor: '0.05',
runningVolume: '200',
const statsList = {
edges: [
{
__typename: 'VolumeDiscountStatsEdge' as const,
node: {
__typename: 'VolumeDiscountStats' as const,
atEpoch: 9,
discountFactor: '0.1',
runningVolume: '100',
},
},
{
__typename: 'VolumeDiscountStatsEdge' as const,
node: {
__typename: 'VolumeDiscountStats' as const,
atEpoch: 10,
discountFactor: '0.3',
runningVolume: '200',
},
},
],
};
const program = {
@@ -28,7 +44,7 @@ describe('useReferralStats', () => {
};
it('returns correct default values', () => {
const { result } = renderHook(() => useVolumeStats(10));
const { result } = renderHook(() => useVolumeStats());
expect(result.current).toEqual({
volumeDiscount: 0,
volumeInWindow: 0,
@@ -37,18 +53,11 @@ describe('useReferralStats', () => {
});
});
it('returns default values if no stat is not from previous epoch', () => {
const { result } = renderHook(() => useVolumeStats(11, stats, program));
expect(result.current).toEqual({
volumeDiscount: 0,
volumeInWindow: 0,
volumeTierIndex: -1,
volumeTiers: program.benefitTiers,
});
});
it('returns formatted data and tiers', () => {
const { result } = renderHook(() => useVolumeStats(10, stats, program));
const { result } = renderHook(() => useVolumeStats(statsList, program));
// should use stats from latest epoch
const stats = statsList.edges[1].node;
expect(result.current).toEqual({
volumeDiscount: Number(stats.discountFactor),
@@ -57,4 +66,30 @@ describe('useReferralStats', () => {
volumeTiers: program.benefitTiers,
});
});
it.each([
{ volume: '100', index: 0 },
{ volume: '150', index: 0 },
{ volume: '200', index: 1 },
{ volume: '250', index: 1 },
{ volume: '300', index: 2 },
{ volume: '350', index: 2 },
])('returns index: $index for the running volume: $volume', (obj) => {
const statsA = {
edges: [
{
__typename: 'VolumeDiscountStatsEdge' as const,
node: {
__typename: 'VolumeDiscountStats' as const,
atEpoch: 10,
discountFactor: '0.3',
runningVolume: obj.volume,
},
},
],
};
const { result } = renderHook(() => useVolumeStats(statsA, program));
expect(result.current.volumeTierIndex).toBe(obj.index);
});
});
@@ -1,15 +1,15 @@
import compact from 'lodash/compact';
import maxBy from 'lodash/maxBy';
import { getVolumeTier } from './utils';
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
export const useVolumeStats = (
previousEpoch: number,
lastEpochStats?: NonNullable<
FeesQuery['volumeDiscountStats']['edges']['0']
>['node'],
stats?: FeesQuery['volumeDiscountStats'],
program?: DiscountProgramsQuery['currentVolumeDiscountProgram']
) => {
const volumeTiers = program?.benefitTiers || [];
if (!lastEpochStats || lastEpochStats.atEpoch !== previousEpoch || !program) {
if (!stats || !program) {
return {
volumeDiscount: 0,
volumeTierIndex: -1,
@@ -18,11 +18,11 @@ export const useVolumeStats = (
};
}
const volumeStats = compact(stats.edges).map((e) => e.node);
const lastEpochStats = maxBy(volumeStats, (s) => s.atEpoch);
const volumeDiscount = Number(lastEpochStats?.discountFactor || 0);
const volumeInWindow = Number(lastEpochStats?.runningVolume || 0);
const volumeTierIndex = volumeTiers.findIndex(
(tier) => tier.volumeDiscountFactor === lastEpochStats?.discountFactor
);
const volumeTierIndex = getVolumeTier(volumeInWindow, volumeTiers);
return {
volumeDiscount,
@@ -21,7 +21,7 @@ describe('getAdjustedFee', () => {
new BigNumber(referralDiscount),
];
// 1 - 0.5 = 0.5
// 1 - 0.5 - 0.5
const v = new BigNumber(1).minus(new BigNumber(volumeDiscount));
// 1 - 0.5 = 0.5
@@ -34,15 +34,13 @@ describe('getAdjustedFee', () => {
// 0.1 + 0.1 + 0.1 = 0.3
const totalFees = fees.reduce((sum, x) => sum.plus(x), new BigNumber(0));
// (1 - 0.3) * 0.75 = 0.525
const expected = new BigNumber(totalFees)
.times(new BigNumber(1).minus(factor))
.toNumber();
// 0.3 * 0.75 = 0.225
const expected = new BigNumber(totalFees).times(factor).toNumber();
expect(getAdjustedFee(fees, discounts)).toBe(expected);
});
it('combines discount factors multiplicatively', () => {
it('combines discount factors multiplicativly', () => {
const volumeDiscount = 0.4;
const referralDiscount = 0.1;
@@ -69,9 +67,7 @@ describe('getAdjustedFee', () => {
// summed fees
const totalFees = fees.reduce((sum, x) => sum.plus(x), new BigNumber(0));
const expected = new BigNumber(totalFees)
.times(new BigNumber(1).minus(factor))
.toNumber();
const expected = new BigNumber(totalFees).times(factor).toNumber();
expect(getAdjustedFee(fees, discounts)).toBe(expected);
});
@@ -12,14 +12,79 @@ export const formatPercentage = (num: number) => {
const pct = new BigNumber(num).times(100);
const dps = pct.decimalPlaces();
const formatter = new Intl.NumberFormat(getUserLocale(), {
// set to 0 in order to remove the "trailing zeroes" for numbers such as:
// 0.123456789 -non-zero-min-> 12.3456800% -zero-min-> 12.34568%
minimumFractionDigits: 0,
minimumFractionDigits: dps || 0,
maximumFractionDigits: dps || 0,
});
return formatter.format(parseFloat(pct.toFixed(5)));
};
/**
* Return the index of the benefit tier for volume discounts. A user
* only needs to fulfill a minimum volume requirement for the tier
*/
export const getVolumeTier = (
volume: number,
tiers: Array<{
minimumRunningNotionalTakerVolume: string;
}>
) => {
return tiers.findIndex((tier, i) => {
const nextTier = tiers[i + 1];
const validVolume =
volume >= Number(tier.minimumRunningNotionalTakerVolume);
if (nextTier) {
return (
validVolume &&
volume < Number(nextTier.minimumRunningNotionalTakerVolume)
);
}
return validVolume;
});
};
/**
* Return the index of the benefit tiers for referrals. A user must
* fulfill both the minimum epochs in the referral set, and the set
* must reach the combined total volume
*/
export const getReferralBenefitTier = (
epochsInSet: number,
volume: number,
tiers: Array<{
minimumRunningNotionalTakerVolume: string;
minimumEpochs: number;
}>
) => {
const indexByEpoch = tiers.findIndex((tier, i) => {
const nextTier = tiers[i + 1];
const validEpochs = epochsInSet >= tier.minimumEpochs;
if (nextTier) {
return validEpochs && epochsInSet < nextTier.minimumEpochs;
}
return validEpochs;
});
const indexByVolume = tiers.findIndex((tier, i) => {
const nextTier = tiers[i + 1];
const validVolume =
volume >= Number(tier.minimumRunningNotionalTakerVolume);
if (nextTier) {
return (
validVolume &&
volume < Number(nextTier.minimumRunningNotionalTakerVolume)
);
}
return validVolume;
});
return Math.min(indexByEpoch, indexByVolume);
};
/**
* Given a set of fees and a set of discounts return
* the adjusted fee factor
@@ -36,7 +101,5 @@ export const getAdjustedFee = (fees: BigNumber[], discounts: BigNumber[]) => {
const totalFactor = new BigNumber(1).minus(combinedFactors);
return totalFee
.times(new BigNumber(1).minus(BigNumber.max(0, totalFactor)))
.toNumber();
return totalFee.times(BigNumber.max(0, totalFactor)).toNumber();
};
@@ -15,12 +15,15 @@ export const LedgerContainer = () => {
});
const assets = (data?.party?.accountsConnection?.edges ?? [])
.map((item) => item?.node?.asset)
.filter((asset): asset is PartyAssetFieldsFragment => !!asset?.id)
.reduce(
(aggr, item) => Object.assign(aggr, { [item.id]: item.symbol }),
{} as Record<string, string>
);
.map<PartyAssetFieldsFragment>(
(item) => item?.node?.asset ?? ({} as PartyAssetFieldsFragment)
)
.reduce((aggr, item) => {
if ('id' in item && 'symbol' in item) {
aggr[item.id as string] = item.symbol as string;
}
return aggr;
}, {} as Record<string, string>);
if (!pubKey) {
return (
@@ -1,12 +0,0 @@
import { Settings } from './settings';
import { render, screen } from '@testing-library/react';
describe('Settings', () => {
it('should the settings component with all the options', () => {
render(<Settings />);
expect(screen.getByText('Dark mode')).toBeInTheDocument();
expect(screen.getByText('Share usage data')).toBeInTheDocument();
expect(screen.getByText('Toast location')).toBeInTheDocument();
expect(screen.getByText('Reset to default')).toBeInTheDocument();
});
});
+2 -55
View File
@@ -1,19 +1,12 @@
import { t } from '@vegaprotocol/i18n';
import {
Dialog,
Intent,
Switch,
ToastPositionSetter,
TradingButton,
} from '@vegaprotocol/ui-toolkit';
import { Switch, ToastPositionSetter } from '@vegaprotocol/ui-toolkit';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import { useTelemetryApproval } from '../../lib/hooks/use-telemetry-approval';
import { useState, type ReactNode } from 'react';
import type { ReactNode } from 'react';
export const Settings = () => {
const { theme, setTheme } = useThemeSwitcher();
const [isApproved, setIsApproved] = useTelemetryApproval();
const [open, setOpen] = useState(false);
return (
<div>
<SettingsGroup label={t('Dark mode')}>
@@ -38,52 +31,6 @@ export const Settings = () => {
<SettingsGroup label={t('Toast location')}>
<ToastPositionSetter />
</SettingsGroup>
<SettingsGroup label={t('Reset to default')}>
<TradingButton
name="reset-to-defaults"
size="small"
intent={Intent.None}
onClick={() => {
setOpen(true);
}}
>
{t('Reset')}
</TradingButton>
<Dialog open={open} title={t('Reset')}>
<div className="mb-4">
<p>
{t(
'You will lose all persisted settings and you will be logged out.'
)}
</p>
<p>
{t('Are you sure you want to reset all settings to default?')}
</p>
</div>
<div className="flex flex-col gap-4">
<TradingButton
name="reset-to-defaults-cancel"
intent={Intent.Primary}
onClick={() => {
localStorage.clear();
window.location.reload();
}}
>
{t('Yes, clear cache and refresh')}
</TradingButton>
<TradingButton
name="reset-to-defaults-cancel"
intent={Intent.None}
onClick={() => {
setOpen(false);
}}
>
{t('No, keep settings')}
</TradingButton>
</div>
</Dialog>
</SettingsGroup>
</div>
);
};
+3 -3
View File
@@ -170,7 +170,7 @@ html [data-theme='dark'] {
.ag-theme-balham,
.ag-theme-balham-dark {
--ag-grid-size: 3px; /* Used for compactness */
--ag-grid-size: 2px; /* Used for compactness */
--ag-row-height: 36px;
--ag-header-height: 28px;
}
@@ -184,7 +184,7 @@ html [data-theme='dark'] {
/* Light variables */
.ag-theme-balham {
--ag-background-color: theme(colors.vega.clight.900);
--ag-background-color: transparent;
--ag-border-color: theme(colors.vega.clight.600);
--ag-header-background-color: theme(colors.vega.clight.700);
--ag-odd-row-background-color: transparent;
@@ -196,7 +196,7 @@ html [data-theme='dark'] {
/* Dark variables */
.ag-theme-balham-dark {
--ag-background-color: theme(colors.vega.cdark.900);
--ag-background-color: transparent;
--ag-border-color: theme(colors.vega.cdark.600);
--ag-header-background-color: theme(colors.vega.cdark.700);
--ag-odd-row-background-color: transparent;
+2 -2
View File
@@ -1,11 +1,11 @@
#!/bin/bash -e
yarn --pure-lockfile
app=${1:-trading}
envCmd="envCmd="yarn -f ./apps/${app}/.env.${2:-mainnet}"
envCmd="envCmd="yarn env-cmd -f ./apps/${app}/.env.${2:-mainnet}"
yarn install
if [ "${app}" = "trading" ]; then
$envCmd yarn nx export trading
DIST_LOCATION=dist/apps/trading/exported/
DIST_LOCATION=dist/apps/trading/exported
else
$envCmd yarn nx build ${app}
DIST_LOCATION=dist/apps/${app}
+40 -28
View File
@@ -1,23 +1,10 @@
import { act, render, screen, within } from '@testing-library/react';
import { act, render, screen } from '@testing-library/react';
import * as Types from '@vegaprotocol/types';
import type { AccountFields } from './accounts-data-provider';
import { getAccountData } from './accounts-data-provider';
import { AccountTable } from './accounts-table';
import userEvent from '@testing-library/user-event';
const asset1 = {
__typename: 'Asset',
id: 'asset-1',
symbol: 'tBTC',
decimals: 5,
name: 'T BTC',
};
const asset2 = {
__typename: 'Asset',
id: 'asset-2',
symbol: 'aBTC',
decimals: 5,
name: 'A BTC',
};
const singleRow = {
__typename: 'AccountBalance',
type: Types.AccountType.ACCOUNT_TYPE_MARGIN,
@@ -26,7 +13,12 @@ const singleRow = {
__typename: 'Market',
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
},
asset: asset1,
asset: {
__typename: 'Asset',
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
symbol: 'tBTC',
decimals: 5,
},
available: '125600000',
used: '125600000',
total: '251200000',
@@ -41,7 +33,12 @@ const secondRow = {
__typename: 'Market',
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
},
asset: asset2,
asset: {
__typename: 'Asset',
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
symbol: 'aBTC',
decimals: 5,
},
available: '125600001',
used: '125600001',
total: '251200002',
@@ -137,7 +134,7 @@ describe('AccountsTable', () => {
it('should sort assets', async () => {
// 7001-COLL-010
render(
const { container } = render(
<AccountTable
rowData={multiRowData}
onClickAsset={() => null}
@@ -145,13 +142,13 @@ describe('AccountsTable', () => {
/>
);
const headerCell = screen
.getAllByRole('columnheader')
.find((h) => h?.getAttribute('col-id') === 'asset.symbol') as HTMLElement;
await userEvent.click(within(headerCell).getByText(/asset/i));
expect(headerCell).toHaveAttribute('aria-sort', 'ascending');
const headerCell = screen.getByText('Asset');
await userEvent.click(headerCell);
const rows = container.querySelectorAll(
'.ag-center-cols-container .ag-row'
);
expect(rows[0].textContent).toContain('aBTC');
expect(rows[1].textContent).toContain('tBTC');
});
it('should apply correct formatting in view as user mode', async () => {
@@ -179,7 +176,12 @@ describe('AccountsTable', () => {
rowData={singleRowData}
onClickAsset={() => null}
isReadOnly={false}
pinnedAsset={asset1}
pinnedAsset={{
decimals: 5,
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
symbol: 'tBTC',
name: 'tBTC',
}}
/>
);
await screen.findAllByRole('rowgroup');
@@ -211,13 +213,23 @@ describe('AccountsTable', () => {
const result = getAccountData([singleRow]);
const expected = [
{
asset: asset1,
asset: {
__typename: 'Asset',
decimals: 5,
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
symbol: 'tBTC',
},
available: '0',
balance: '0',
breakdown: [
{
__typename: 'AccountBalance',
asset: asset1,
asset: {
__typename: 'Asset',
decimals: 5,
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
symbol: 'tBTC',
},
available: '0',
balance: '125600000',
total: '125600000',
+4 -11
View File
@@ -1,10 +1,9 @@
import sortBy from 'lodash/sortBy';
import * as Schema from '@vegaprotocol/types';
import { truncateByChars } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
NetworkParams,
useNetworkParams,
useNetworkParam,
} from '@vegaprotocol/network-parameters';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type { Transfer } from '@vegaprotocol/wallet';
@@ -22,11 +21,7 @@ export const ALLOWED_ACCOUNTS = [
export const TransferContainer = ({ assetId }: { assetId?: string }) => {
const { pubKey, pubKeys } = useVegaWallet();
const { params } = useNetworkParams([
NetworkParams.transfer_fee_factor,
NetworkParams.transfer_minTransferQuantumMultiple,
]);
const { param } = useNetworkParam(NetworkParams.transfer_fee_factor);
const { data } = useDataProvider({
dataProvider: accountsDataProvider,
variables: { partyId: pubKey || '' },
@@ -45,7 +40,6 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
const accounts = data
? data.filter((account) => ALLOWED_ACCOUNTS.includes(account.type))
: [];
const sortedAccounts = sortBy(accounts, (a) => a.asset.symbol.toLowerCase());
return (
<>
@@ -65,10 +59,9 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
pubKey={pubKey}
pubKeys={pubKeys ? pubKeys?.map((pk) => pk.publicKey) : null}
assetId={assetId}
feeFactor={params.transfer_fee_factor}
minQuantumMultiple={params.transfer_minTransferQuantumMultiple}
feeFactor={param}
submitTransfer={transfer}
accounts={sortedAccounts}
accounts={accounts}
/>
</>
);
+37 -204
View File
@@ -1,10 +1,4 @@
import {
fireEvent,
render,
screen,
waitFor,
within,
} 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 {
@@ -13,12 +7,20 @@ import {
TransferForm,
type TransferFormProps,
} from './transfer-form';
import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
import { AccountType } from '@vegaprotocol/types';
import { removeDecimal } from '@vegaprotocol/utils';
import { MockedProvider } from '@apollo/client/testing';
describe('TransferForm', () => {
const renderComponent = (props: TransferFormProps) => {
return render(<TransferForm {...props} />);
return render(
// Wrap with mock provider as the form will make queries to fetch the selected
// toVegaKey accounts. We don't test this for now but we need to wrap so that
// the component has access to the client
<MockedProvider>
<TransferForm {...props} />
</MockedProvider>
);
};
const submit = async () => {
@@ -45,17 +47,20 @@ describe('TransferForm', () => {
};
const amount = '100';
const pubKey = '1'.repeat(64);
const pubKey =
'70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680';
const asset = {
id: 'eur',
symbol: '€',
name: 'EUR',
decimals: 2,
quantum: '1',
};
const props = {
pubKey,
pubKeys: [pubKey, '2'.repeat(64)],
pubKeys: [
pubKey,
'a4b6e3de5d7ef4e31ae1b090be49d1a2ef7bcefff60cccf7658a0d4922651cce',
],
feeFactor: '0.001',
submitTransfer: jest.fn(),
accounts: [
@@ -67,10 +72,9 @@ describe('TransferForm', () => {
{
type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
asset,
balance: '10000',
balance: '100000',
},
],
minQuantumMultiple: '1',
};
it('form tooltips correctly displayed', async () => {
@@ -128,7 +132,7 @@ describe('TransferForm', () => {
// 1003-TRAN-004
renderComponent(props);
await submit();
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey is set as default value
expect(await screen.findAllByText('Required')).toHaveLength(3); // pubkey is set as default value
const toggle = screen.getByText('Enter manually');
await userEvent.click(toggle);
// has switched to input
@@ -141,12 +145,12 @@ describe('TransferForm', () => {
screen.getByLabelText('To Vega key'),
'invalid-address'
);
expect(screen.getAllByTestId('input-error-text')[1]).toHaveTextContent(
expect(screen.getAllByTestId('input-error-text')[0]).toHaveTextContent(
'Invalid Vega key'
);
});
it('sends transfer from general accounts', async () => {
it('validates fields and submits', async () => {
// 1003-TRAN-002
// 1003-TRAN-003
// 1002-WITH-010
@@ -164,7 +168,7 @@ describe('TransferForm', () => {
]);
await submit();
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey is set as default value
expect(await screen.findAllByText('Required')).toHaveLength(3); // pubkey is set as default value
// Select a pubkey
await userEvent.selectOptions(
@@ -177,20 +181,15 @@ describe('TransferForm', () => {
await userEvent.selectOptions(
screen.getByLabelText('From account'),
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
AccountType.ACCOUNT_TYPE_VESTED_REWARDS
);
const amountInput = screen.getByLabelText('Amount');
// Test use max button
await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
expect(amountInput).toHaveValue('1000.00');
// Test amount validation
await userEvent.clear(amountInput);
await userEvent.type(amountInput, '0.001'); // Below quantum multiple amount
await userEvent.type(amountInput, '0.00000001');
expect(
await screen.findByText(/Amount below minimum requirement/)
await screen.findByText('Value is below minimum')
).toBeInTheDocument();
await userEvent.clear(amountInput);
@@ -211,7 +210,7 @@ describe('TransferForm', () => {
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,
@@ -221,165 +220,6 @@ describe('TransferForm', () => {
});
});
it('sends transfer from vested accounts', async () => {
const mockSubmit = jest.fn();
renderComponent({
...props,
submitTransfer: mockSubmit,
minQuantumMultiple: '100000',
});
// 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
);
await submit();
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value
// Select a pubkey
await userEvent.selectOptions(
screen.getByLabelText('To Vega key'),
props.pubKeys[1] // Use not current pubkey so we can check it switches to current pubkey later
);
// Select asset
await selectAsset(asset);
await userEvent.selectOptions(
screen.getByLabelText('From account'),
`${AccountType.ACCOUNT_TYPE_VESTED_REWARDS}-${asset.id}`
);
// Check switch back to connected key
expect(screen.getByLabelText('To Vega key')).toHaveValue(props.pubKey);
const amountInput = screen.getByLabelText('Amount');
const checkbox = screen.getByTestId('include-transfer-fee');
expect(checkbox).not.toBeChecked();
await userEvent.clear(amountInput);
await userEvent.type(amountInput, '50');
expect(await screen.findByText(/Use max to bypass/)).toBeInTheDocument();
// Test use max button
await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
expect(amountInput).toHaveValue('100.00');
// If transfering from a vested account 'include fees' checkbox should
// be disabled and fees should be 0
expect(checkbox).not.toBeChecked();
expect(checkbox).toBeDisabled();
const expectedFee = '0';
const total = new BigNumber(amount).plus(expectedFee).toFixed();
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(amount);
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total);
await submit();
await waitFor(() => {
// 1003-TRAN-023
expect(mockSubmit).toHaveBeenCalledTimes(1);
expect(mockSubmit).toHaveBeenCalledWith({
fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
to: props.pubKey,
asset: asset.id,
amount: removeDecimal(amount, asset.decimals),
oneOff: {},
});
});
});
it('handles lots of decimal places', async () => {
const balance = '904195168829277777';
const expectedBalance = '0.904195168829277777';
const longDecimalAsset = {
id: 'assetId',
symbol: 'VEGA',
name: 'VEGA',
decimals: 18,
quantum: '1',
};
const account = {
type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
asset: longDecimalAsset,
balance,
};
const mockSubmit = jest.fn();
renderComponent({
...props,
accounts: [account],
submitTransfer: mockSubmit,
minQuantumMultiple: '100000',
});
// Select a pubkey
await userEvent.selectOptions(
screen.getByLabelText('To Vega key'),
props.pubKeys[1] // Use not current pubkey so we can check it switches to current pubkey later
);
// Select asset
await selectAsset(longDecimalAsset);
const accountSelect = screen.getByLabelText('From account');
const option = within(accountSelect)
.getAllByRole('option')
.find(
(o) => o.getAttribute('value') === `${account.type}-${account.asset.id}`
);
// plus one for disabled 'please select' option
expect(option).toHaveTextContent(
`${AccountTypeMapping[account.type]} (${expectedBalance} ${
account.asset.symbol
})`
);
await userEvent.selectOptions(
accountSelect,
`${AccountType.ACCOUNT_TYPE_VESTED_REWARDS}-${longDecimalAsset.id}`
);
expect(accountSelect).toHaveValue(
`${AccountType.ACCOUNT_TYPE_VESTED_REWARDS}-${longDecimalAsset.id}`
);
// Check switch back to connected key
const amountInput = screen.getByLabelText('Amount');
// Test use max button
await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
expect(amountInput).toHaveValue(expectedBalance);
await submit();
await waitFor(() => {
// 1003-TRAN-023
expect(mockSubmit).toHaveBeenCalledTimes(1);
expect(mockSubmit).toHaveBeenCalledWith({
fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
to: props.pubKey,
asset: longDecimalAsset.id,
amount: balance,
oneOff: {},
});
});
});
describe('IncludeFeesCheckbox', () => {
it('validates fields and submits when checkbox is checked', async () => {
const mockSubmit = jest.fn();
@@ -394,7 +234,7 @@ describe('TransferForm', () => {
);
await submit();
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value
expect(await screen.findAllByText('Required')).toHaveLength(3); // pubkey set as default value
// Select a pubkey
await userEvent.selectOptions(
@@ -407,7 +247,7 @@ describe('TransferForm', () => {
await userEvent.selectOptions(
screen.getByLabelText('From account'),
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
AccountType.ACCOUNT_TYPE_VESTED_REWARDS
);
const amountInput = screen.getByLabelText('Amount');
@@ -441,7 +281,7 @@ describe('TransferForm', () => {
// 1003-TRAN-023
expect(mockSubmit).toHaveBeenCalledTimes(1);
expect(mockSubmit).toHaveBeenCalledWith({
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
to: props.pubKeys[1],
asset: asset.id,
@@ -463,7 +303,7 @@ describe('TransferForm', () => {
);
await submit();
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value
expect(await screen.findAllByText('Required')).toHaveLength(3); // pubkey set as default value
// Select a pubkey
await userEvent.selectOptions(
@@ -474,11 +314,6 @@ describe('TransferForm', () => {
// Select asset
await selectAsset(asset);
await userEvent.selectOptions(
screen.getByLabelText('From account'),
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
);
const amountInput = screen.getByLabelText('Amount');
const checkbox = screen.getByTestId('include-transfer-fee');
expect(checkbox).not.toBeChecked();
@@ -498,28 +333,26 @@ describe('TransferForm', () => {
describe('AddressField', () => {
const props = {
mode: 'select' as const,
select: <div>select</div>,
input: <div>input</div>,
onChange: jest.fn(),
};
it('renders correct content by mode prop and calls onChange', async () => {
it('toggles content and calls onChange', async () => {
const mockOnChange = jest.fn();
const { rerender } = render(
<AddressField {...props} onChange={mockOnChange} />
);
render(<AddressField {...props} onChange={mockOnChange} />);
// select should be shown by default
expect(screen.getByText('select')).toBeInTheDocument();
expect(screen.queryByText('input')).not.toBeInTheDocument();
await userEvent.click(screen.getByText('Enter manually'));
expect(mockOnChange).toHaveBeenCalled();
rerender(<AddressField {...props} mode="input" />);
expect(screen.queryByText('select')).not.toBeInTheDocument();
expect(screen.getByText('input')).toBeInTheDocument();
expect(mockOnChange).toHaveBeenCalledTimes(1);
await userEvent.click(screen.getByText('Select from wallet'));
expect(screen.getByText('select')).toBeInTheDocument();
expect(screen.queryByText('input')).not.toBeInTheDocument();
expect(mockOnChange).toHaveBeenCalledTimes(2);
});
});
+176 -222
View File
@@ -1,11 +1,12 @@
import sortBy from 'lodash/sortBy';
import {
minSafe,
maxSafe,
required,
vegaPublicKey,
addDecimal,
formatNumber,
toBigNum,
addDecimalsFormatNumber,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
@@ -26,20 +27,14 @@ 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';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { accountsDataProvider } from './accounts-data-provider';
interface FormFields {
toVegaKey: string;
asset: string; // This is used to simply filter the from account list, the fromAccount type should be used in the tx
asset: string;
amount: string;
fromAccount: string; // AccountType-AssetId
}
interface Asset {
id: string;
symbol: string;
name: string;
decimals: number;
quantum: string;
fromAccount: AccountType;
}
export interface TransferFormProps {
@@ -48,11 +43,10 @@ export interface TransferFormProps {
accounts: Array<{
type: AccountType;
balance: string;
asset: Asset;
asset: { id: string; symbol: string; name: string; decimals: number };
}>;
assetId?: string;
feeFactor: string | null;
minQuantumMultiple: string | null;
submitTransfer: (transfer: Transfer) => void;
}
@@ -63,7 +57,6 @@ export const TransferForm = ({
feeFactor,
submitTransfer,
accounts,
minQuantumMultiple,
}: TransferFormProps) => {
const {
control,
@@ -79,8 +72,6 @@ export const TransferForm = ({
},
});
const [toVegaKeyMode, setToVegaKeyMode] = useState<ToVegaKeyMode>('select');
const assets = sortBy(
accounts
.filter(
@@ -108,29 +99,48 @@ export const TransferForm = ({
...account.asset,
balance: addDecimal(account.balance, account.asset.decimals),
})),
(a) => a.symbol.toLowerCase()
'name'
);
const selectedPubKey = watch('toVegaKey');
const amount = watch('amount');
const fromAccount = watch('fromAccount');
const selectedAssetId = watch('asset');
const assetId = watch('asset');
// Convert the account type (Type-AssetId) into separate values
const [accountType, accountAssetId] = fromAccount
? parseFromAccount(fromAccount)
: [undefined, undefined];
const fromVested = accountType === AccountType.ACCOUNT_TYPE_VESTED_REWARDS;
const asset = assets.find((a) => a.id === accountAssetId);
const asset = assets.find((a) => a.id === assetId);
const { data: toAccounts } = useDataProvider({
dataProvider: accountsDataProvider,
variables: {
partyId: selectedPubKey,
},
skip: !selectedPubKey,
});
const account = accounts.find(
(a) => a.asset.id === accountAssetId && a.type === accountType
(a) => a.asset.id === assetId && a.type === fromAccount
);
const accountBalance =
account && addDecimal(account.balance, account.asset.decimals);
// The general account of the selected pubkey. You can only transfer
// to general accounts, either when redeeming vested rewards or just
// during normal general -> general transfers
const toGeneralAccount =
toAccounts &&
toAccounts.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);
@@ -154,21 +164,16 @@ export const TransferForm = ({
const onSubmit = useCallback(
(fields: FormFields) => {
if (!transferAmount) {
throw new Error('Submitted transfer with no amount selected');
}
const [type, assetId] = parseFromAccount(fields.fromAccount);
const asset = assets.find((a) => a.id === assetId);
if (!asset) {
throw new Error('Submitted transfer with no asset selected');
}
if (!transferAmount) {
throw new Error('Submitted transfer with no amount selected');
}
const transfer = normalizeTransfer(
fields.toVegaKey,
transferAmount,
type,
fields.fromAccount,
AccountType.ACCOUNT_TYPE_GENERAL, // field is readonly in the form
{
id: asset.id,
@@ -177,7 +182,7 @@ export const TransferForm = ({
);
submitTransfer(transfer);
},
[submitTransfer, transferAmount, assets]
[asset, submitTransfer, transferAmount]
);
// reset for placeholder workaround https://github.com/radix-ui/primitives/issues/1569
@@ -193,120 +198,11 @@ export const TransferForm = ({
className="text-sm"
data-testid="transfer-form"
>
<TradingFormGroup label={t('Asset')} labelFor="asset">
<Controller
control={control}
name="asset"
render={({ field }) => (
<TradingRichSelect
data-testid="select-asset"
id={field.name}
name={field.name}
onValueChange={(value) => {
field.onChange(value);
setValue('fromAccount', '');
}}
placeholder={t('Please select an asset')}
value={field.value}
>
{assets.map((a) => (
<AssetOption
key={a.key}
asset={a}
balance={<Balance balance={a.balance} symbol={a.symbol} />}
/>
))}
</TradingRichSelect>
)}
/>
{errors.asset?.message && (
<TradingInputError forInput="asset">
{errors.asset.message}
</TradingInputError>
)}
</TradingFormGroup>
<TradingFormGroup label={t('From account')} labelFor="fromAccount">
<Controller
control={control}
name="fromAccount"
rules={{
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;
},
},
}}
render={({ field }) => (
<TradingSelect
id="fromAccount"
defaultValue=""
{...field}
onChange={(e) => {
field.onChange(e);
const [type] = parseFromAccount(e.target.value);
// Enforce that if transferring from a vested rewards account it must go to
// the current connected general account
if (
type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS &&
pubKey
) {
setValue('toVegaKey', pubKey);
setToVegaKeyMode('select');
setIncludeFee(false);
}
}}
>
<option value="" disabled={true}>
{t('Please select')}
</option>
{accounts
.filter((a) => {
if (!selectedAssetId) return true;
return selectedAssetId === a.asset.id;
})
.map((a) => {
const id = `${a.type}-${a.asset.id}`;
return (
<option value={id} key={id}>
{AccountTypeMapping[a.type]} (
{addDecimal(a.balance, a.asset.decimals)} {a.asset.symbol}
)
</option>
);
})}
</TradingSelect>
)}
/>
{errors.fromAccount?.message && (
<TradingInputError forInput="fromAccount">
{errors.fromAccount.message}
</TradingInputError>
)}
</TradingFormGroup>
<TradingFormGroup label="To Vega key" labelFor="toVegaKey">
<AddressField
onChange={() => {
setValue('toVegaKey', '');
setToVegaKeyMode((curr) => (curr === 'input' ? 'select' : 'input'));
}}
mode={toVegaKeyMode}
onChange={() => setValue('toVegaKey', '')}
select={
<TradingSelect
{...register('toVegaKey')}
disabled={fromVested}
id="toVegaKey"
>
<TradingSelect {...register('toVegaKey')} id="toVegaKey">
<option value="" disabled={true}>
{t('Please select')}
</option>
@@ -322,21 +218,18 @@ export const TransferForm = ({
</TradingSelect>
}
input={
fromVested ? null : (
<TradingInput
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={true} // focus input immediately after is shown
id="toVegaKey"
type="text"
disabled={fromVested}
{...register('toVegaKey', {
validate: {
required,
vegaPublicKey,
},
})}
/>
)
<TradingInput
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={true} // focus input immediately after is shown
id="toVegaKey"
type="text"
{...register('toVegaKey', {
validate: {
required,
vegaPublicKey,
},
})}
/>
}
/>
{errors.toVegaKey?.message && (
@@ -345,6 +238,111 @@ export const TransferForm = ({
</TradingInputError>
)}
</TradingFormGroup>
<TradingFormGroup label={t('Asset')} labelFor="asset">
<Controller
control={control}
name="asset"
rules={{
validate: {
required,
},
}}
render={({ field }) => (
<TradingRichSelect
data-testid="select-asset"
id={field.name}
name={field.name}
onValueChange={(value) => {
field.onChange(value);
}}
placeholder={t('Please select an asset')}
value={field.value}
>
{assets.map((a) => (
<AssetOption
key={a.key}
asset={a}
balance={
<Balance
balance={formatNumber(a.balance, a.decimals)}
symbol={a.symbol}
/>
}
/>
))}
</TradingRichSelect>
)}
/>
{errors.asset?.message && (
<TradingInputError forInput="asset">
{errors.asset.message}
</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}>
{toGeneralAccount
? `${
AccountTypeMapping[AccountType.ACCOUNT_TYPE_GENERAL]
} (${addDecimalsFormatNumber(
toGeneralAccount.balance,
toGeneralAccount.asset.decimals
)} ${toGeneralAccount.asset.symbol})`
: `${AccountTypeMapping[AccountType.ACCOUNT_TYPE_GENERAL]} ${
asset ? `(0 ${asset.symbol})` : ''
}`}
</option>
</TradingSelect>
</TradingFormGroup>
<TradingFormGroup label="Amount" labelFor="amount">
<TradingInput
id="amount"
@@ -355,43 +353,7 @@ export const TransferForm = ({
{...register('amount', {
validate: {
required,
minSafe: (v) => {
if (!asset || !minQuantumMultiple) return true;
const value = new BigNumber(v);
if (value.isZero()) {
return t('Amount cannot be 0');
}
const minByQuantumMultiple = toBigNum(
minQuantumMultiple,
asset.decimals
);
if (fromVested) {
// special conditions which let you bypass min transfer rules set by quantum multiple
if (value.isGreaterThanOrEqualTo(max)) {
return true;
}
if (value.isLessThan(minByQuantumMultiple)) {
return t(
'Amount below minimum requirements for partial transfer. Use max to bypass'
);
}
return true;
} else {
if (value.isLessThan(minByQuantumMultiple)) {
return t(
'Amount below minimum requirement set by transfer.minTransferQuantumMultiple'
);
}
}
return true;
},
minSafe: (value) => minSafe(new BigNumber(min))(value),
maxSafe: (v) => {
const value = new BigNumber(v);
if (value.isGreaterThan(max)) {
@@ -407,9 +369,7 @@ export const TransferForm = ({
type="button"
className="absolute top-0 right-0 ml-auto text-xs underline"
onClick={() =>
setValue('amount', accountBalance, {
shouldValidate: true,
})
setValue('amount', parseFloat(accountBalance).toString())
}
>
{t('Use max')}
@@ -430,10 +390,10 @@ export const TransferForm = ({
<div>
<TradingCheckbox
name="include-transfer-fee"
disabled={!transferAmount || fromVested}
disabled={!transferAmount}
label={t('Include transfer fee')}
checked={includeFee}
onCheckedChange={() => setIncludeFee((x) => !x)}
onCheckedChange={() => setIncludeFee(!includeFee)}
/>
</div>
</Tooltip>
@@ -443,7 +403,7 @@ export const TransferForm = ({
amount={transferAmount}
transferAmount={transferAmount}
feeFactor={feeFactor}
fee={fromVested ? '0' : fee}
fee={fee}
decimals={asset?.decimals}
/>
)}
@@ -525,38 +485,32 @@ export const TransferFee = ({
);
};
type ToVegaKeyMode = 'input' | 'select';
interface AddressInputProps {
select: ReactNode;
input: ReactNode;
mode: ToVegaKeyMode;
onChange: () => void;
}
export const AddressField = ({
select,
input,
mode,
onChange,
}: AddressInputProps) => {
const isInput = mode === 'input';
const [isInput, setIsInput] = useState(false);
return (
<>
{isInput ? input : select}
{select && input && (
<button
type="button"
onClick={onChange}
className="absolute top-0 right-0 ml-auto text-xs 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>
</>
);
};
const parseFromAccount = (fromAccountStr: string) => {
return fromAccountStr.split('-') as [AccountType, string];
};
@@ -37,9 +37,6 @@ export const AgGridThemed = ({
<div className={wrapperClasses}>
<AgGridReact
defaultColDef={defaultColDef}
overlayLoadingTemplate={t('Loading...')}
overlayNoRowsTemplate={t('No data')}
suppressDragLeaveHidesColumns
ref={gridRef}
{...defaultProps}
{...props}
@@ -72,11 +72,15 @@ export const DealTicketFeeDetails = ({
return (
<KeyValue
label={
label={t('Fees')}
value={
totalDiscountedFeeAmount &&
`~${formatValue(totalDiscountedFeeAmount, assetDecimals)}`
}
formattedValue={
<>
{t('Fees')}
{totalDiscountFactor ? (
<Pill size="xxs" intent={Intent.Info} className="ml-1">
<Pill size="xxs" intent={Intent.Warning} className="mr-1">
-
{formatNumberPercentage(
new BigNumber(totalDiscountFactor).multipliedBy(100),
@@ -84,16 +88,10 @@ export const DealTicketFeeDetails = ({
)}
</Pill>
) : null}
{totalDiscountedFeeAmount &&
`~${formatValue(totalDiscountedFeeAmount, assetDecimals, quantum)}`}
</>
}
value={
totalDiscountedFeeAmount &&
`~${formatValue(totalDiscountedFeeAmount, assetDecimals)}`
}
formattedValue={
totalDiscountedFeeAmount &&
`~${formatValue(totalDiscountedFeeAmount, assetDecimals, quantum)}`
}
labelDescription={
<div className="flex flex-col gap-2">
<p>
@@ -151,7 +149,6 @@ export const DealTicketMarginDetails = ({
const { decimals: assetDecimals, quantum } = asset;
let marginRequiredBestCase: string | undefined = undefined;
let marginRequiredWorstCase: string | undefined = undefined;
if (marginEstimate) {
if (currentMargins) {
marginRequiredBestCase = (
@@ -289,7 +286,7 @@ export const DealTicketMarginDetails = ({
const quoteName = getQuoteName(market);
return (
<div className="flex flex-col w-full gap-2 pt-2">
<div className="flex flex-col w-full gap-2">
<Accordion>
<AccordionPanel
itemId="margin"
@@ -297,7 +294,7 @@ export const DealTicketMarginDetails = ({
<AccordionPrimitive.Trigger
data-testid="accordion-toggle"
className={classNames(
'w-full',
'w-full pt-2',
'flex items-center gap-2 text-xs',
'group'
)}
@@ -79,11 +79,7 @@ export const FeesBreakdown = ({
volumeDiscountFactor
);
const {
discountedFee: discountedTotalFeeAmount,
volumeDiscount,
referralDiscount,
} = getDiscountedFee(
const { volumeDiscount, referralDiscount } = getDiscountedFee(
totalFeeAmount,
referralDiscountFactor,
volumeDiscountFactor
@@ -135,7 +131,7 @@ export const FeesBreakdown = ({
<FeesBreakdownItem
label={t('Total fees')}
factor={feeFactors ? sumFeesFactors(feeFactors)?.toString() : undefined}
value={discountedTotalFeeAmount}
value={totalFeeAmount}
symbol={symbol}
decimals={decimals}
/>
+85 -45
View File
@@ -4,8 +4,14 @@ import { getDateTimeFormat } from '@vegaprotocol/utils';
import * as Schema from '@vegaprotocol/types';
import type { PartialDeep } from 'type-fest';
import type { Trade } from './fills-data-provider';
import { FeesDiscountBreakdownTooltip, FillsTable } from './fills-table';
import {
FeesDiscountBreakdownTooltip,
FillsTable,
getFeesBreakdown,
getTotalFeesDiscounts,
} from './fills-table';
import { generateFill } from './test-helpers';
import type { TradeFeeFieldsFragment } from './__generated__/Fills';
const partyId = 'party-id';
const defaultFill: PartialDeep<Trade> = {
@@ -29,7 +35,6 @@ const defaultFill: PartialDeep<Trade> = {
},
createdAt: new Date('2022-02-02T14:00:00').toISOString(),
};
describe('FillsTable', () => {
it('correct columns are rendered', async () => {
// 7005-FILL-001
@@ -60,7 +65,7 @@ describe('FillsTable', () => {
expect(headers.map((h) => h.textContent?.trim())).toEqual(expectedHeaders);
});
it('formats cells correctly for buyer fill for maker', async () => {
it('formats cells correctly for buyer fill', async () => {
const buyerFill = generateFill({
...defaultFill,
buyer: {
@@ -84,7 +89,7 @@ describe('FillsTable', () => {
'3.00 BTC',
'Maker',
'2.00 BTC',
'0.09 BTC',
'0.27 BTC',
getDateTimeFormat().format(new Date(buyerFill.createdAt)),
'', // action column
];
@@ -266,48 +271,83 @@ describe('FillsTable', () => {
.find((c) => c.getAttribute('col-id') === 'size');
expect(sizeCell).toHaveTextContent('3,000,000,000');
});
});
describe('FeesDiscountBreakdownTooltip', () => {
it('shows all discounts', () => {
const data = generateFill({
...defaultFill,
buyer: {
id: partyId,
},
});
const props = {
data,
partyId,
value: data.market,
} as Parameters<typeof FeesDiscountBreakdownTooltip>['0'];
const { container } = render(<FeesDiscountBreakdownTooltip {...props} />);
const dt = container.querySelectorAll('dt');
const dd = container.querySelectorAll('dd');
const expectedDt = [
'Infrastructure Fee',
'Referral Discount',
'Volume Discount',
'Liquidity Fee',
'Referral Discount',
'Volume Discount',
'Maker Fee',
'Referral Discount',
'Volume Discount',
];
const expectedDD = [
'0.05 BTC',
'0.06 BTC',
'0.01 BTC',
'0.02 BTC',
'0.03 BTC',
'0.04 BTC',
];
expectedDt.forEach((label, i) => {
expect(dt[i]).toHaveTextContent(label);
});
expectedDD.forEach((label, i) => {
expect(dd[i]).toHaveTextContent(label);
});
describe('FeesDiscountBreakdownTooltip', () => {
it('shows all discounts', () => {
const data = generateFill({
...defaultFill,
buyer: {
id: partyId,
},
});
const props = {
data,
partyId,
value: data.market,
} as Parameters<typeof FeesDiscountBreakdownTooltip>['0'];
const { container } = render(<FeesDiscountBreakdownTooltip {...props} />);
const dt = container.querySelectorAll('dt');
const dd = container.querySelectorAll('dd');
const expected = [
{ label: 'Infrastructure Fee Referral Discount', value: '0.05 BTC' },
{ label: 'Infrastructure Fee Volume Discount', value: '0.06 BTC' },
{ label: 'Liquidity Fee Referral Discount', value: '0.01 BTC' },
{ label: 'Liquidity Fee Volume Discount', value: '0.02 BTC' },
{ label: 'Maker Fee Referral Discount', value: '0.03 BTC' },
{ label: 'Maker Fee Volume Discount', value: '0.04 BTC' },
];
expected.forEach(({ label, value }, i) => {
expect(dt[i]).toHaveTextContent(label);
expect(dd[i]).toHaveTextContent(value);
});
});
});
describe('getFeesBreakdown', () => {
it('should return correct fees breakdown for a taker', () => {
const fees = {
makerFee: '1000',
infrastructureFee: '2000',
liquidityFee: '3000',
};
const expectedBreakdown = {
infrastructureFee: '2000',
liquidityFee: '3000',
makerFee: '1000',
totalFee: '6000',
};
expect(getFeesBreakdown('Taker', fees)).toEqual(expectedBreakdown);
});
it('should return correct fees breakdown for a maker', () => {
const fees = {
makerFee: '1000',
infrastructureFee: '2000',
liquidityFee: '3000',
};
const expectedBreakdown = {
infrastructureFee: '2000',
liquidityFee: '3000',
makerFee: '-1000',
totalFee: '4000',
};
expect(getFeesBreakdown('Maker', fees)).toEqual(expectedBreakdown);
});
});
describe('getTotalFeesDiscounts', () => {
it('should return correct total value', () => {
const fees = {
infrastructureFeeReferralDiscount: '1',
infrastructureFeeVolumeDiscount: '2',
liquidityFeeReferralDiscount: '3',
liquidityFeeVolumeDiscount: '4',
makerFeeReferralDiscount: '5',
makerFeeVolumeDiscount: '6',
};
expect(getTotalFeesDiscounts(fees as TradeFeeFieldsFragment)).toEqual(
(1 + 2 + 3 + 4 + 5 + 6).toString()
);
});
});
+121 -60
View File
@@ -1,11 +1,10 @@
import { useMemo } from 'react';
import {
type AgGridReact,
type AgGridReactProps,
type AgReactUiProps,
import type {
AgGridReact,
AgGridReactProps,
AgReactUiProps,
} from 'ag-grid-react';
import type { ColDef } from 'ag-grid-community';
import type { ITooltipParams } from 'ag-grid-community';
import type { ITooltipParams, ColDef } from 'ag-grid-community';
import {
addDecimal,
addDecimalsFormatNumber,
@@ -30,11 +29,19 @@ import type {
import { forwardRef } from 'react';
import BigNumber from 'bignumber.js';
import type { Trade } from './fills-data-provider';
import type {
FillFieldsFragment,
TradeFeeFieldsFragment,
} from './__generated__/Fills';
import { FillActionsDropdown } from './fill-actions-dropdown';
import { getAsset } from '@vegaprotocol/markets';
import { MAKER, TAKER, getFeesBreakdown, getRoleAndFees } from './fills-utils';
type Props = (AgGridReactProps | AgReactUiProps) & {
const TAKER = 'Taker';
const MAKER = 'Maker';
export type Role = typeof TAKER | typeof MAKER | '-';
export type Props = (AgGridReactProps | AgReactUiProps) & {
partyId: string;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
};
@@ -254,13 +261,63 @@ const formatFeeDiscount = (partyId: string) => {
}: VegaValueFormatterParams<Trade, 'market'>) => {
if (!market || !data) return '-';
const asset = getAsset(market);
const { fees: roleFees, role } = getRoleAndFees({ data, partyId });
if (!roleFees) return '-';
const { totalFeeDiscount } = getFeesBreakdown(role, roleFees);
return addDecimalsFormatNumber(totalFeeDiscount, asset.decimals);
const { fees } = getRoleAndFees({ data, partyId });
if (!fees) return '-';
const total = getTotalFeesDiscounts(fees);
return addDecimalsFormatNumber(total, asset.decimals);
};
};
export const isEmptyFeeObj = (feeObj: Schema.TradeFee) => {
if (!feeObj) return true;
return (
feeObj.liquidityFee === '0' &&
feeObj.makerFee === '0' &&
feeObj.infrastructureFee === '0'
);
};
export const getRoleAndFees = ({
data,
partyId,
}: {
data: Pick<
FillFieldsFragment,
'buyerFee' | 'sellerFee' | 'buyer' | 'seller' | 'aggressor'
>;
partyId?: string;
}) => {
let role: Role;
let fees;
if (data?.buyer.id === partyId) {
if (data.aggressor === Schema.Side.SIDE_BUY) {
role = TAKER;
fees = data?.buyerFee;
} else if (data.aggressor === Schema.Side.SIDE_SELL) {
role = MAKER;
fees = data?.sellerFee;
} else {
role = '-';
fees = !isEmptyFeeObj(data?.buyerFee) ? data.buyerFee : data.sellerFee;
}
} else if (data?.seller.id === partyId) {
if (data.aggressor === Schema.Side.SIDE_SELL) {
role = TAKER;
fees = data?.sellerFee;
} else if (data.aggressor === Schema.Side.SIDE_BUY) {
role = MAKER;
fees = data?.buyerFee;
} else {
role = '-';
fees = !isEmptyFeeObj(data.sellerFee) ? data.sellerFee : data.buyerFee;
}
} else {
return { role: '-', fees: undefined };
}
return { role, fees };
};
const FeesBreakdownTooltip = ({
data,
value: market,
@@ -272,23 +329,16 @@ const FeesBreakdownTooltip = ({
const asset = getAsset(market);
const { role, fees, marketState } = getRoleAndFees({ data, partyId }) ?? {};
const { role, fees } = getRoleAndFees({ data, partyId }) ?? {};
if (!fees) return null;
const { infrastructureFee, liquidityFee, makerFee, totalFee } =
getFeesBreakdown(role, fees, marketState);
getFeesBreakdown(role, fees);
return (
<div
data-testid="fee-breakdown-tooltip"
className="z-20 max-w-sm px-4 py-2 text-xs text-black border rounded bg-vega-light-100 dark:bg-vega-dark-100 border-vega-light-200 dark:border-vega-dark-200 break-word dark:text-white"
className="z-20 max-w-sm px-4 py-2 text-sm text-black border rounded bg-vega-light-100 dark:bg-vega-dark-100 border-vega-light-200 dark:border-vega-dark-200 break-word dark:text-white"
>
{marketState && (
<p className="mb-1 italic">
{t('If the market was %s', [
Schema.MarketStateMapping[marketState].toLowerCase(),
])}
</p>
)}
{role === MAKER && (
<>
<p className="mb-1">{t('The maker will receive the maker fee.')}</p>
@@ -302,7 +352,7 @@ const FeesBreakdownTooltip = ({
{role === TAKER && (
<p className="mb-1">{t('Fees to be paid by the taker.')}</p>
)}
{(role === '-' || marketState === Schema.MarketState.STATE_SUSPENDED) && (
{role === '-' && (
<p className="mb-1">
{t(
'If the market is in monitoring auction, half of the infrastructure and liquidity fees will be paid.'
@@ -341,10 +391,10 @@ const FeesDiscountBreakdownTooltipItem = ({
label: string;
asset: ReturnType<typeof getAsset>;
}) =>
value && value !== '0' ? (
value ? (
<>
<dt className="col-span-2">{label}</dt>
<dd className="text-right col-span-2">
<dt className="col-span-1">{label}</dt>
<dd className="text-right col-span-1">
{addDecimalsFormatNumber(value, asset.decimals)} {asset.symbol}
</dd>
</>
@@ -359,70 +409,81 @@ export const FeesDiscountBreakdownTooltip = ({
}
const asset = getAsset(data.market);
const {
fees: roleFees,
marketState,
role,
} = getRoleAndFees({ data, partyId }) ?? {};
if (!roleFees) return null;
const fees = getFeesBreakdown(role, roleFees, marketState);
const { fees } = getRoleAndFees({ data, partyId }) ?? {};
if (!fees) return null;
return (
<div
data-testid="fee-discount-breakdown-tooltip"
className="max-w-sm bg-vega-light-100 dark:bg-vega-dark-100 border border-vega-light-200 dark:border-vega-dark-200 px-4 py-2 z-20 rounded text-sm break-word text-black dark:text-white"
>
<dl className="grid grid-cols-6 gap-x-1 text-xs">
{(fees.infrastructureFeeReferralDiscount || '0') !== '0' ||
(fees.infrastructureFeeVolumeDiscount || '0') !== '0' ? (
<dt className="col-span-2">{t('Infrastructure Fee')}</dt>
) : null}
<dl className="grid grid-cols-2 gap-x-1">
<FeesDiscountBreakdownTooltipItem
value={fees.infrastructureFeeReferralDiscount}
label={t('Referral Discount')}
label={t('Infrastructure Fee Referral Discount')}
asset={asset}
/>
<FeesDiscountBreakdownTooltipItem
value={fees.infrastructureFeeVolumeDiscount}
label={t('Volume Discount')}
label={t('Infrastructure Fee Volume Discount')}
asset={asset}
/>
{(fees.liquidityFeeReferralDiscount || '0') !== '0' ||
(fees.liquidityFeeVolumeDiscount || '0') !== '0' ? (
<dt className="col-span-2">{t('Liquidity Fee')}</dt>
) : null}
<FeesDiscountBreakdownTooltipItem
value={fees.liquidityFeeReferralDiscount}
label={t('Referral Discount')}
label={t('Liquidity Fee Referral Discount')}
asset={asset}
/>
<FeesDiscountBreakdownTooltipItem
value={fees.liquidityFeeVolumeDiscount}
label={t('Volume Discount')}
label={t('Liquidity Fee Volume Discount')}
asset={asset}
/>
{(fees.makerFeeReferralDiscount || '0') !== '0' ||
(fees.makerFeeVolumeDiscount || '0') !== '0' ? (
<dt className="col-span-2">{t('Maker Fee')}</dt>
) : null}
<FeesDiscountBreakdownTooltipItem
value={fees.makerFeeReferralDiscount}
label={t('Referral Discount')}
label={t('Maker Fee Referral Discount')}
asset={asset}
/>
<FeesDiscountBreakdownTooltipItem
value={fees.makerFeeVolumeDiscount}
label={t('Volume Discount')}
asset={asset}
/>
<dt className="col-span-2">{t('Total Fee Discount')}</dt>
<FeesDiscountBreakdownTooltipItem
value={fees.totalFeeDiscount}
label={''}
label={t('Maker Fee Volume Discount')}
asset={asset}
/>
</dl>
</div>
);
};
export const getTotalFeesDiscounts = (fees: TradeFeeFieldsFragment) => {
return (
BigInt(fees.infrastructureFeeReferralDiscount || '0') +
BigInt(fees.infrastructureFeeVolumeDiscount || '0') +
BigInt(fees.liquidityFeeReferralDiscount || '0') +
BigInt(fees.liquidityFeeVolumeDiscount || '0') +
BigInt(fees.makerFeeReferralDiscount || '0') +
BigInt(fees.makerFeeVolumeDiscount || '0')
).toString();
};
export const getFeesBreakdown = (
role: Role,
feesObj: TradeFeeFieldsFragment
) => {
const makerFee =
role === MAKER
? new BigNumber(feesObj.makerFee).times(-1).toString()
: feesObj.makerFee;
const infrastructureFee = feesObj.infrastructureFee;
const liquidityFee = feesObj.liquidityFee;
const totalFee = new BigNumber(infrastructureFee)
.plus(makerFee)
.plus(liquidityFee)
.toString();
return {
infrastructureFee,
liquidityFee,
makerFee,
totalFee,
};
};
-183
View File
@@ -1,183 +0,0 @@
import { getFeesBreakdown } from './fills-utils';
import * as Schema from '@vegaprotocol/types';
describe('getFeesBreakdown', () => {
it('should return correct fees breakdown for a taker', () => {
const fees = {
makerFee: '1000',
infrastructureFee: '2000',
liquidityFee: '3000',
};
const expectedBreakdown = {
infrastructureFee: '2000',
liquidityFee: '3000',
makerFee: '1000',
totalFee: '6000',
totalFeeDiscount: '0',
};
expect(getFeesBreakdown('Taker', fees)).toEqual(expectedBreakdown);
});
it('should return correct fees breakdown for a maker if market is active', () => {
const fees = {
makerFee: '1000',
infrastructureFee: '2000',
liquidityFee: '3000',
};
const expectedBreakdown = {
infrastructureFee: '0',
liquidityFee: '0',
makerFee: '-1000',
totalFee: '-1000',
totalFeeDiscount: '0',
};
expect(
getFeesBreakdown('Maker', fees, Schema.MarketState.STATE_ACTIVE)
).toEqual(expectedBreakdown);
});
it('should return correct fees breakdown for a maker if the market is suspended', () => {
const fees = {
infrastructureFee: '2000',
liquidityFee: '3000',
makerFee: '0',
};
const expectedBreakdown = {
infrastructureFee: '1000',
liquidityFee: '1500',
makerFee: '0',
totalFee: '2500',
totalFeeDiscount: '0',
};
expect(
getFeesBreakdown('Maker', fees, Schema.MarketState.STATE_SUSPENDED)
).toEqual(expectedBreakdown);
});
it('should return correct fees breakdown for a taker if the market is suspended', () => {
const fees = {
infrastructureFee: '2000',
liquidityFee: '3000',
makerFee: '0',
};
const expectedBreakdown = {
infrastructureFee: '1000',
liquidityFee: '1500',
makerFee: '0',
totalFee: '2500',
totalFeeDiscount: '0',
};
expect(
getFeesBreakdown('Taker', fees, Schema.MarketState.STATE_SUSPENDED)
).toEqual(expectedBreakdown);
});
it('should return correct fees breakdown for a taker if market is active', () => {
const fees = {
makerFee: '1000',
infrastructureFee: '2000',
liquidityFee: '3000',
};
const expectedBreakdown = {
infrastructureFee: '2000',
liquidityFee: '3000',
makerFee: '1000',
totalFee: '6000',
totalFeeDiscount: '0',
};
expect(
getFeesBreakdown('Taker', fees, Schema.MarketState.STATE_ACTIVE)
).toEqual(expectedBreakdown);
});
it('should return correct fees breakdown for a maker', () => {
const fees = {
makerFee: '1000',
infrastructureFee: '2000',
liquidityFee: '3000',
};
const expectedBreakdown = {
infrastructureFee: '0',
liquidityFee: '0',
makerFee: '-1000',
totalFee: '-1000',
totalFeeDiscount: '0',
};
expect(getFeesBreakdown('Maker', fees)).toEqual(expectedBreakdown);
});
it('should return correct total fees discount value for a taker (if the market is active - default)', () => {
const fees = {
infrastructureFeeReferralDiscount: '1',
infrastructureFeeVolumeDiscount: '2',
liquidityFeeReferralDiscount: '3',
liquidityFeeVolumeDiscount: '4',
makerFeeReferralDiscount: '5',
makerFeeVolumeDiscount: '6',
infrastructureFee: '1000',
liquidityFee: '2000',
makerFee: '3000',
};
const { totalFeeDiscount } = getFeesBreakdown('Taker', fees);
expect(totalFeeDiscount).toEqual((1 + 2 + 3 + 4 + 5 + 6).toString());
});
it('should return correct total fees discount value for a maker (if the market is active - default)', () => {
const fees = {
infrastructureFeeReferralDiscount: '1',
infrastructureFeeVolumeDiscount: '2',
liquidityFeeReferralDiscount: '3',
liquidityFeeVolumeDiscount: '4',
makerFeeReferralDiscount: '5',
makerFeeVolumeDiscount: '6',
infrastructureFee: '1000',
liquidityFee: '2000',
makerFee: '3000',
};
const { totalFeeDiscount } = getFeesBreakdown('Maker', fees);
// makerFeeReferralDiscount and makerFeeVolumeDiscount are added, infra and liq. fees are zeroed
expect(totalFeeDiscount).toEqual((5 + 6).toString());
});
it('should return correct total fees discount value for a maker (if the market is suspended)', () => {
const fees = {
infrastructureFeeReferralDiscount: '1',
infrastructureFeeVolumeDiscount: '2',
liquidityFeeReferralDiscount: '3',
liquidityFeeVolumeDiscount: '4',
makerFeeReferralDiscount: '5',
makerFeeVolumeDiscount: '6',
infrastructureFee: '1000',
liquidityFee: '2000',
makerFee: '3000',
};
const { totalFeeDiscount } = getFeesBreakdown(
'Maker',
fees,
Schema.MarketState.STATE_SUSPENDED
);
// makerFeeReferralDiscount and makerFeeVolumeDiscount are zeroed, infra and liq. fees are halved
expect(totalFeeDiscount).toEqual(((1 + 2 + 3 + 4) / 2).toString());
});
it('should return correct total fees discount value for a taker (if the market is suspended)', () => {
const fees = {
infrastructureFeeReferralDiscount: '1',
infrastructureFeeVolumeDiscount: '2',
liquidityFeeReferralDiscount: '3',
liquidityFeeVolumeDiscount: '4',
makerFeeReferralDiscount: '5',
makerFeeVolumeDiscount: '6',
infrastructureFee: '1000',
liquidityFee: '2000',
makerFee: '3000',
};
const { totalFeeDiscount } = getFeesBreakdown(
'Taker',
fees,
Schema.MarketState.STATE_SUSPENDED
);
// makerFeeReferralDiscount and makerFeeVolumeDiscount are zeroed, infra and liq. fees are halved
expect(totalFeeDiscount).toEqual(((1 + 2 + 3 + 4) / 2).toString());
});
});
-164
View File
@@ -1,164 +0,0 @@
import BigNumber from 'bignumber.js';
import type {
FillFieldsFragment,
TradeFeeFieldsFragment,
} from './__generated__/Fills';
import * as Schema from '@vegaprotocol/types';
export const TAKER = 'Taker';
export const MAKER = 'Maker';
export type Role = typeof TAKER | typeof MAKER | '-';
export const getRoleAndFees = ({
data,
partyId,
}: {
data: Pick<
FillFieldsFragment,
'buyerFee' | 'sellerFee' | 'buyer' | 'seller' | 'aggressor'
>;
partyId?: string;
}): {
role: Role;
fees?: TradeFeeFieldsFragment;
marketState?: Schema.MarketState;
} => {
let role: Role;
let fees;
if (data?.buyer.id === partyId) {
if (data.aggressor === Schema.Side.SIDE_BUY) {
role = TAKER;
fees = data?.buyerFee;
} else if (data.aggressor === Schema.Side.SIDE_SELL) {
role = MAKER;
fees = data?.sellerFee;
} else {
role = '-';
fees = !isEmptyFeeObj(data?.buyerFee) ? data.buyerFee : data.sellerFee;
}
} else if (data?.seller.id === partyId) {
if (data.aggressor === Schema.Side.SIDE_SELL) {
role = TAKER;
fees = data?.sellerFee;
} else if (data.aggressor === Schema.Side.SIDE_BUY) {
role = MAKER;
fees = data?.buyerFee;
} else {
role = '-';
fees = !isEmptyFeeObj(data.sellerFee) ? data.sellerFee : data.buyerFee;
}
} else {
return { role: '-', fees: undefined };
}
// We make the assumption that the market state is active if the maker fee is zero on both sides
// This needs to be updated when we have a way to get the correct market state when that fill happened from the API
// because the maker fee factor can be set to 0 via governance
const marketState =
data?.buyerFee.makerFee === data.sellerFee.makerFee &&
new BigNumber(data?.buyerFee.makerFee).isZero()
? Schema.MarketState.STATE_SUSPENDED
: Schema.MarketState.STATE_ACTIVE;
return { role, fees, marketState };
};
export const getFeesBreakdown = (
role: Role,
fees: TradeFeeFieldsFragment,
marketState: Schema.MarketState = Schema.MarketState.STATE_ACTIVE
) => {
// If market is in auction we assume maker fee is zero
const isMarketActive = marketState === Schema.MarketState.STATE_ACTIVE;
// If role is taker, then these are the fees to be paid
let { makerFee, infrastructureFee, liquidityFee } = fees;
// If role is taker, then these are the fees discounts to be applied
let {
makerFeeVolumeDiscount,
makerFeeReferralDiscount,
infrastructureFeeVolumeDiscount,
infrastructureFeeReferralDiscount,
liquidityFeeVolumeDiscount,
liquidityFeeReferralDiscount,
} = fees;
if (isMarketActive) {
if (role === MAKER) {
makerFee = new BigNumber(fees.makerFee).times(-1).toString();
infrastructureFee = '0';
liquidityFee = '0';
// discounts are also zero or we can leave them undefined
infrastructureFeeReferralDiscount =
infrastructureFeeReferralDiscount && '0';
infrastructureFeeVolumeDiscount = infrastructureFeeVolumeDiscount && '0';
liquidityFeeReferralDiscount = liquidityFeeReferralDiscount && '0';
liquidityFeeVolumeDiscount = liquidityFeeVolumeDiscount && '0';
// we leave maker discount fees as they are defined
}
} else {
// If market is suspended (in monitoring auction), then half of the fees are paid
infrastructureFee = new BigNumber(infrastructureFee)
.dividedBy(2)
.toString();
liquidityFee = new BigNumber(liquidityFee).dividedBy(2).toString();
// maker fee is already zero
makerFee = '0';
// discounts are also halved
infrastructureFeeReferralDiscount =
infrastructureFeeReferralDiscount &&
new BigNumber(infrastructureFeeReferralDiscount).dividedBy(2).toString();
infrastructureFeeVolumeDiscount =
infrastructureFeeVolumeDiscount &&
new BigNumber(infrastructureFeeVolumeDiscount).dividedBy(2).toString();
liquidityFeeReferralDiscount =
liquidityFeeReferralDiscount &&
new BigNumber(liquidityFeeReferralDiscount).dividedBy(2).toString();
liquidityFeeVolumeDiscount =
liquidityFeeVolumeDiscount &&
new BigNumber(liquidityFeeVolumeDiscount).dividedBy(2).toString();
// maker discount fees should already be zero
makerFeeReferralDiscount = makerFeeReferralDiscount && '0';
makerFeeVolumeDiscount = makerFeeVolumeDiscount && '0';
}
const totalFee = new BigNumber(infrastructureFee)
.plus(makerFee)
.plus(liquidityFee)
.toString();
const totalFeeDiscount = new BigNumber(makerFeeVolumeDiscount || '0')
.plus(makerFeeReferralDiscount || '0')
.plus(infrastructureFeeReferralDiscount || '0')
.plus(infrastructureFeeVolumeDiscount || '0')
.plus(liquidityFeeReferralDiscount || '0')
.plus(liquidityFeeVolumeDiscount || '0')
.toString();
return {
infrastructureFee,
infrastructureFeeReferralDiscount,
infrastructureFeeVolumeDiscount,
liquidityFee,
liquidityFeeReferralDiscount,
liquidityFeeVolumeDiscount,
makerFee,
makerFeeReferralDiscount,
makerFeeVolumeDiscount,
totalFee,
totalFeeDiscount,
};
};
export const isEmptyFeeObj = (feeObj: Schema.TradeFee) => {
if (!feeObj) return true;
return (
feeObj.liquidityFee === '0' &&
feeObj.makerFee === '0' &&
feeObj.infrastructureFee === '0'
);
};
@@ -278,4 +278,18 @@ describe('createDownloadUrl', () => {
)}&dateRange.endTimestamp=${toNanoSeconds(dateTo)}`
);
});
it('should throw if invalid args are provided', () => {
// invalid url
expect(() => {
// @ts-ignore override z.infer type
createDownloadUrl({ ...args, protohost: 'foo' });
}).toThrow();
// invalid partyId
expect(() => {
// @ts-ignore override z.infer type
createDownloadUrl({ ...args, partyId: 'z'.repeat(64) });
}).toThrow();
});
});
+145 -200
View File
@@ -1,18 +1,18 @@
import { useRef, useCallback } from 'react';
import { subDays } from 'date-fns';
import { Controller, useForm } from 'react-hook-form';
import { useRef, useState } from 'react';
import { format, subDays } from 'date-fns';
import {
InputError,
Intent,
Loader,
TradingButton,
TradingFormGroup,
TradingInput,
TradingSelect,
} from '@vegaprotocol/ui-toolkit';
import { z } from 'zod';
import {
formatForInput,
getDateTimeFormat,
toNanoSeconds,
VEGA_ID_REGEX,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { localLoggerFactory } from '@vegaprotocol/logger';
@@ -35,18 +35,18 @@ const getProtoHost = (vegaurl: string) => {
return `${loc.protocol}//${loc.host}`;
};
type LedgerFormValues = {
assetId: string;
dateFrom: string;
dateTo?: string;
};
const downloadSchema = z.object({
protohost: z.string().url().nonempty(),
partyId: z.string().regex(VEGA_ID_REGEX).nonempty(),
assetId: z.string().regex(VEGA_ID_REGEX).nonempty(),
dateFrom: z.string().nonempty(),
dateTo: z.string().optional(),
});
export const createDownloadUrl = (args: z.infer<typeof downloadSchema>) => {
// check args from form inputs
downloadSchema.parse(args);
export const createDownloadUrl = (
args: LedgerFormValues & {
partyId: string;
protohost: string;
}
) => {
const params = new URLSearchParams();
params.append('partyId', args.partyId);
params.append('assetId', args.assetId);
@@ -71,102 +71,115 @@ interface Props {
export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
const now = useRef(new Date());
const { control, handleSubmit, watch } = useForm<LedgerFormValues>({
defaultValues: {
dateFrom: formatForInput(subDays(now.current, 7)),
dateTo: '',
assetId: Object.keys(assets)[0],
},
const [dateFrom, setDateFrom] = useState(() => {
return formatForInput(subDays(now.current, 7));
});
const dateTo = watch('dateTo');
const [dateTo, setDateTo] = useState('');
const maxFromDate = formatForInput(new Date(dateTo || now.current));
const maxToDate = formatForInput(now.current);
const [assetId, setAssetId] = useState(Object.keys(assets)[0]);
const protohost = getProtoHost(vegaUrl);
const disabled = Boolean(!assetId);
const hasItem = useLedgerDownloadFile((store) => store.hasItem);
const updateDownloadQueue = useLedgerDownloadFile(
(store) => store.updateQueue
);
const startDownload = useCallback(
async (formValues: LedgerFormValues) => {
const link = createDownloadUrl({
protohost,
partyId,
...formValues,
});
const dateTimeFormatter = getDateTimeFormat();
const title = t('Downloading for %s from %s till %s', [
assets[formValues.assetId],
dateTimeFormatter.format(new Date(formValues.dateFrom)),
dateTimeFormatter.format(new Date(formValues.dateTo || Date.now())),
]);
const downloadStoreItem = {
title,
link,
isChanged: true,
};
if (hasItem(link)) {
updateDownloadQueue(downloadStoreItem);
return;
}
const ts = setTimeout(() => {
updateDownloadQueue({
...downloadStoreItem,
intent: Intent.Warning,
isDelayed: true,
isChanged: true,
});
}, 1000 * 30);
try {
updateDownloadQueue(downloadStoreItem);
const resp = await fetch(link);
if (!resp?.ok) {
if (resp?.status === 429) {
throw new Error('Too many requests. Try again later.');
}
throw new Error('Download of ledger entries failed');
}
const { headers } = resp;
const nameHeader = headers.get('content-disposition');
const filename =
nameHeader?.split('=').pop() ?? DEFAULT_EXPORT_FILE_NAME;
updateDownloadQueue({
...downloadStoreItem,
filename,
});
const blob = await resp.blob();
if (blob) {
updateDownloadQueue({
...downloadStoreItem,
blob,
isDownloaded: true,
isChanged: true,
intent: Intent.Success,
});
}
} catch (err) {
localLoggerFactory({ application: 'ledger' }).error(
'Download file',
err
);
updateDownloadQueue({
...downloadStoreItem,
intent: Intent.Danger,
isError: true,
isChanged: true,
errorMessage: (err as Error).message || undefined,
});
} finally {
clearTimeout(ts);
}
},
[assets, hasItem, partyId, protohost, updateDownloadQueue]
const assetDropDown = (
<TradingSelect
id="select-ledger-asset"
value={assetId}
onChange={(e) => {
setAssetId(e.target.value);
}}
className="w-full"
data-testid="select-ledger-asset"
>
{Object.keys(assets).map((assetKey) => (
<option key={assetKey} value={assetKey}>
{assets[assetKey]}
</option>
))}
</TradingSelect>
);
const link = createDownloadUrl({
protohost,
partyId,
assetId,
dateFrom,
dateTo,
});
const startDownload = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const title = t('Downloading for %s from %s till %s', [
assets[assetId],
format(new Date(dateFrom), 'dd MMMM yyyy HH:mm'),
format(new Date(dateTo || Date.now()), 'dd MMMM yyyy HH:mm'),
]);
const downloadStoreItem = {
title,
link,
isChanged: true,
};
if (hasItem(link)) {
updateDownloadQueue(downloadStoreItem);
return;
}
const ts = setTimeout(() => {
updateDownloadQueue({
...downloadStoreItem,
intent: Intent.Warning,
isDelayed: true,
isChanged: true,
});
}, 1000 * 30);
try {
updateDownloadQueue(downloadStoreItem);
const resp = await fetch(link);
if (!resp?.ok) {
if (resp?.status === 429) {
throw new Error('Too many requests. Try again later.');
}
throw new Error('Download of ledger entries failed');
}
const { headers } = resp;
const nameHeader = headers.get('content-disposition');
const filename = nameHeader?.split('=').pop() ?? DEFAULT_EXPORT_FILE_NAME;
updateDownloadQueue({
...downloadStoreItem,
filename,
});
const blob = await resp.blob();
if (blob) {
updateDownloadQueue({
...downloadStoreItem,
blob,
isDownloaded: true,
isChanged: true,
intent: Intent.Success,
});
}
} catch (err) {
localLoggerFactory({ application: 'ledger' }).error('Download file', err);
updateDownloadQueue({
...downloadStoreItem,
intent: Intent.Danger,
isError: true,
isChanged: true,
errorMessage: (err as Error).message || undefined,
});
} finally {
clearTimeout(ts);
}
};
if (!protohost || Object.keys(assets).length === 0) {
return null;
}
@@ -174,117 +187,49 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
const offset = new Date().getTimezoneOffset();
return (
<form
onSubmit={handleSubmit(startDownload)}
className="p-4 w-[350px]"
noValidate
>
<form onSubmit={startDownload} className="p-4 w-[350px]">
<h2 className="mb-4">{t('Export ledger entries')}</h2>
<Controller
name="assetId"
control={control}
rules={{
required: t('You need to select an asset'),
}}
render={({ field, fieldState }) => (
<div className="mb-2">
<TradingFormGroup
label={t('Select asset')}
labelFor="asset"
compact
>
<TradingSelect
{...field}
id="select-ledger-asset"
className="w-full"
data-testid="select-ledger-asset"
>
{Object.keys(assets).map((assetKey) => (
<option key={assetKey} value={assetKey}>
{assets[assetKey]}
</option>
))}
</TradingSelect>
</TradingFormGroup>
{fieldState.error && (
<InputError>{fieldState.error.message}</InputError>
)}
</div>
)}
/>
<Controller
name="dateFrom"
control={control}
rules={{
required: t('You need to provide a date from'),
max: {
value: maxFromDate,
message: dateTo
? t('Date from cannot be greater than date to')
: t('Date from cannot be in the future'),
},
deps: ['dateTo'],
}}
render={({ field, fieldState }) => (
<div className="mb-2">
<TradingFormGroup
label={t('Date from')}
labelFor="date-from"
compact
>
<TradingInput
{...field}
type="datetime-local"
data-testid="date-from"
id="date-from"
max={maxFromDate}
/>
</TradingFormGroup>
{fieldState.error && (
<InputError>{fieldState.error.message}</InputError>
)}
</div>
)}
/>
<Controller
name="dateTo"
control={control}
rules={{
max: {
value: maxToDate,
message: t('Date to cannot be in the future'),
},
}}
render={({ field, fieldState }) => (
<div className="mb-2">
<TradingFormGroup label={t('Date to')} labelFor="date-to" compact>
<TradingInput
{...field}
type="datetime-local"
data-testid="date-to"
id="date-to"
max={maxToDate}
/>
</TradingFormGroup>
{fieldState.error && (
<InputError>{fieldState.error.message}</InputError>
)}
</div>
)}
/>
<TradingFormGroup label={t('Select asset')} labelFor="asset">
{assetDropDown}
</TradingFormGroup>
<TradingFormGroup label={t('Date from')} labelFor="date-from">
<TradingInput
type="datetime-local"
data-testid="date-from"
id="date-from"
value={dateFrom}
onChange={(e) => setDateFrom(e.target.value)}
max={maxFromDate}
/>
</TradingFormGroup>
<TradingFormGroup label={t('Date to')} labelFor="date-to">
<TradingInput
type="datetime-local"
data-testid="date-to"
id="date-to"
value={dateTo}
onChange={(e) => setDateTo(e.target.value)}
max={maxToDate}
/>
</TradingFormGroup>
<div className="relative text-sm" title={t('Download all to .csv file')}>
<TradingButton fill type="submit" data-testid="ledger-download-button">
<TradingButton
fill
disabled={disabled}
type="submit"
data-testid="ledger-download-button"
>
{t('Download')}
</TradingButton>
</div>
{offset ? (
{offset && (
<p className="text-xs text-neutral-400 mt-1">
{t(
'The downloaded file uses the UTC time zone for all listed times. Your time zone is UTC%s.',
[toHoursAndMinutes(offset)]
)}
</p>
) : null}
)}
</form>
);
};
+17 -14
View File
@@ -2,6 +2,7 @@ import { useMemo } from 'react';
import {
addDecimalsFormatNumber,
addDecimalsFormatNumberQuantum,
formatNumberPercentage,
getDateTimeFormat,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
@@ -28,12 +29,6 @@ import { LiquidityProvisionStatus } from '@vegaprotocol/types';
import { LiquidityProvisionStatusMapping } from '@vegaprotocol/types';
import type { LiquidityProvisionData } from './liquidity-data-provider';
const formatNumberPercentage = (value: BigNumber, decimals?: number) => {
const decimalPlaces =
typeof decimals === 'undefined' ? value.dp() || 0 : decimals;
return `${value.toFixed(decimalPlaces, 1)}%`;
};
const percentageFormatter = ({ value }: ValueFormatterParams) => {
if (!value) return '-';
return formatNumberPercentage(new BigNumber(value).times(100), 2) || '-';
@@ -106,6 +101,9 @@ export const LiquidityTable = ({
const feesAccruedTooltip = ({ value, data }: ITooltipParams) => {
if (!value) return '-';
const newValue = new BigNumber(value)
.times(Number(stakeToCcyVolume) || 1)
.toString();
let lessThanFull = false,
lessThanMinimum = false;
if (data.sla) {
@@ -128,11 +126,11 @@ export const LiquidityTable = ({
new BigNumber(data.sla.currentEpochFractionOfTimeOnBook).times(
100
),
4
2
),
formatNumberPercentage(
new BigNumber(data.commitmentMinTimeFraction).times(100),
4
2
),
]
);
@@ -145,12 +143,12 @@ export const LiquidityTable = ({
new BigNumber(data.sla.currentEpochFractionOfTimeOnBook).times(
100
),
4
2
),
]
);
}
return addDecimalsFormatNumber(value, assetDecimalPlaces ?? 0);
return addDecimalsFormatNumber(newValue, assetDecimalPlaces ?? 0);
};
const stakeToCcyVolumeQuantumFormatter = ({
@@ -396,7 +394,7 @@ export const LiquidityTable = ({
headerTooltip: t(
`The liquidity fees accrued by each provider, which will be distributed at the end of the epoch after applying any penalties.`
),
valueFormatter: assetDecimalsQuantumFormatter,
valueFormatter: stakeToCcyVolumeQuantumFormatter,
tooltipValueGetter: feesAccruedTooltip,
cellClassRules: {
'text-warning': ({ data }: { data: LiquidityProvisionData }) => {
@@ -412,9 +410,14 @@ export const LiquidityTable = ({
},
'text-red-500': ({ data }: { data: LiquidityProvisionData }) => {
if (!data.sla) return false;
return new BigNumber(
data.sla.currentEpochFractionOfTimeOnBook
).isLessThan(data.commitmentMinTimeFraction);
return (
new BigNumber(
data.sla.currentEpochFractionOfTimeOnBook
).isLessThan(data.commitmentMinTimeFraction) &&
new BigNumber(
data.sla.currentEpochFractionOfTimeOnBook
).isGreaterThan(0)
);
},
},
},
+1 -1
View File
@@ -6,4 +6,4 @@ export * from './markets-candles';
export * from './markets-data';
export * from './OracleMarketsSpec';
export * from './OracleSpecDataConnection';
export * from './SuccessorMarket'
export * from './SuccessorMarket'
-16
View File
@@ -28,22 +28,6 @@ export const getAsset = (market: Partial<Market>) => {
throw new Error('Failed to retrieve asset. Invalid product type');
};
export const getProductType = (market: Partial<Market>) => {
if (!market.tradableInstrument?.instrument.product) {
throw new Error(
'Failed to retrieve product type. Invalid tradable instrument'
);
}
const type = market.tradableInstrument.instrument.product.__typename;
if (!type) {
throw new Error('Failed to retrieve asset. Invalid product type');
}
return type;
};
export const getQuoteName = (market: Partial<Market>) => {
if (!market.tradableInstrument?.instrument.product) {
throw new Error(
@@ -1,3 +1,12 @@
fragment MarketCandlesFields on Candle {
high
low
open
close
volume
periodStart
}
query MarketsCandles($interval: Interval!, $since: String!) {
marketsConnection {
edges {
@@ -176,7 +176,6 @@ export const NetworkParams = {
market_liquidity_feeCalculationTimeStep:
'market_liquidity_feeCalculationTimeStep',
transfer_fee_factor: 'transfer_fee_factor',
transfer_minTransferQuantumMultiple: 'transfer_minTransferQuantumMultiple',
network_validators_incumbentBonus: 'network_validators_incumbentBonus',
} as const;
@@ -27,9 +27,7 @@ jest.mock('@vegaprotocol/data-provider', () => ({
}));
describe('PositionsManager', () => {
// TODO: temporarily disable close position
// eslint-disable-next-line jest/no-disabled-tests
it.skip('should close position with max uint64', async () => {
it('should close position with max uint64', async () => {
render(<PositionsManager partyIds={['partyId']} isReadOnly={false} />, {
wrapper: MockedProvider,
});
+1 -3
View File
@@ -29,7 +29,6 @@ export const PositionsManager = ({
}: PositionsManagerProps) => {
const { pubKeys, pubKey } = useVegaWallet();
const create = useVegaTransactionStore((store) => store.create);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const onClose = useCallback(
({ marketId, openVolume }: { marketId: string; openVolume: string }) =>
create({
@@ -74,8 +73,7 @@ export const PositionsManager = ({
pubKeys={pubKeys}
rowData={data}
onMarketClick={onMarketClick}
// TODO: temporarily disable close position
// onClose={onClose}
onClose={onClose}
isReadOnly={isReadOnly}
multipleKeys={partyIds.length > 1}
overlayNoRowsTemplate={error ? error.message : t('No positions')}
@@ -29,7 +29,7 @@ export const MarketProposalNotification = ({
</div>
);
return (
<div className="border-default min-w-min border-l pb-1 pl-1 pr-1">
<div className="border-l border-default pl-1 pr-1 pb-1 min-w-min whitespace-nowrap">
<Notification
intent={Intent.Warning}
message={message}
@@ -85,6 +85,7 @@ describe('ProposalsList', () => {
'Settlement asset',
'State',
'Parent market',
'Voting',
'Closing date',
'Enactment date',
'', // actions col
@@ -1,6 +1,8 @@
import { useMemo } from 'react';
import BigNumber from 'bignumber.js';
import type { ColDef } from 'ag-grid-community';
import {
CenteredGridCellWrapper,
COL_DEFS,
DateRangeFilter,
SetFilter,
@@ -9,18 +11,33 @@ import {
import compact from 'lodash/compact';
import { getDateTimeFormat } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import type {
VegaICellRendererParams,
VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import {
ProposalProductTypeShortName,
ProductTypeMapping,
ProductTypeShortName,
ProposalStateMapping,
} from '@vegaprotocol/types';
import type { ProposalListFieldsFragment } from '../../lib/proposals-data-provider/__generated__/Proposals';
import { VoteProgress } from '../voting-progress';
import { ProposalActionsDropdown } from '../proposal-actions-dropdown';
export const useColumnDefs = () => {
const { params } = useNetworkParams([
NetworkParams.governance_proposal_market_requiredMajority,
]);
const requiredMajorityPercentage = useMemo(() => {
const requiredMajority =
params?.governance_proposal_market_requiredMajority ?? 1;
return new BigNumber(requiredMajority).times(100);
}, [params?.governance_proposal_market_requiredMajority]);
const columnDefs: ColDef[] = useMemo(() => {
return compact([
{
@@ -37,38 +54,27 @@ export const useColumnDefs = () => {
}) => {
if (!value || !data) return '-';
const getProductType = (data: ProposalListFieldsFragment) => {
if (
data.terms.__typename === 'ProposalTerms' &&
data.terms.change.__typename === 'NewMarket'
) {
return data.terms.change.instrument.product?.__typename;
}
return undefined;
};
const productType = getProductType(data);
// TODO: update when we switch to ProductConfiguration
const productType = 'Future';
return (
productType && (
<StackedCell
primary={value}
secondary={
<span
title={ProposalProductTypeShortName[productType]}
className="uppercase"
>
{ProposalProductTypeShortName[productType]}
</span>
}
/>
)
<StackedCell
primary={value}
secondary={
<span
title={ProductTypeMapping[productType]}
className="uppercase"
>
{ProductTypeShortName[productType]}
</span>
}
/>
);
},
},
{
colId: 'asset',
headerName: t('Settlement asset'),
field: 'terms.change.instrument.product.settlementAsset.symbol',
field: 'terms.change.instrument.futureProduct.settlementAsset.symbol',
},
{
colId: 'state',
@@ -88,6 +94,32 @@ export const useColumnDefs = () => {
field: 'terms.change.successorConfiguration.parentMarketId',
cellRenderer: 'ParentMarketCell',
},
{
colId: 'voting',
headerName: t('Voting'),
cellRenderer: ({
data,
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
if (data) {
const yesTokens = new BigNumber(data.votes.yes.totalTokens);
const noTokens = new BigNumber(data.votes.no.totalTokens);
const totalTokensVoted = yesTokens.plus(noTokens);
const yesPercentage = totalTokensVoted.isZero()
? new BigNumber(0)
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
return (
<CenteredGridCellWrapper>
<VoteProgress
threshold={requiredMajorityPercentage}
progress={yesPercentage}
/>
</CenteredGridCellWrapper>
);
}
return '-';
},
filter: false,
},
{
colId: 'closing-date',
headerName: t('Closing date'),
@@ -124,7 +156,7 @@ export const useColumnDefs = () => {
},
},
]);
}, []);
}, [requiredMajorityPercentage]);
return columnDefs;
};
@@ -16,90 +16,78 @@ fragment NewMarketFields on NewMarket {
instrument {
name
code
product {
... on FutureProduct {
settlementAsset {
id
name
symbol
decimals
quantum
}
quoteName
dataSourceSpecForSettlementData {
sourceType {
... on DataSourceDefinitionExternal {
sourceType {
... on DataSourceSpecConfiguration {
signers {
signer {
... on PubKey {
key
}
... on ETHAddress {
address
}
futureProduct {
settlementAsset {
id
name
symbol
decimals
quantum
}
quoteName
dataSourceSpecForSettlementData {
sourceType {
... on DataSourceDefinitionExternal {
sourceType {
... on DataSourceSpecConfiguration {
signers {
signer {
... on PubKey {
key
}
... on ETHAddress {
address
}
}
filters {
key {
name
type
}
conditions {
operator
value
}
}
filters {
key {
name
type
}
conditions {
operator
value
}
}
}
}
}
}
dataSourceSpecForTradingTermination {
sourceType {
... on DataSourceDefinitionExternal {
sourceType {
... on DataSourceSpecConfiguration {
signers {
signer {
... on PubKey {
key
}
... on ETHAddress {
address
}
}
}
filters {
key {
name
type
}
conditions {
operator
value
}
}
}
}
}
}
}
dataSourceSpecBinding {
settlementDataProperty
tradingTerminationProperty
}
}
... on PerpetualProduct {
settlementAsset {
id
name
symbol
decimals
quantum
dataSourceSpecForTradingTermination {
sourceType {
... on DataSourceDefinitionExternal {
sourceType {
... on DataSourceSpecConfiguration {
signers {
signer {
... on PubKey {
key
}
... on ETHAddress {
address
}
}
}
filters {
key {
name
type
}
conditions {
operator
value
}
}
}
}
}
}
quoteName
}
dataSourceSpecBinding {
settlementDataProperty
tradingTerminationProperty
}
}
}
File diff suppressed because one or more lines are too long
@@ -128,7 +128,7 @@ export const createProposalListFieldsFragment = (
instrument: {
code: 'ETHUSD',
name: 'ETHUSD',
product: {
futureProduct: {
settlementAsset: {
id: 'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
name: 'tDAI TEST',
@@ -262,7 +262,7 @@ const proposalListFields: ProposalListFieldsFragment[] = [
instrument: {
code: 'ETHUSD',
name: 'ETHUSD',
product: {
futureProduct: {
settlementAsset: {
id: 'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
name: 'tDAI TEST',
@@ -352,7 +352,7 @@ const proposalListFields: ProposalListFieldsFragment[] = [
instrument: {
code: 'LINKUSD',
name: 'LINKUSD',
product: {
futureProduct: {
settlementAsset: {
id: 'eb30d55e90e1f9e5c4727d6fa2a5a8cd36ab9ae9738eb8f3faf53e2bee4861ee',
name: 'mUSDT-II',
@@ -442,7 +442,7 @@ const proposalListFields: ProposalListFieldsFragment[] = [
instrument: {
code: 'LINKUSD',
name: 'LINKUSD',
product: {
futureProduct: {
settlementAsset: {
id: 'eb30d55e90e1f9e5c4727d6fa2a5a8cd36ab9ae9738eb8f3faf53e2bee4861ee',
name: 'mUSDT-II',
@@ -532,7 +532,7 @@ const proposalListFields: ProposalListFieldsFragment[] = [
instrument: {
code: 'ETHUSD',
name: 'ETHUSD',
product: {
futureProduct: {
settlementAsset: {
id: 'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
name: 'tDAI TEST',
@@ -622,7 +622,7 @@ const proposalListFields: ProposalListFieldsFragment[] = [
instrument: {
code: 'LINKUSD',
name: 'LINKUSD',
product: {
futureProduct: {
settlementAsset: {
id: 'eb30d55e90e1f9e5c4727d6fa2a5a8cd36ab9ae9738eb8f3faf53e2bee4861ee',
name: 'mUSDT-II',
@@ -712,7 +712,7 @@ const proposalListFields: ProposalListFieldsFragment[] = [
instrument: {
code: 'ETHDAI.MF21',
name: 'ETHDAI Monthly (Dec 2022)',
product: {
futureProduct: {
settlementAsset: {
id: 'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
name: 'tDAI TEST',
@@ -802,7 +802,7 @@ const proposalListFields: ProposalListFieldsFragment[] = [
instrument: {
code: 'AAPL.MF21',
name: 'Apple Monthly (Dec 2022)',
product: {
futureProduct: {
settlementAsset: {
id: 'c9fe6fc24fce121b2cc72680543a886055abb560043fda394ba5376203b7527d',
name: 'tUSDC TEST',
@@ -892,7 +892,7 @@ const proposalListFields: ProposalListFieldsFragment[] = [
instrument: {
code: 'BTCUSD.MF21',
name: 'BTCUSD Monthly (Dec 2022)',
product: {
futureProduct: {
settlementAsset: {
id: 'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
name: 'tDAI TEST',
@@ -982,7 +982,7 @@ const proposalListFields: ProposalListFieldsFragment[] = [
instrument: {
code: 'TSLA.QM21',
name: 'Tesla Quarterly (Feb 2023)',
product: {
futureProduct: {
settlementAsset: {
id: '177e8f6c25a955bd18475084b99b2b1d37f28f3dec393fab7755a7e69c3d8c3b',
name: 'tEURO TEST',
@@ -1072,7 +1072,7 @@ const proposalListFields: ProposalListFieldsFragment[] = [
instrument: {
code: 'AAVEDAI.MF21',
name: 'AAVEDAI Monthly (Dec 2022)',
product: {
futureProduct: {
settlementAsset: {
id: 'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
name: 'tDAI TEST',
@@ -1162,7 +1162,7 @@ const proposalListFields: ProposalListFieldsFragment[] = [
instrument: {
code: 'ETHBTC.QM21',
name: 'ETHBTC Quarterly (Feb 2023)',
product: {
futureProduct: {
settlementAsset: {
id: 'cee709223217281d7893b650850ae8ee8a18b7539b5658f9b4cc24de95dd18ad',
name: 'tBTC TEST',
@@ -1252,7 +1252,7 @@ const proposalListFields: ProposalListFieldsFragment[] = [
instrument: {
code: 'UNIDAI.MF21',
name: 'UNIDAI Monthly (Dec 2022)',
product: {
futureProduct: {
settlementAsset: {
id: 'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
name: 'tDAI TEST',
+1 -3
View File
@@ -22,13 +22,11 @@ export const CompactNumber = ({
decimals = 'infer',
compactDisplay = 'short',
testId = 'compact-number',
compactAbove = DEFAULT_COMPACT_ABOVE,
}: {
number: BigNumber;
decimals?: number | 'infer';
compactDisplay?: 'short' | 'long';
testId?: string;
compactAbove?: number;
}) => {
if (!number.isFinite()) {
return (
@@ -46,7 +44,7 @@ export const CompactNumber = ({
return <span data-testid={testId}>{INFINITY}</span>;
}
if (number.isLessThan(compactAbove)) {
if (number.isLessThan(DEFAULT_COMPACT_ABOVE)) {
return (
<span data-testid={testId}>{formatNumber(number, decimalPlaces)}</span>
);
+1 -13
View File
@@ -4376,19 +4376,7 @@ export type Query = {
/** The last block process by the blockchain */
lastBlockHeight: Scalars['String'];
/**
* Get a list of ledger entries within the given date range. The date range is restricted to a maximum of 5 days.
* This query requests and sums the number of ledger entries from a given subset of accounts, specified via the 'filter' argument.
* It returns a time series - implemented as a list of AggregateLedgerEntry structs - with a row for every time
* the summed ledger entries of the set of specified accounts changes.
* Each account filter must contain no more than one party ID.
* At least one party ID must be specified in the from or to account filter.
*
* Entries can be filtered by:
* - the sending account (market ID, asset ID, account type)
* - receiving account (market ID, asset ID, account type)
* - sending AND receiving account
* - transfer type either in addition to the above filters or as a standalone option
*
* Get ledger entries by asset, market, party, account type, transfer type within the given date range.
* Note: The date range is restricted to any 5 days.
* If no start or end date is provided, only ledger entries from the last 5 days will be returned.
* If a start and end date are provided, but the end date is more than 5 days after the start date, only data up to 5 days after the start date will be returned.
@@ -93,7 +93,7 @@ export const Notification = ({
</div>
<div
className={classNames(
'flex flex-col items-start overflow-hidden gap-0',
'flex flex-col items-start overflow-hidden gap-0 mt-1',
'text-vega-clight-50 dark:text-vega-cdark-50',
'font-alpha',
{ 'text-sm': size === 'small', 'text-base': size === 'medium' }
@@ -1,72 +0,0 @@
import { renderHook, waitFor } from '@testing-library/react';
import { useVegaWallet } from '../use-vega-wallet';
import { useChainId } from './use-chain-id';
global.fetch = jest.fn();
const mockFetch = global.fetch as jest.Mock;
mockFetch.mockImplementation((url: string) => {
return Promise.resolve({ ok: true });
});
jest.mock('../use-vega-wallet', () => {
const original = jest.requireActual('../use-vega-wallet');
return {
...original,
useVegaWallet: jest.fn(),
};
});
describe('useChainId', () => {
it('does not call fetch when statistics url could not be determined', async () => {
(useVegaWallet as jest.Mock).mockReturnValue({
vegaUrl: '',
});
renderHook(() => useChainId());
await waitFor(() => {
expect(mockFetch).toHaveBeenCalledTimes(0);
});
});
it('calls fetch with correct statistics url', async () => {
(useVegaWallet as jest.Mock).mockReturnValue({
vegaUrl: 'http://localhost:1234/graphql',
});
renderHook(() => useChainId());
await waitFor(() => {
expect(mockFetch).toHaveBeenCalledWith(
'http://localhost:1234/statistics'
);
});
});
it('does not return chain id when chain id is not present in response', async () => {
(useVegaWallet as jest.Mock).mockReturnValue({
vegaUrl: 'http://localhost:1234/graphql',
});
const { result } = renderHook(() => useChainId());
await waitFor(() => {
expect(result.current).toBeUndefined();
});
});
it('returns chain id when chain id is present in response', async () => {
(useVegaWallet as jest.Mock).mockReturnValue({
vegaUrl: 'http://localhost:1234/graphql',
});
mockFetch.mockImplementation(() => {
return Promise.resolve({
ok: true,
json: () => ({
statistics: {
chainId: '1234',
},
}),
});
});
const { result } = renderHook(() => useChainId());
await waitFor(() => {
expect(result.current).not.toBeUndefined();
expect(result.current).toEqual('1234');
});
});
});
+2 -22
View File
@@ -3,37 +3,17 @@ import { useVegaWallet } from '../use-vega-wallet';
const cache: Record<string, string> = {};
/**
* Gets the statistics url for a given vega url.
* Example:
* https://graphql.example.com/graphql -> https://graphql.example.com/statistics
*/
const getNodeStatisticsUrl = (vegaUrl: string) => {
try {
const url = new URL(vegaUrl);
url.pathname = 'statistics';
return url.toString();
} catch (err) {
return undefined;
}
};
export const useChainId = () => {
const { vegaUrl } = useVegaWallet();
const [chainId, setChainId] = useState<undefined | string>(cache[vegaUrl]);
const [fetchAttempt, setFetchAttempt] = useState(1);
const statisticsUrl = getNodeStatisticsUrl(vegaUrl);
useEffect(() => {
// abort when `/statistics` URL could not be determined
if (!statisticsUrl) return;
let isCancelled = false;
if (cache[vegaUrl]) {
setChainId(cache[vegaUrl]);
return;
}
fetch(statisticsUrl)
fetch(vegaUrl.replace('graphql', 'statistics'))
.then((response) => response.json())
.then((response) => {
if (isCancelled) {
@@ -52,6 +32,6 @@ export const useChainId = () => {
return () => {
isCancelled = true;
};
}, [fetchAttempt, statisticsUrl, vegaUrl]);
}, [fetchAttempt, vegaUrl]);
return chainId;
};
+1 -1
View File
@@ -71,7 +71,7 @@
"jsondiffpatch": "^0.4.1",
"lodash": "^4.17.21",
"next": "13.3.0",
"pennant": "^1.14.1",
"pennant": "1.14.0",
"react": "18.2.0",
"react-copy-to-clipboard": "^5.0.4",
"react-dom": "18.2.0",
+4 -4
View File
@@ -20500,10 +20500,10 @@ pend@~1.2.0:
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
pennant@^1.14.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/pennant/-/pennant-1.14.1.tgz#8e7a53256095e398b03397af31c831b02a56032b"
integrity sha512-rjzo/tlFanO96OKhJiyjQtjug7sY6pjVZqVbieD3Tf4zt20bqIb6m4L2JfwOXEe0jqP/yddgCNPY+vlkVuJW1Q==
pennant@1.14.0:
version "1.14.0"
resolved "https://registry.yarnpkg.com/pennant/-/pennant-1.14.0.tgz#4100c25a6d836d6f0ff425181fb6f812f9fe5778"
integrity sha512-9H0zWzFUSbD1BlDXnHFmKwkAxXGb1xTxjkUD+RwaMygtSwPXzQEyk2ScVyMqxdcz0RuJmI5HCVmZTOjdr1NwuA==
dependencies:
"@babel/runtime" "^7.13.10"
"@d3fc/d3fc-technical-indicator" "^8.0.1"