Compare commits

...
25 changed files with 724 additions and 356 deletions
@@ -28,9 +28,10 @@ const headers = [
'Adjusted stake share',
'Share',
'Live supplied liquidity',
'Live time fraction on book',
'Fees accrued this epoch',
'Live time on book',
'Live liquidity quality score (%)',
'Last time fraction on the book',
'Last time on the book',
'Last fee penalty',
'Last bond penalty',
'Status',
+4 -8
View File
@@ -3,17 +3,16 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.sentry.io/6300613
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/fairground/vegawallet-fairground.toml
NX_VEGA_ENV=TESTNET
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml
NX_VEGA_ENV=STAGNET1
NX_VEGA_EXPLORER_URL=https://explorer.stagnet1.vega.rocks
NX_VEGA_NETWORKS={\"MAINNET\":\"https://console.vega.xyz\",\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
NX_VEGA_TOKEN_URL=https://governance.fairground.wtf
NX_VEGA_TOKEN_URL=https://governance.stagnet1.vega.rocks
NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
@@ -26,6 +25,3 @@ NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_REFERRALS=true
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
@@ -27,6 +27,7 @@ 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';
export const ReferralStatistics = () => {
const { pubKey } = useVegaWallet();
@@ -102,9 +103,9 @@ export const Statistics = ({
!isNaN(t.discountFactor) &&
t.discountFactor === discountFactorValue
);
const nextBenefitTierValue =
currentBenefitTierValue &&
benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier - 1);
const nextBenefitTierValue = currentBenefitTierValue
? 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
@@ -189,7 +190,7 @@ export const Statistics = ({
const currentBenefitTierTile = (
<StatTile title={t('Current tier')}>
{currentBenefitTierValue?.tier || '-'}
{currentBenefitTierValue?.tier || 'None'}
</StatTile>
);
const discountFactorTile = (
@@ -204,14 +205,24 @@ export const Statistics = ({
<StatTile title={t('Epochs in set')}>{epochsValue}</StatTile>
);
const nextTierVolumeTile = (
<StatTile title={t('Volume to next tier')}>
<StatTile
title={t(
'Volume to next tier %s',
nextBenefitTierValue?.tier ? `(${nextBenefitTierValue.tier})` : ''
)}
>
{nextBenefitTierVolumeValue <= 0
? '0'
: compactNumFormat.format(nextBenefitTierVolumeValue)}
</StatTile>
);
const nextTierEpochsTile = (
<StatTile title={t('Epochs to next tier')}>
<StatTile
title={t(
'Epochs to next tier %s',
nextBenefitTierValue?.tier ? `(${nextBenefitTierValue.tier})` : ''
)}
>
{nextBenefitTierEpochsValue <= 0 ? '0' : nextBenefitTierEpochsValue}
</StatTile>
);
+2 -2
View File
@@ -1,5 +1,5 @@
import { ToastsContainer, useToasts } from '@vegaprotocol/ui-toolkit';
import { useUpdateNetworkParametersToasts } from '@vegaprotocol/proposals';
import { useProposalToasts } from '@vegaprotocol/proposals';
import { useVegaTransactionToasts } from '@vegaprotocol/web3';
import { useEthereumTransactionToasts } from '@vegaprotocol/web3';
import { useEthereumWithdrawApprovalsToasts } from '@vegaprotocol/web3';
@@ -7,7 +7,7 @@ import { useReadyToWithdrawalToasts } from '@vegaprotocol/withdraws';
import { Links } from '../lib/links';
export const ToastsManager = () => {
useUpdateNetworkParametersToasts();
useProposalToasts();
useVegaTransactionToasts();
useEthereumTransactionToasts();
useEthereumWithdrawApprovalsToasts();
+14 -3
View File
@@ -4,7 +4,7 @@ fragment LiquidityProvisionFields on LiquidityProvision {
id
party {
id
accountsConnection(marketId: $marketId, type: ACCOUNT_TYPE_BOND) {
accountsConnection(marketId: $marketId) {
edges {
node {
type
@@ -22,10 +22,21 @@ fragment LiquidityProvisionFields on LiquidityProvision {
query LiquidityProvisions($marketId: ID!) {
market(id: $marketId) {
liquidityProvisionsConnection(live: true) {
liquiditySLAParameters {
priceRange
commitmentMinTimeFraction
performanceHysteresisEpochs
slaCompetitionFactor
}
liquidityProvisions(live: true) {
edges {
node {
...LiquidityProvisionFields
current {
...LiquidityProvisionFields
}
pending {
...LiquidityProvisionFields
}
}
}
}
@@ -1,10 +1,12 @@
fragment MarketNode on Market {
id
liquidityProvisionsConnection(live: true) {
liquidityProvisions(live: true) {
edges {
node {
commitmentAmount
fee
current {
commitmentAmount
fee
}
}
}
}
+15 -4
View File
@@ -10,7 +10,7 @@ export type LiquidityProvisionsQueryVariables = Types.Exact<{
}>;
export type LiquidityProvisionsQuery = { __typename?: 'Query', market?: { __typename?: 'Market', liquidityProvisionsConnection?: { __typename?: 'LiquidityProvisionsConnection', edges?: Array<{ __typename?: 'LiquidityProvisionsEdge', node: { __typename?: 'LiquidityProvision', id: string, createdAt: any, updatedAt?: any | null, commitmentAmount: string, fee: string, status: Types.LiquidityProvisionStatus, party: { __typename?: 'Party', id: string, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string } } | null> | null } | null } } } | null> | null } | null } | null };
export type LiquidityProvisionsQuery = { __typename?: 'Query', market?: { __typename?: 'Market', liquiditySLAParameters?: { __typename?: 'LiquiditySLAParameters', priceRange: string, commitmentMinTimeFraction: string, performanceHysteresisEpochs: number, slaCompetitionFactor: string } | null, liquidityProvisions?: { __typename?: 'LiquidityProvisionsWithPendingConnection', edges?: Array<{ __typename?: 'LiquidityProvisionWithPendingEdge', node: { __typename?: 'LiquidityProvisionWithPending', current: { __typename?: 'LiquidityProvision', id: string, createdAt: any, updatedAt?: any | null, commitmentAmount: string, fee: string, status: Types.LiquidityProvisionStatus, party: { __typename?: 'Party', id: string, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string } } | null> | null } | null } }, pending?: { __typename?: 'LiquidityProvision', id: string, createdAt: any, updatedAt?: any | null, commitmentAmount: string, fee: string, status: Types.LiquidityProvisionStatus, party: { __typename?: 'Party', id: string, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string } } | null> | null } | null } } | null } } | null> | null } | null } | null };
export type LiquidityProviderFeeShareFieldsFragment = { __typename?: 'LiquidityProviderFeeShare', equityLikeShare: string, averageEntryValuation: string, averageScore: string, virtualStake: string };
@@ -30,7 +30,7 @@ export const LiquidityProvisionFieldsFragmentDoc = gql`
id
party {
id
accountsConnection(marketId: $marketId, type: ACCOUNT_TYPE_BOND) {
accountsConnection(marketId: $marketId) {
edges {
node {
type
@@ -82,10 +82,21 @@ ${LiquidityProviderSLAFieldsFragmentDoc}`;
export const LiquidityProvisionsDocument = gql`
query LiquidityProvisions($marketId: ID!) {
market(id: $marketId) {
liquidityProvisionsConnection(live: true) {
liquiditySLAParameters {
priceRange
commitmentMinTimeFraction
performanceHysteresisEpochs
slaCompetitionFactor
}
liquidityProvisions(live: true) {
edges {
node {
...LiquidityProvisionFields
current {
...LiquidityProvisionFields
}
pending {
...LiquidityProvisionFields
}
}
}
}
+7 -5
View File
@@ -3,21 +3,23 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type MarketNodeFragment = { __typename?: 'Market', id: string, liquidityProvisionsConnection?: { __typename?: 'LiquidityProvisionsConnection', edges?: Array<{ __typename?: 'LiquidityProvisionsEdge', node: { __typename?: 'LiquidityProvision', commitmentAmount: string, fee: string } } | null> | null } | null, data?: { __typename?: 'MarketData', targetStake?: string | null } | null };
export type MarketNodeFragment = { __typename?: 'Market', id: string, liquidityProvisions?: { __typename?: 'LiquidityProvisionsWithPendingConnection', edges?: Array<{ __typename?: 'LiquidityProvisionWithPendingEdge', node: { __typename?: 'LiquidityProvisionWithPending', current: { __typename?: 'LiquidityProvision', commitmentAmount: string, fee: string } } } | null> | null } | null, data?: { __typename?: 'MarketData', targetStake?: string | null } | null };
export type LiquidityProvisionMarketsQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type LiquidityProvisionMarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, liquidityProvisionsConnection?: { __typename?: 'LiquidityProvisionsConnection', edges?: Array<{ __typename?: 'LiquidityProvisionsEdge', node: { __typename?: 'LiquidityProvision', commitmentAmount: string, fee: string } } | null> | null } | null, data?: { __typename?: 'MarketData', targetStake?: string | null } | null } }> } | null };
export type LiquidityProvisionMarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, liquidityProvisions?: { __typename?: 'LiquidityProvisionsWithPendingConnection', edges?: Array<{ __typename?: 'LiquidityProvisionWithPendingEdge', node: { __typename?: 'LiquidityProvisionWithPending', current: { __typename?: 'LiquidityProvision', commitmentAmount: string, fee: string } } } | null> | null } | null, data?: { __typename?: 'MarketData', targetStake?: string | null } | null } }> } | null };
export const MarketNodeFragmentDoc = gql`
fragment MarketNode on Market {
id
liquidityProvisionsConnection(live: true) {
liquidityProvisions(live: true) {
edges {
node {
commitmentAmount
fee
current {
commitmentAmount
fee
}
}
}
}
@@ -1,13 +1,12 @@
import { AccountType } from '@vegaprotocol/types';
import type { LiquidityProvisionFields } from './liquidity-data-provider';
import { getLiquidityProvision } from './liquidity-data-provider';
import type {
LiquidityProviderFieldsFragment,
LiquidityProvisionFieldsFragment,
} from './__generated__/MarketLiquidity';
import type { LiquidityProviderFieldsFragment } from './__generated__/MarketLiquidity';
const input = {
liquidityProvisions: [
{
id: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
party: {
id: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
accountsConnection: {
@@ -31,7 +30,11 @@ const input = {
fee: '0.001',
status: 'STATUS_ACTIVE',
__typename: 'LiquidityProvision',
} as LiquidityProvisionFieldsFragment,
priceRange: '0',
commitmentMinTimeFraction: '0.5',
performanceHysteresisEpochs: 5678,
slaCompetitionFactor: '0',
} as unknown as LiquidityProvisionFields,
],
liquidityProviders: [
{
@@ -49,9 +52,12 @@ const input = {
const result = [
{
__typename: undefined,
balance: '1.8003328918633596575e+22',
balance: 1.8003328918633597e22,
earmarkedFees: 0,
commitmentAmount: '18003328918633596575000',
createdAt: '2022-12-16T09:28:29.071781Z',
commitmentMinTimeFraction: '0.5',
id: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
feeShare: {
equityLikeShare: '1',
__typename: 'LiquidityProviderFeeShare',
@@ -59,6 +65,9 @@ const result = [
},
fee: '0.001',
partyId: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
performanceHysteresisEpochs: 5678,
priceRange: '0',
slaCompetitionFactor: '0',
party: {
__typename: 'Party',
accountsConnection: {
@@ -106,7 +115,9 @@ describe('getLiquidityProvision', () => {
{
__typename: 'LiquidityProvision',
commitmentAmount: '18003328918633596575000',
commitmentMinTimeFraction: '0.5',
createdAt: '2022-12-16T09:28:29.071781Z',
id: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
fee: '0.001',
party: {
__typename: 'Party',
@@ -125,6 +136,9 @@ describe('getLiquidityProvision', () => {
},
id: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
},
performanceHysteresisEpochs: 5678,
priceRange: '0',
slaCompetitionFactor: '0',
status: 'STATUS_ACTIVE',
updatedAt: '2023-01-04T22:13:27.761985Z',
},
@@ -20,20 +20,24 @@ import type {
LiquidityProvisionsQueryVariables,
} from './__generated__/MarketLiquidity';
export type LiquidityProvisionFields = LiquidityProvisionFieldsFragment &
Schema.LiquiditySLAParameters;
export const liquidityProvisionsDataProvider = makeDataProvider<
LiquidityProvisionsQuery,
LiquidityProvisionFieldsFragment[],
LiquidityProvisionFields[],
never,
never,
LiquidityProvisionsQueryVariables
>({
query: LiquidityProvisionsDocument,
getData: (responseData: LiquidityProvisionsQuery | null) => {
return (
responseData?.market?.liquidityProvisionsConnection?.edges?.map(
(e) => e?.node
) ?? []
).filter((n) => !!n) as LiquidityProvisionFieldsFragment[];
return (responseData?.market?.liquidityProvisions?.edges
?.filter((n) => !!n)
.map((e) => ({
...e?.node.current,
...responseData.market?.liquiditySLAParameters,
})) ?? []) as LiquidityProvisionFields[];
},
});
@@ -81,10 +85,7 @@ export const lpAggregatedDataProvider = makeDerivedDataProvider<
}
);
export const matchFilter = (
filter: Filter,
lp: LiquidityProvisionFieldsFragment
) => {
export const matchFilter = (filter: Filter, lp: LiquidityProvisionData) => {
if (filter.partyId && lp.party.id !== filter.partyId) {
return false;
}
@@ -104,15 +105,18 @@ export const matchFilter = (
};
export interface LiquidityProvisionData
extends Omit<LiquidityProvisionFieldsFragment, '__typename'> {
extends Omit<LiquidityProvisionFieldsFragment, '__typename'>,
Partial<LiquidityProviderFieldsFragment>,
Omit<Schema.LiquiditySLAParameters, '__typename'> {
assetDecimalPlaces?: number;
balance?: string;
balance?: number;
averageEntryValuation?: string;
equityLikeShare?: string;
earmarkedFees?: number;
}
export const getLiquidityProvision = (
liquidityProvisions: LiquidityProvisionFieldsFragment[],
liquidityProvisions: LiquidityProvisionFields[],
liquidityProvider: LiquidityProviderFieldsFragment[],
filter?: Filter
): LiquidityProvisionData[] => {
@@ -141,17 +145,29 @@ export const getLiquidityProvision = (
const bondAccounts = accounts?.filter(
(a) => a?.type === Schema.AccountType.ACCOUNT_TYPE_BOND
);
const feeAccounts = accounts?.filter(
(a) => a?.type === Schema.AccountType.ACCOUNT_TYPE_LP_LIQUIDITY_FEES
);
const balance =
bondAccounts
?.reduce(
(acc, a) => acc.plus(new BigNumber(a.balance ?? 0)),
new BigNumber(0)
)
.toString() || '0';
.toNumber() ?? 0;
const earmarkedFees =
feeAccounts
?.reduce(
(acc, a) => acc.plus(new BigNumber(a.balance ?? 0)),
new BigNumber(0)
)
.toNumber() ?? 0;
return {
...lp,
...lpObj,
balance,
earmarkedFees,
__typename: undefined,
};
});
@@ -55,9 +55,10 @@ describe('LiquidityTable', () => {
'Adjusted stake share',
'Share',
'Live supplied liquidity',
'Live time fraction on book',
'Fees accrued this epoch',
'Live time on book',
'Live liquidity quality score (%)',
'Last time fraction on the book',
'Last time on the book',
'Last fee penalty',
'Last bond penalty',
'Status',
+92 -4
View File
@@ -96,6 +96,58 @@ export const LiquidityTable = ({
return `${addDecimalsFormatNumber(newValue, assetDecimalPlaces ?? 0)}`;
};
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) {
lessThanFull =
data.sla &&
new BigNumber(data.sla.currentEpochFractionOfTimeOnBook).isLessThan(
1
);
lessThanMinimum =
data.sla &&
new BigNumber(data.sla.currentEpochFractionOfTimeOnBook).isLessThan(
data.commitmentMinTimeFraction
);
}
if (lessThanMinimum) {
return t(
`This LP's time on the book in the current epoch (%s) is less than the minimum required (%s), so they could lose all fee revenue for this epoch.`,
[
formatNumberPercentage(
new BigNumber(data.sla.currentEpochFractionOfTimeOnBook).times(
100
),
2
),
formatNumberPercentage(
new BigNumber(data.commitmentMinTimeFraction).times(100),
2
),
]
);
}
if (lessThanFull) {
return t(
`This LP's time on the book in the current epoch (%s) is less than 100%, so they could lose some fees to a better performing LP.`,
[
formatNumberPercentage(
new BigNumber(data.sla.currentEpochFractionOfTimeOnBook).times(
100
),
2
),
]
);
}
return addDecimalsFormatNumber(newValue, assetDecimalPlaces ?? 0);
};
const stakeToCcyVolumeQuantumFormatter = ({
value,
}: ValueFormatterParams) => {
@@ -178,7 +230,7 @@ export const LiquidityTable = ({
],
},
{
headerName: t('Live liquidity details'),
headerName: t('Live liquidity data'),
marryChildren: true,
children: [
{
@@ -192,7 +244,41 @@ export const LiquidityTable = ({
tooltipValueGetter: stakeToCcyVolumeFormatter,
},
{
headerName: t(`Live time fraction on book`),
headerName: t('Fees accrued this epoch'),
field: 'earmarkedFees',
type: 'rightAligned',
headerTooltip: t(
`The liquidity fees accrued by each provider, which will be distributed at the end of the epoch after applying any penalties.`
),
valueFormatter: stakeToCcyVolumeQuantumFormatter,
tooltipValueGetter: feesAccruedTooltip,
cellClassRules: {
'text-warning': ({ data }: { data: LiquidityProvisionData }) => {
if (!data.sla) return false;
return (
new BigNumber(
data.sla.currentEpochFractionOfTimeOnBook
).isLessThan(1) &&
new BigNumber(
data.sla.currentEpochFractionOfTimeOnBook
).isGreaterThan(data.commitmentMinTimeFraction)
);
},
'text-red-500': ({ data }: { data: LiquidityProvisionData }) => {
if (!data.sla) return false;
return (
new BigNumber(
data.sla.currentEpochFractionOfTimeOnBook
).isLessThan(data.commitmentMinTimeFraction) &&
new BigNumber(
data.sla.currentEpochFractionOfTimeOnBook
).isGreaterThan(0)
);
},
},
},
{
headerName: t(`Live time on book`),
field: 'sla.currentEpochFractionOfTimeOnBook',
type: 'rightAligned',
headerTooltip: t('Current epoch fraction of time on the book.'),
@@ -212,7 +298,7 @@ export const LiquidityTable = ({
marryChildren: true,
children: [
{
headerName: t(`Last time fraction on the book`),
headerName: t(`Last time on the book`),
field: 'sla.lastEpochFractionOfTimeOnBook',
type: 'rightAligned',
headerTooltip: t('Last epoch fraction of time on the book.'),
@@ -275,7 +361,9 @@ export const LiquidityTable = ({
return (
<AgGrid
overlayNoRowsTemplate={t('No liquidity provisions')}
getRowId={({ data }: { data: LiquidityProvisionData }) => data.id || ''}
getRowId={({ data }: { data: LiquidityProvisionData }) => {
return data.id || '';
}}
tooltipShowDelay={500}
defaultColDef={defaultColDef}
{...props}
+6 -4
View File
@@ -12,12 +12,14 @@ export const liquidityProvisionsQuery = (
): LiquidityProvisionsQuery => {
const defaultResult: LiquidityProvisionsQuery = {
market: {
liquidityProvisionsConnection: {
__typename: 'LiquidityProvisionsConnection',
liquidityProvisions: {
__typename: 'LiquidityProvisionsWithPendingConnection',
edges: liquidityFields.map((node) => {
return {
__typename: 'LiquidityProvisionsEdge',
node,
__typename: 'LiquidityProvisionWithPendingEdge',
node: {
current: node,
},
};
}),
},
@@ -106,10 +106,9 @@ export const getLiquidityForMarket = (
markets: LiquidityProvisionMarket[]
) => {
const liquidity =
markets.find((m) => m.id === marketId)?.liquidityProvisionsConnection
?.edges || [];
markets.find((m) => m.id === marketId)?.liquidityProvisions?.edges || [];
return liquidity.map((l) => l?.node);
return liquidity.map((l) => l?.node.current);
};
export const getTargetStake = (
@@ -1001,7 +1001,7 @@ export const LiquiditySLAParametersInfoPanel = ({
const { params: networkParams } = useNetworkParams([
NetworkParams.market_liquidity_bondPenaltyParameter,
NetworkParams.market_liquidity_nonPerformanceBondPenaltySlope,
NetworkParams.market_liquidity_sla_nonPerformanceBondPenaltySlope,
NetworkParams.market_liquidity_sla_nonPerformanceBondPenaltyMax,
NetworkParams.market_liquidity_maximumLiquidityFeeFactorLevel,
NetworkParams.market_liquidity_stakeToCcyVolume,
@@ -1017,7 +1017,7 @@ export const LiquiditySLAParametersInfoPanel = ({
bondPenaltyParameter:
networkParams['market_liquidity_bondPenaltyParameter'],
nonPerformanceBondPenaltySlope:
networkParams['market_liquidity_nonPerformanceBondPenaltySlope'],
networkParams['market_liquidity_sla_nonPerformanceBondPenaltySlope'],
nonPerformanceBondPenaltyMax:
networkParams['market_liquidity_sla_nonPerformanceBondPenaltyMax'],
maxLiquidityFeeFactorLevel:
@@ -159,8 +159,8 @@ export const NetworkParams = {
'market_liquidity_targetstake_triggering_ratio',
market_liquidity_bondPenaltyParameter:
'market_liquidity_bondPenaltyParameter',
market_liquidity_nonPerformanceBondPenaltySlope:
'market_liquidity_nonPerformanceBondPenaltySlope',
market_liquidity_sla_nonPerformanceBondPenaltySlope:
'market_liquidity_sla_nonPerformanceBondPenaltySlope',
market_liquidity_sla_nonPerformanceBondPenaltyMax:
'market_liquidity_sla_nonPerformanceBondPenaltyMax',
market_liquidity_maximumLiquidityFeeFactorLevel:
@@ -12,10 +12,15 @@ subscription ProposalEvent($partyId: ID!) {
}
}
fragment UpdateNetworkParameterProposal on Proposal {
fragment OnProposalFragment on Proposal {
id
state
datetime
rationale {
title
description
}
rejectionReason
terms {
enactmentDatetime
change {
@@ -26,9 +31,9 @@ fragment UpdateNetworkParameterProposal on Proposal {
}
}
subscription OnUpdateNetworkParameters {
subscription OnProposal {
proposals {
...UpdateNetworkParameterProposal
...OnProposalFragment
}
}
@@ -13,12 +13,12 @@ export type ProposalEventSubscriptionVariables = Types.Exact<{
export type ProposalEventSubscription = { __typename?: 'Subscription', proposals: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null } };
export type UpdateNetworkParameterProposalFragment = { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, datetime: any, terms: { __typename?: 'ProposalTerms', enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } };
export type OnProposalFragmentFragment = { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, terms: { __typename?: 'ProposalTerms', enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } };
export type OnUpdateNetworkParametersSubscriptionVariables = Types.Exact<{ [key: string]: never; }>;
export type OnProposalSubscriptionVariables = Types.Exact<{ [key: string]: never; }>;
export type OnUpdateNetworkParametersSubscription = { __typename?: 'Subscription', proposals: { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, datetime: any, terms: { __typename?: 'ProposalTerms', enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } } };
export type OnProposalSubscription = { __typename?: 'Subscription', proposals: { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, terms: { __typename?: 'ProposalTerms', enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } } };
export type ProposalOfMarketQueryVariables = Types.Exact<{
marketId: Types.Scalars['ID'];
@@ -64,11 +64,16 @@ export const ProposalEventFieldsFragmentDoc = gql`
errorDetails
}
`;
export const UpdateNetworkParameterProposalFragmentDoc = gql`
fragment UpdateNetworkParameterProposal on Proposal {
export const OnProposalFragmentFragmentDoc = gql`
fragment OnProposalFragment on Proposal {
id
state
datetime
rationale {
title
description
}
rejectionReason
terms {
enactmentDatetime
change {
@@ -109,35 +114,35 @@ export function useProposalEventSubscription(baseOptions: Apollo.SubscriptionHoo
}
export type ProposalEventSubscriptionHookResult = ReturnType<typeof useProposalEventSubscription>;
export type ProposalEventSubscriptionResult = Apollo.SubscriptionResult<ProposalEventSubscription>;
export const OnUpdateNetworkParametersDocument = gql`
subscription OnUpdateNetworkParameters {
export const OnProposalDocument = gql`
subscription OnProposal {
proposals {
...UpdateNetworkParameterProposal
...OnProposalFragment
}
}
${UpdateNetworkParameterProposalFragmentDoc}`;
${OnProposalFragmentFragmentDoc}`;
/**
* __useOnUpdateNetworkParametersSubscription__
* __useOnProposalSubscription__
*
* To run a query within a React component, call `useOnUpdateNetworkParametersSubscription` and pass it any options that fit your needs.
* When your component renders, `useOnUpdateNetworkParametersSubscription` returns an object from Apollo Client that contains loading, error, and data properties
* To run a query within a React component, call `useOnProposalSubscription` and pass it any options that fit your needs.
* When your component renders, `useOnProposalSubscription` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useOnUpdateNetworkParametersSubscription({
* const { data, loading, error } = useOnProposalSubscription({
* variables: {
* },
* });
*/
export function useOnUpdateNetworkParametersSubscription(baseOptions?: Apollo.SubscriptionHookOptions<OnUpdateNetworkParametersSubscription, OnUpdateNetworkParametersSubscriptionVariables>) {
export function useOnProposalSubscription(baseOptions?: Apollo.SubscriptionHookOptions<OnProposalSubscription, OnProposalSubscriptionVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useSubscription<OnUpdateNetworkParametersSubscription, OnUpdateNetworkParametersSubscriptionVariables>(OnUpdateNetworkParametersDocument, options);
return Apollo.useSubscription<OnProposalSubscription, OnProposalSubscriptionVariables>(OnProposalDocument, options);
}
export type OnUpdateNetworkParametersSubscriptionHookResult = ReturnType<typeof useOnUpdateNetworkParametersSubscription>;
export type OnUpdateNetworkParametersSubscriptionResult = Apollo.SubscriptionResult<OnUpdateNetworkParametersSubscription>;
export type OnProposalSubscriptionHookResult = ReturnType<typeof useOnProposalSubscription>;
export type OnProposalSubscriptionResult = Apollo.SubscriptionResult<OnProposalSubscription>;
export const ProposalOfMarketDocument = gql`
query ProposalOfMarket($marketId: ID!) {
proposal(id: $marketId) {
@@ -3,7 +3,7 @@ export * from './use-proposal-event';
export * from './use-vega-transaction';
export * from './use-proposal-submit';
export * from './use-update-proposal';
export * from './use-update-network-paramaters-toasts';
export * from './use-proposal-toasts';
export * from './use-successor-market-proposal-details';
export * from './use-new-transfer-proposal-details';
export * from './use-cancel-transfer-proposal-details';
@@ -0,0 +1,273 @@
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import type { ProposalRejectionReason } from '@vegaprotocol/types';
import {
ProposalChangeMapping,
ProposalState,
ProposalStateMapping,
} from '@vegaprotocol/types';
import type { ReactNode } from 'react';
import {
PROPOSAL_STATES_TO_TOAST,
ProposalToastContent,
useProposalToasts,
} from './use-proposal-toasts';
import { useToasts } from '@vegaprotocol/ui-toolkit';
import { waitFor, renderHook, render } from '@testing-library/react';
import {
OnProposalDocument,
type OnProposalFragmentFragment,
type OnProposalSubscription,
} from './__generated__/Proposal';
import sample from 'lodash/sample';
const renderUseProposalToasts = (mocks?: MockedResponse[]) => {
const wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider mocks={mocks}>{children}</MockedProvider>
);
return renderHook(() => useProposalToasts(), { wrapper });
};
type ProposalChange = OnProposalFragmentFragment['terms']['change'];
const NEW_MARKET_CHANGE: ProposalChange = { __typename: 'NewMarket' };
const UPDATE_MARKET_CHANGE: ProposalChange = { __typename: 'UpdateMarket' };
const UPDATE_NETWORK_PARAMETER_CHANGE: ProposalChange = {
__typename: 'UpdateNetworkParameter',
networkParameter: {
__typename: 'NetworkParameter',
key: 'abc.def',
value: '123',
},
};
const NEW_ASSET_CHANGE: ProposalChange = { __typename: 'NewAsset' };
const UPDATE_ASSET_CHANGE: ProposalChange = { __typename: 'UpdateAsset' };
const NEW_FREEFORM_CHANGE: ProposalChange = { __typename: 'NewFreeform' };
const NEW_TRANSFER_CHANGE: ProposalChange = { __typename: 'NewTransfer' };
const CANCEL_TRANSFER_CHANGE: ProposalChange = { __typename: 'CancelTransfer' };
const UPDATE_MARKET_STATE_CHANGE: ProposalChange = {
__typename: 'UpdateMarketState',
};
const NEW_SPOT_MARKET_CHANGE: ProposalChange = { __typename: 'NewSpotMarket' };
const UPDATE_SPOT_MARKET_CHANGE: ProposalChange = {
__typename: 'UpdateSpotMarket',
};
const UPDATE_VOLUME_DISCOUNT_PROGRAM_CHANGE: ProposalChange = {
__typename: 'UpdateVolumeDiscountProgram',
};
const UPDATE_REFERRAL_PROGRAM_CHANGE: ProposalChange = {
__typename: 'UpdateReferralProgram',
};
const GenericToastProposals = [
NEW_MARKET_CHANGE,
UPDATE_MARKET_CHANGE,
NEW_ASSET_CHANGE,
UPDATE_ASSET_CHANGE,
NEW_FREEFORM_CHANGE,
NEW_TRANSFER_CHANGE,
CANCEL_TRANSFER_CHANGE,
UPDATE_MARKET_STATE_CHANGE,
NEW_SPOT_MARKET_CHANGE,
UPDATE_SPOT_MARKET_CHANGE,
UPDATE_VOLUME_DISCOUNT_PROGRAM_CHANGE,
UPDATE_REFERRAL_PROGRAM_CHANGE,
];
const generateProposal = (
title: string,
state: ProposalState = ProposalState.STATE_OPEN,
change: ProposalChange = { __typename: undefined },
rejectionReason: ProposalRejectionReason | null = null
): OnProposalFragmentFragment => ({
__typename: 'Proposal',
id: Math.random().toString(),
datetime: Math.random().toString(),
rationale: {
title,
description: '',
},
rejectionReason,
state,
terms: {
__typename: 'ProposalTerms',
enactmentDatetime: '2022-12-09T14:40:38Z',
change,
},
});
const INITIAL = useToasts.getState();
const clear = () => {
useToasts.setState(INITIAL);
};
describe('useProposalToasts', () => {
beforeEach(clear);
afterAll(clear);
it.each(PROPOSAL_STATES_TO_TOAST)(
'renders toast for %s proposal',
async (state) => {
const mockProposal: MockedResponse<OnProposalSubscription> = {
request: {
query: OnProposalDocument,
},
result: {
data: {
proposals: generateProposal(
'Things to change',
state,
NEW_MARKET_CHANGE
),
},
},
};
const { result } = renderUseProposalToasts([mockProposal]);
expect(result.current.loading).toBe(true);
await waitFor(() => {
expect(result.current.loading).toBe(false);
expect(useToasts.getState().count).toBe(1);
});
}
);
const IGNORE_STATES = Object.keys(ProposalState).filter((state) => {
return !PROPOSAL_STATES_TO_TOAST.includes(state as ProposalState);
}) as ProposalState[];
it.each(IGNORE_STATES)(
'does not render toast for %s proposal',
async (state) => {
const mockFailedProposal: MockedResponse<OnProposalSubscription> = {
request: {
query: OnProposalDocument,
},
result: {
data: {
proposals: generateProposal('Things to change but ignored', state),
},
},
};
const { result } = renderUseProposalToasts([mockFailedProposal]);
expect(result.current.loading).toBe(true);
await waitFor(() => {
expect(result.current.loading).toBe(false);
expect(useToasts.getState().count).toBe(0);
});
}
);
it('does not render toast for empty proposal', async () => {
const error = console.error;
console.error = () => {
/* no op */
};
const mockEmptyProposal: MockedResponse<OnProposalSubscription> = {
request: {
query: OnProposalDocument,
},
result: {
data: {
proposals: undefined as unknown as OnProposalFragmentFragment,
},
},
};
const { result } = renderUseProposalToasts([mockEmptyProposal]);
expect(result.current.loading).toBe(true);
await waitFor(() => {
expect(result.current.loading).toBe(false);
expect(useToasts.getState().count).toBe(0);
});
console.error = error;
});
const allTypes: [
ProposalChange['__typename'],
ProposalChange,
ProposalState?
][] = [...GenericToastProposals, UPDATE_NETWORK_PARAMETER_CHANGE].map(
(ch) => [ch.__typename, ch, sample(PROPOSAL_STATES_TO_TOAST)]
);
it.each(allTypes)(
'renders toast for %s proposal',
async (_, change, state) => {
const proposalData = generateProposal('Things to change', state, change);
const mockProposal: MockedResponse<OnProposalSubscription> = {
request: {
query: OnProposalDocument,
},
result: {
data: {
proposals: proposalData,
},
},
};
const { result } = renderUseProposalToasts([mockProposal]);
expect(result.current.loading).toBe(true);
await waitFor(() => {
expect(result.current.loading).toBe(false);
expect(useToasts.getState().count).toBe(1);
});
}
);
});
describe('ProposalToastContent', () => {
const genericTypes: [
ProposalChange['__typename'],
ProposalChange,
ProposalState?
][] = GenericToastProposals.map((ch) => [
ch.__typename,
ch,
sample(PROPOSAL_STATES_TO_TOAST),
]);
it.each(genericTypes)(
'renders generic toast content for %s',
async (_, change, state) => {
const proposalData = generateProposal('Things to change', state, change);
const { container } = render(
<ProposalToastContent proposal={proposalData} />
);
const title = container.querySelector(
'[data-testid="proposal-toast-title"]'
);
const rationale = container.querySelector(
'[data-testid="proposal-toast-rationale-title"]'
);
const expectedChangeName = change.__typename
? ProposalChangeMapping[change.__typename]
: '';
const expectedState =
ProposalStateMapping[proposalData.state].toLocaleLowerCase();
expect(title).toHaveTextContent(
`${expectedChangeName} proposal ${expectedState}`
);
expect(rationale).toHaveTextContent('Things to change');
}
);
it('renders specific content for UpdateNetworkParameter proposal', () => {
const proposalData = generateProposal(
'Things to change',
ProposalState.STATE_OPEN,
UPDATE_NETWORK_PARAMETER_CHANGE
);
const { container } = render(
<ProposalToastContent proposal={proposalData} />
);
const title = container.querySelector(
'[data-testid="proposal-toast-title"]'
);
const rationale = container.querySelector(
'[data-testid="proposal-toast-rationale-title"]'
);
const param = container.querySelector(
'[data-testid="proposal-toast-network-param"]'
);
expect(title).toHaveTextContent('Update network parameter proposal open');
expect(rationale).toBe(null);
expect(param).toHaveTextContent('Update abc.def to 123');
});
});
@@ -0,0 +1,136 @@
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
import { getDateTimeFormat } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
ProposalChangeMapping,
ProposalRejectionReasonMapping,
ProposalStateMapping,
} from '@vegaprotocol/types';
import { ProposalState } from '@vegaprotocol/types';
import type { Toast } from '@vegaprotocol/ui-toolkit';
import { ToastHeading } from '@vegaprotocol/ui-toolkit';
import { useToasts } from '@vegaprotocol/ui-toolkit';
import { ExternalLink, Intent } from '@vegaprotocol/ui-toolkit';
import { useCallback } from 'react';
import {
useOnProposalSubscription,
type OnProposalFragmentFragment,
} from './__generated__/Proposal';
export const PROPOSAL_STATES_TO_TOAST = [
ProposalState.STATE_DECLINED,
ProposalState.STATE_ENACTED,
ProposalState.STATE_OPEN,
ProposalState.STATE_PASSED,
];
const CLOSE_AFTER = 0;
type Proposal = OnProposalFragmentFragment;
const ProposalDetails = ({ proposal }: { proposal: Proposal }) => {
const change = proposal.terms.change;
switch (change.__typename) {
case 'UpdateNetworkParameter':
return <UpdateNetworkParameterDetails proposal={proposal} />;
default:
// generic details: rationale title and rejection reason if rejected
return (
<>
{proposal.rationale.title ? (
<p data-testid="proposal-toast-rationale-title" className="italic">
{proposal.rationale.title}
</p>
) : null}
{proposal.state === ProposalState.STATE_REJECTED &&
proposal.rejectionReason ? (
<p data-testid="proposal-toast-rejection-reason">
{t('Rejection reason:')}{' '}
{ProposalRejectionReasonMapping[proposal.rejectionReason]}
</p>
) : null}
</>
);
}
};
const UpdateNetworkParameterDetails = ({
proposal,
}: {
proposal: Proposal;
}) => {
const change = proposal.terms.change;
if (change.__typename !== 'UpdateNetworkParameter') return null;
return (
<p data-testid="proposal-toast-network-param" className="italic">
'{t('Update ')}
<span className="break-all">{change.networkParameter.key}</span>
{t(' to ')}
<span>{change.networkParameter.value}</span>'
</p>
);
};
export const ProposalToastContent = ({ proposal }: { proposal: Proposal }) => {
const tokenLink = useLinks(DApp.Governance);
const change = proposal.terms.change;
// Generates toast's title,
// e.g. Update market proposal enacted, New transfer proposal open, ...
const title = t('%s proposal %s', [
change.__typename ? ProposalChangeMapping[change.__typename] : 'Unknown',
ProposalStateMapping[proposal.state].toLowerCase(),
]);
const enactment = Date.parse(proposal.terms.enactmentDatetime);
return (
<div>
<ToastHeading data-testid="proposal-toast-title">{title}</ToastHeading>
<ProposalDetails proposal={proposal} />
{!isNaN(enactment) && (
<p>
{t('Enactment date:')} {getDateTimeFormat().format(enactment)}
</p>
)}
<p>
<ExternalLink
href={tokenLink(TOKEN_PROPOSAL).replace(':id', proposal?.id || '')}
>
{t('View proposal details')}
</ExternalLink>
</p>
</div>
);
};
export const useProposalToasts = () => {
const { setToast, remove } = useToasts((store) => ({
setToast: store.setToast,
remove: store.remove,
}));
const fromProposal = useCallback(
(proposal: Proposal): Toast => {
const id = `proposal-toast-${proposal.id}`;
return {
id,
intent: Intent.Warning,
content: <ProposalToastContent proposal={proposal} />,
onClose: () => {
remove(id);
},
closeAfter: CLOSE_AFTER,
};
},
[remove]
);
return useOnProposalSubscription({
onData: ({ data }) => {
const proposal = data.data?.proposals;
if (!proposal || !proposal.terms.change.__typename) return;
if (PROPOSAL_STATES_TO_TOAST.includes(proposal.state)) {
setToast(fromProposal(proposal));
}
},
});
};
@@ -1,96 +0,0 @@
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
import { getDateTimeFormat } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type { UpdateNetworkParameter } from '@vegaprotocol/types';
import { ProposalStateMapping } from '@vegaprotocol/types';
import { ProposalState } from '@vegaprotocol/types';
import type { Toast } from '@vegaprotocol/ui-toolkit';
import { ToastHeading } from '@vegaprotocol/ui-toolkit';
import { useToasts } from '@vegaprotocol/ui-toolkit';
import { ExternalLink, Intent } from '@vegaprotocol/ui-toolkit';
import { useCallback } from 'react';
import type { UpdateNetworkParameterProposalFragment } from './__generated__/Proposal';
import { useOnUpdateNetworkParametersSubscription } from './__generated__/Proposal';
export const PROPOSAL_STATES_TO_TOAST = [
ProposalState.STATE_DECLINED,
ProposalState.STATE_ENACTED,
ProposalState.STATE_OPEN,
ProposalState.STATE_PASSED,
];
const CLOSE_AFTER = 0;
type Proposal = UpdateNetworkParameterProposalFragment;
const UpdateNetworkParameterToastContent = ({
proposal,
}: {
proposal: Proposal;
}) => {
const tokenLink = useLinks(DApp.Governance);
const change = proposal.terms.change as UpdateNetworkParameter;
const title = t('Network change proposal %s').replace(
'%s',
ProposalStateMapping[proposal.state].toLowerCase()
);
const enactment = Date.parse(proposal.terms.enactmentDatetime);
return (
<div>
<ToastHeading>{title}</ToastHeading>
<p className="italic">
'{t('Update ')}
<span className="break-all">{change.networkParameter.key}</span>
{t(' to ')}
<span>{change.networkParameter.value}</span>'
</p>
{!isNaN(enactment) && (
<p>
{t('Enactment date:')} {getDateTimeFormat().format(enactment)}
</p>
)}
<p>
<ExternalLink
href={tokenLink(TOKEN_PROPOSAL).replace(':id', proposal?.id || '')}
>
{t('View proposal details')}
</ExternalLink>
</p>
</div>
);
};
export const useUpdateNetworkParametersToasts = () => {
const { setToast, remove } = useToasts((store) => ({
setToast: store.setToast,
remove: store.remove,
}));
const fromProposal = useCallback(
(proposal: Proposal): Toast => {
const id = `update-network-param-proposal-${proposal.id}`;
return {
id: `update-network-param-proposal-${proposal.id}`,
intent: Intent.Warning,
content: <UpdateNetworkParameterToastContent proposal={proposal} />,
onClose: () => {
remove(id);
},
closeAfter: CLOSE_AFTER,
};
},
[remove]
);
return useOnUpdateNetworkParametersSubscription({
onData: ({ data }) => {
// note proposals is poorly named, it is actually a single proposal
const proposal = data.data?.proposals;
if (!proposal) return;
if (proposal.terms.change.__typename !== 'UpdateNetworkParameter') return;
// if one of the following states show a toast
if (PROPOSAL_STATES_TO_TOAST.includes(proposal.state)) {
setToast(fromProposal(proposal));
}
},
});
};
@@ -1,168 +0,0 @@
import merge from 'lodash/merge';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import { ProposalState } from '@vegaprotocol/types';
import type { ReactNode } from 'react';
import {
PROPOSAL_STATES_TO_TOAST,
useUpdateNetworkParametersToasts,
} from './use-update-network-paramaters-toasts';
import type {
UpdateNetworkParameterProposalFragment,
OnUpdateNetworkParametersSubscription,
} from './__generated__/Proposal';
import { OnUpdateNetworkParametersDocument } from './__generated__/Proposal';
import { useToasts } from '@vegaprotocol/ui-toolkit';
import { waitFor, renderHook } from '@testing-library/react';
const render = (mocks?: MockedResponse[]) => {
const wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider mocks={mocks}>{children}</MockedProvider>
);
return renderHook(() => useUpdateNetworkParametersToasts(), { wrapper });
};
const generateUpdateNetworkParametersProposal = (
key: string,
value: string,
state: ProposalState = ProposalState.STATE_OPEN
): UpdateNetworkParameterProposalFragment => ({
__typename: 'Proposal',
id: Math.random().toString(),
datetime: Math.random().toString(),
state,
terms: {
__typename: 'ProposalTerms',
enactmentDatetime: '2022-12-09T14:40:38Z',
change: {
__typename: 'UpdateNetworkParameter',
networkParameter: {
__typename: 'NetworkParameter',
key,
value,
},
},
},
});
const INITIAL = useToasts.getState();
const clear = () => {
useToasts.setState(INITIAL);
};
describe('useUpdateNetworkParametersToasts', () => {
beforeEach(clear);
afterAll(clear);
it.each(PROPOSAL_STATES_TO_TOAST)(
'toasts for %s network param proposals',
async (state) => {
const mockOpenProposal: MockedResponse<OnUpdateNetworkParametersSubscription> =
{
request: {
query: OnUpdateNetworkParametersDocument,
},
result: {
data: {
proposals: generateUpdateNetworkParametersProposal(
'abc.def',
'123.456',
state
),
},
},
};
const { result } = render([mockOpenProposal]);
expect(result.current.loading).toBe(true);
await waitFor(() => {
expect(result.current.loading).toBe(false);
expect(useToasts.getState().count).toBe(1);
});
}
);
const IGNORE_STATES = Object.keys(ProposalState).filter((state) => {
return !PROPOSAL_STATES_TO_TOAST.includes(state as ProposalState);
}) as ProposalState[];
it.each(IGNORE_STATES)('does not toast for %s proposals', async (state) => {
const mockFailedProposal: MockedResponse<OnUpdateNetworkParametersSubscription> =
{
request: {
query: OnUpdateNetworkParametersDocument,
},
result: {
data: {
proposals: generateUpdateNetworkParametersProposal(
'abc.def',
'123.456',
state
),
},
},
};
const { result } = render([mockFailedProposal]);
expect(result.current.loading).toBe(true);
await waitFor(() => {
expect(result.current.loading).toBe(false);
expect(useToasts.getState().count).toBe(0);
});
});
it('does not return toast for empty propsal', async () => {
const error = console.error;
console.error = () => {
/* no op */
};
const mockEmptyProposal: MockedResponse<OnUpdateNetworkParametersSubscription> =
{
request: {
query: OnUpdateNetworkParametersDocument,
},
result: {
data: {
proposals:
undefined as unknown as UpdateNetworkParameterProposalFragment,
},
},
};
const { result } = render([mockEmptyProposal]);
expect(result.current.loading).toBe(true);
await waitFor(() => {
expect(result.current.loading).toBe(false);
expect(useToasts.getState().count).toBe(0);
});
console.error = error;
});
it('does not return toast for wrong proposal type', async () => {
const wrongProposalType = merge(
generateUpdateNetworkParametersProposal('a', 'b'),
{
terms: {
change: {
__typename: 'NewMarket',
},
},
}
);
const mockWrongProposalType: MockedResponse<OnUpdateNetworkParametersSubscription> =
{
request: {
query: OnUpdateNetworkParametersDocument,
},
result: {
data: {
proposals: wrongProposalType,
},
},
};
const { result } = render([mockWrongProposalType]);
expect(result.current.loading).toBe(true);
await waitFor(() => {
expect(result.current.loading).toBe(false);
expect(useToasts.getState().count).toBe(0);
});
});
});
+36 -1
View File
@@ -3387,6 +3387,8 @@ export type Party = {
transfersConnection?: Maybe<TransferConnection>;
/** The current reward vesting summary of the party for the last epoch */
vestingBalancesSummary: PartyVestingBalancesSummary;
/** The current statistics about a party's vesting rewards for the last epoch */
vestingStats?: Maybe<PartyVestingStats>;
/** All votes on proposals in the Vega network by the given party */
votesConnection?: Maybe<ProposalVoteConnection>;
/** The list of all withdrawals initiated by the party */
@@ -3502,6 +3504,7 @@ export type PartytradesConnectionArgs = {
/** Represents a party on Vega, could be an ethereum wallet address in the future */
export type PartytransfersConnectionArgs = {
direction?: InputMaybe<TransferDirection>;
isReward?: InputMaybe<Scalars['Boolean']>;
pagination?: InputMaybe<Pagination>;
};
@@ -3616,6 +3619,15 @@ export type PartyVestingBalancesSummary = {
vestingBalances?: Maybe<Array<PartyVestingBalance>>;
};
/** Statistics about a party's vesting rewards */
export type PartyVestingStats = {
__typename?: 'PartyVestingStats';
/** Epoch for which the statistics are valid */
epochSeq: Scalars['Int'];
/** The reward bonus multiplier */
rewardBonusMultiplier: Scalars['String'];
};
/** Create an order linked to an index rather than a price */
export type PeggedOrder = {
__typename?: 'PeggedOrder';
@@ -4905,6 +4917,7 @@ export type QuerytransferArgs = {
/** Queries allow a caller to read data and filter data via GraphQL. */
export type QuerytransfersConnectionArgs = {
direction?: InputMaybe<TransferDirection>;
isReward?: InputMaybe<Scalars['Boolean']>;
pagination?: InputMaybe<Pagination>;
partyId?: InputMaybe<Scalars['ID']>;
};
@@ -5081,6 +5094,8 @@ export type ReferralSetStats = {
rewardsFactorMultiplier: Scalars['String'];
/** The multiplier applied to the referral reward factor when calculating referral rewards due to the referrer. */
rewardsMultiplier: Scalars['String'];
/** Indicates if the referral set was eligible to be part of the referral program. */
wasEligible: Scalars['Boolean'];
};
/** Connection type for retrieving cursor-based paginated referral set statistics information */
@@ -6102,11 +6117,31 @@ export enum TransferDirection {
export type TransferEdge = {
__typename?: 'TransferEdge';
cursor: Scalars['String'];
node: Transfer;
node: TransferNode;
};
/** A transfer fee record */
export type TransferFee = {
__typename?: 'TransferFee';
/** The fee amount */
amount: Scalars['String'];
/** The epoch when this fee was paid */
epoch: Scalars['Int'];
/** Transfer ID of the transfer for which the fee was paid */
transferId: Scalars['ID'];
};
export type TransferKind = OneOffGovernanceTransfer | OneOffTransfer | RecurringGovernanceTransfer | RecurringTransfer;
/** A transfer record with the fee payments associated with the transfer */
export type TransferNode = {
__typename?: 'TransferNode';
/** The list of fee payments made */
fees?: Maybe<Array<Maybe<TransferFee>>>;
/** The transfer record */
transfer: Transfer;
};
export type TransferResponse = {
__typename?: 'TransferResponse';
/** The balances of accounts involved in the transfer */
+24
View File
@@ -3,6 +3,7 @@ import type {
GovernanceTransferKind,
GovernanceTransferType,
PeggedReference,
ProposalChange,
} from './__generated__/types';
import type { AccountType } from './__generated__/types';
import type {
@@ -299,6 +300,29 @@ export const OrderTypeMapping: {
TYPE_NETWORK: 'Network',
};
/**
* Proposal change type mapping
*/
export const ProposalChangeMapping: Record<
NonNullable<ProposalChange['__typename']>,
string
> = {
NewMarket: 'New market',
UpdateMarket: 'Update market',
UpdateNetworkParameter: 'Update network parameter',
NewAsset: 'New asset',
UpdateAsset: 'Update asset',
/* cspell:disable-next-line */
NewFreeform: 'New free-form',
NewTransfer: 'New transfer',
CancelTransfer: 'Cancel transfer',
UpdateMarketState: 'Update market state',
NewSpotMarket: 'New spot market',
UpdateSpotMarket: 'Update spot market',
UpdateVolumeDiscountProgram: 'Update volume discount program',
UpdateReferralProgram: 'Update referral program',
};
/**
* Reason for the proposal being rejected by the core node
*/