Compare commits

..
31 changed files with 889 additions and 181 deletions
+5 -5
View File
@@ -4,7 +4,7 @@ The front-end monorepo provides a toolkit for building apps that interact with V
This repository is managed using [Nx](https://nx.dev).
# 🔎 Applications in this repo
## 🔎 Applications in this repo
### [Block explorer](./apps/explorer)
@@ -30,7 +30,7 @@ Hosting for static content being shared across apps, for example fonts.
The utility dApp for validators wishing to add or remove themselves as a signer of the multisig contract.
# 🧱 Libraries in this repo
## 🧱 Libraries in this repo
### [UI toolkit](./libs/ui-toolkit)
@@ -53,7 +53,7 @@ A utility library for connecting to the Ethereum network and interacting with Ve
Generic react helpers that can be used across multiple applications, along with other utilities.
# 💻 Develop
## 💻 Develop
### Set up
@@ -103,7 +103,7 @@ In CI linting, formatting and also run. These checks can be seen in the [CI work
Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more.
# 🐋 Hosting a console
## 🐋 Hosting a console
To host a console there are two possible build scenarios for running the frontends: nx performed **outside** or **inside** docker build. For specific build instructions follow [build instructions](#build-instructions).
@@ -226,6 +226,6 @@ Note: The script is only needed if capsule was built for first time or fresh. To
vega wallet service run -n DV --load-tokens --tokens-passphrase-file passphrase --no-version-check --automatic-consent --home ~/.vegacapsule/testnet/wallet
```
# 📑 License
## 📑 License
[MIT](./LICENSE)
@@ -64,7 +64,9 @@ export const ProposalSummary = ({
return (
<div className="w-auto max-w-lg border-2 border-solid border-vega-light-100 dark:border-vega-dark-200 p-5">
{id && <ProposalStatusIcon id={id} />}
{rationale?.title && <h1 className="text-xl pb-1">{rationale.title}</h1>}
{rationale?.title && (
<h1 className="text-xl pb-1 break-all">{rationale.title}</h1>
)}
{rationale?.description && (
<div className="pt-2 text-sm leading-tight">
<ReactMarkdown
@@ -0,0 +1,18 @@
query ExplorerTransferStatus($id: ID!) {
transfer(id: $id) {
transfer {
reference
timestamp
status
reason
fromAccountType
from
to
toAccountType
asset {
id
}
amount
}
}
}
@@ -0,0 +1,61 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerTransferStatusQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
export type ExplorerTransferStatusQuery = { __typename?: 'Query', transfer?: { __typename?: 'TransferNode', transfer: { __typename?: 'Transfer', reference?: string | null, timestamp: any, status: Types.TransferStatus, reason?: string | null, fromAccountType: Types.AccountType, from: string, to: string, toAccountType: Types.AccountType, amount: string, asset?: { __typename?: 'Asset', id: string } | null } } | null };
export const ExplorerTransferStatusDocument = gql`
query ExplorerTransferStatus($id: ID!) {
transfer(id: $id) {
transfer {
reference
timestamp
status
reason
fromAccountType
from
to
toAccountType
asset {
id
}
amount
}
}
}
`;
/**
* __useExplorerTransferStatusQuery__
*
* To run a query within a React component, call `useExplorerTransferStatusQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerTransferStatusQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useExplorerTransferStatusQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useExplorerTransferStatusQuery(baseOptions: Apollo.QueryHookOptions<ExplorerTransferStatusQuery, ExplorerTransferStatusQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerTransferStatusQuery, ExplorerTransferStatusQueryVariables>(ExplorerTransferStatusDocument, options);
}
export function useExplorerTransferStatusLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerTransferStatusQuery, ExplorerTransferStatusQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerTransferStatusQuery, ExplorerTransferStatusQueryVariables>(ExplorerTransferStatusDocument, options);
}
export type ExplorerTransferStatusQueryHookResult = ReturnType<typeof useExplorerTransferStatusQuery>;
export type ExplorerTransferStatusLazyQueryHookResult = ReturnType<typeof useExplorerTransferStatusLazyQuery>;
export type ExplorerTransferStatusQueryResult = Apollo.QueryResult<ExplorerTransferStatusQuery, ExplorerTransferStatusQueryVariables>;
@@ -111,7 +111,7 @@ export function TransferParticipants({
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 9"
className="fill-vega-light-100 dark:fill-black"
className="fill-white dark:fill-black"
>
<path d="M0,0L8,9l8,-9Z" />
</svg>
@@ -120,7 +120,7 @@ export function TransferParticipants({
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 9"
className="fill-vega-light-100 dark:fill-vega-dark-200"
className="fill-vega-light-200 dark:fill-vega-dark-200"
>
<path d="M0,0L8,9l8,-9Z" />
</svg>
@@ -1,97 +1,223 @@
import { t } from '@vegaprotocol/i18n';
import { AssetLink, MarketLink } from '../../../../links';
import { headerClasses, wrapperClasses } from '../transfer-details';
import type { components } from '../../../../../../types/explorer';
import type { Recurring } from '../transfer-details';
import { DispatchMetricLabels } from '@vegaprotocol/types';
import {
DispatchMetricLabels,
DistributionStrategy,
} from '@vegaprotocol/types';
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '@vegaprotocol/utils';
export type Metric = components['schemas']['vegaDispatchMetric'];
export type Strategy = components['schemas']['vegaDispatchStrategy'];
export const wrapperClasses = 'border pv-2 w-full flex-auto basis-full';
export const headerClasses =
'bg-solid bg-vega-light-150 dark:bg-vega-dark-150 text-center text-xl py-2 font-alpha calt';
const metricLabels: Record<Metric, string> = {
DISPATCH_METRIC_UNSPECIFIED: 'Unknown metric',
...DispatchMetricLabels,
};
// Maps the two (non-null) values of entityScope to the icon that represents it
const entityScopeIcons: Record<
string,
typeof VegaIconNames[keyof typeof VegaIconNames]
> = {
ENTITY_SCOPE_INDIVIDUALS: VegaIconNames.MAN,
ENTITY_SCOPE_TEAMS: VegaIconNames.TEAM,
};
const distributionStrategyLabel: Record<DistributionStrategy, string> = {
[DistributionStrategy.DISTRIBUTION_STRATEGY_PRO_RATA]: 'Pro Rata',
[DistributionStrategy.DISTRIBUTION_STRATEGY_RANK]: 'Ranked',
};
interface TransferRewardsProps {
recurring: Recurring;
}
/**
* Renderer for a transfer. These can vary quite
* widely, essentially every field can be null.
* Renders recurring transfers/game details in a way that is, perhaps, easy to understand
*
* @param transfer A recurring transfer object
*/
export function TransferRewards({ recurring }: TransferRewardsProps) {
const metric =
recurring?.dispatchStrategy?.metric || 'DISPATCH_METRIC_UNSPECIFIED';
if (!recurring || !recurring.dispatchStrategy) {
return null;
}
// Destructure to make things a bit more readable
const {
entityScope,
individualScope,
teamScope,
distributionStrategy,
lockPeriod,
markets,
stakingRequirement,
windowLength,
notionalTimeWeightedAveragePositionRequirement,
rankTable,
nTopPerformers,
} = recurring.dispatchStrategy;
return (
<div className={wrapperClasses}>
<h2 className={headerClasses}>{t('Reward metrics')}</h2>
<ul className="relative block rounded-lg py-6 text-center p-6">
{recurring.dispatchStrategy.assetForMetric ? (
<h2 className={headerClasses}>{getRewardTitle(entityScope)}</h2>
<ul className="relative block rounded-lg py-6 text-left p-6">
{entityScope && entityScopeIcons[entityScope] ? (
<li>
<strong>{t('Asset')}</strong>:{' '}
<AssetLink assetId={recurring.dispatchStrategy.assetForMetric} />
<strong>{t('Scope')}</strong>:{' '}
<VegaIcon name={entityScopeIcons[entityScope]} />
&nbsp;
{individualScope ? individualScopeLabels[individualScope] : null}
{getScopeLabel(entityScope, teamScope)}
</li>
) : null}
<li>
<strong>{t('Metric')}</strong>: {metricLabels[metric]}
</li>
{recurring.dispatchStrategy.markets &&
recurring.dispatchStrategy.markets.length > 0 ? (
{recurring.dispatchStrategy &&
recurring.dispatchStrategy.assetForMetric && (
<li>
<strong>{t('Asset for metric')}</strong>:{' '}
<AssetLink assetId={recurring.dispatchStrategy.assetForMetric} />
</li>
)}
{recurring.dispatchStrategy.metric &&
metricLabels[recurring.dispatchStrategy.metric] && (
<li>
<strong>{t('Metric')}</strong>:{' '}
{metricLabels[recurring.dispatchStrategy.metric]}
</li>
)}
{lockPeriod && (
<li>
<strong>{t('Reward lock')}</strong>:&nbsp;
{recurring.dispatchStrategy.lockPeriod}{' '}
{recurring.dispatchStrategy.lockPeriod === '1'
? t('epoch')
: t('epochs')}
</li>
)}
{markets && markets.length > 0 ? (
<li>
<strong>{t('Markets in scope')}</strong>:
<ul>
{recurring.dispatchStrategy.markets.map((m) => (
<li key={m}>
<ul className="inline-block ml-1">
{markets.map((m) => (
<li key={m} className="inline-block mr-2">
<MarketLink id={m} />
</li>
))}
</ul>
</li>
) : null}
<li>
<strong>{t('Factor')}</strong>: {recurring.factor}
</li>
{stakingRequirement && stakingRequirement !== '0' ? (
<li>
<strong>{t('Staking requirement')}</strong>: {stakingRequirement}
</li>
) : null}
{windowLength && windowLength !== '0' ? (
<li>
<strong>{t('Window length')}</strong>:{' '}
{recurring.dispatchStrategy.windowLength}{' '}
{recurring.dispatchStrategy.windowLength === '1'
? t('epoch')
: t('epochs')}
</li>
) : null}
{notionalTimeWeightedAveragePositionRequirement &&
notionalTimeWeightedAveragePositionRequirement !== '' ? (
<li>
<strong>{t('Notional TWAP')}</strong>:{' '}
{notionalTimeWeightedAveragePositionRequirement}
</li>
) : null}
{nTopPerformers && (
<li>
<strong>{t('Elligible team members:')}</strong> top{' '}
{`${formatNumber(Number(nTopPerformers) * 100, 0)}%`}
</li>
)}
{distributionStrategy &&
distributionStrategy !== 'DISTRIBUTION_STRATEGY_UNSPECIFIED' && (
<li>
<strong>{t('Distribution strategy')}</strong>:{' '}
{distributionStrategyLabel[distributionStrategy]}
</li>
)}
</ul>
<div className="px-6 pt-1 pb-5">
{rankTable && rankTable.length > 0 ? (
<table className="border-collapse border border-gray-400 ">
<thead>
<tr>
<th className="border border-gray-300 bg-gray-300 px-3">
<strong>{t('Start rank')}</strong>
</th>
<th className="border border-gray-300 bg-gray-300 px-3">
<strong>{t('Share of reward pool')}</strong>
</th>
</tr>
</thead>
<tbody>
{rankTable.map((row, i) => {
return (
<tr key={`rank-${i}`}>
<td className="border border-slate-300 text-center">
{row.startRank}
</td>
<td className="border border-slate-300 text-center">
{row.shareRatio}
</td>
</tr>
);
})}
</tbody>
</table>
) : null}
</div>
</div>
);
}
interface TransferRecurringStrategyProps {
strategy: Strategy;
}
/**
* Simple renderer for a dispatch strategy in a recurring transfer
*
* @param strategy Dispatch strategy object
*/
export function TransferRecurringStrategy({
strategy,
}: TransferRecurringStrategyProps) {
if (!strategy) {
return null;
export function getScopeLabel(
scope: components['schemas']['vegaEntityScope'] | undefined,
teamScope: readonly string[] | undefined
): string {
if (scope === 'ENTITY_SCOPE_TEAMS') {
if (teamScope && teamScope.length !== 0) {
return ` ${teamScope.length} teams`;
} else {
return t('All teams');
}
} else if (scope === 'ENTITY_SCOPE_INDIVIDUALS') {
return t('Individuals');
} else {
return '';
}
return (
<>
{strategy.assetForMetric ? (
<li>
<strong>{t('Asset for metric')}</strong>:{' '}
<AssetLink assetId={strategy.assetForMetric} />
</li>
) : null}
<li>
<strong>{t('Metric')}</strong>: {strategy.metric}
</li>
</>
);
}
export function getRewardTitle(
scope?: components['schemas']['vegaEntityScope']
) {
if (scope === 'ENTITY_SCOPE_TEAMS') {
return t('Game');
}
return t('Reward metrics');
}
const individualScopeLabels: Record<
components['schemas']['vegaIndividualScope'],
string
> = {
// Unspecified and All are not rendered
INDIVIDUAL_SCOPE_UNSPECIFIED: '',
INDIVIDUAL_SCOPE_ALL: '',
INDIVIDUAL_SCOPE_IN_TEAM: '(in team)',
INDIVIDUAL_SCOPE_NOT_IN_TEAM: '(not in team)',
};
@@ -0,0 +1,84 @@
import { t } from '@vegaprotocol/i18n';
import { headerClasses, wrapperClasses } from '../transfer-details';
import { Icon, Loader } from '@vegaprotocol/ui-toolkit';
import type { IconName } from '@vegaprotocol/ui-toolkit';
import type { ApolloError } from '@apollo/client';
import { TransferStatus, TransferStatusMapping } from '@vegaprotocol/types';
import { IconNames } from '@blueprintjs/icons';
interface TransferStatusProps {
status: TransferStatus | undefined;
error: ApolloError | undefined;
loading: boolean;
}
/**
* Renderer for a transfer. These can vary quite
* widely, essentially every field can be null.
*
* @param transfer A recurring transfer object
*/
export function TransferStatusView({ status, loading }: TransferStatusProps) {
if (!status) {
status = TransferStatus.STATUS_PENDING;
}
return (
<div className={wrapperClasses}>
<h2 className={headerClasses}>{t('Status')}</h2>
<div className="relative block rounded-lg py-6 text-center p-6">
{loading ? (
<div className="leading-10 mt-12">
<Loader size={'small'} />
</div>
) : (
<>
<p className="leading-10 my-2">
<Icon
name={getIconForStatus(status)}
className={getColourForStatus(status)}
/>
</p>
<p className="leading-10 my-2">{TransferStatusMapping[status]}</p>
</>
)}
</div>
</div>
);
}
/**
* Simple mapping from status to icon name
* @param status TransferStatus
* @returns IconName
*/
export function getIconForStatus(status: TransferStatus): IconName {
switch (status) {
case TransferStatus.STATUS_PENDING:
return IconNames.TIME;
case TransferStatus.STATUS_DONE:
return IconNames.TICK;
case TransferStatus.STATUS_REJECTED:
return IconNames.CROSS;
default:
return IconNames.TIME;
}
}
/**
* Simple mapping from status to colour
* @param status TransferStatus
* @returns string Tailwind classname
*/
export function getColourForStatus(status: TransferStatus): string {
switch (status) {
case TransferStatus.STATUS_PENDING:
return 'text-yellow-500';
case TransferStatus.STATUS_DONE:
return 'text-green-500';
case TransferStatus.STATUS_REJECTED:
return 'text-red-500';
default:
return 'text-yellow-500';
}
}
@@ -2,12 +2,15 @@ import type { components } from '../../../../../types/explorer';
import { TransferRepeat } from './blocks/transfer-repeat';
import { TransferRewards } from './blocks/transfer-rewards';
import { TransferParticipants } from './blocks/transfer-participants';
import { useExplorerTransferStatusQuery } from './__generated__/Transfer';
import { TransferStatusView } from './blocks/transfer-status';
import { TransferStatus } from '@vegaprotocol/types';
export type Recurring = components['schemas']['commandsv1RecurringTransfer'];
export type Metric = components['schemas']['vegaDispatchMetric'];
export const wrapperClasses =
'border border-vega-light-150 dark:border-vega-dark-200 rounded-md pv-2 mb-5 w-full sm:w-1/4 min-w-[200px] ';
'border border-vega-light-150 dark:border-vega-dark-200 pv-2 w-full sm:w-1/3 basis-1/3';
export const headerClasses =
'bg-solid bg-vega-light-150 dark:bg-vega-dark-150 border-vega-light-150 text-center text-xl py-2 font-alpha calt';
@@ -16,6 +19,7 @@ export type Transfer = components['schemas']['commandsv1Transfer'];
interface TransferDetailsProps {
transfer: Transfer;
from: string;
id: string;
}
/**
@@ -24,13 +28,24 @@ interface TransferDetailsProps {
*
* @param transfer A recurring transfer object
*/
export function TransferDetails({ transfer, from }: TransferDetailsProps) {
export function TransferDetails({ transfer, from, id }: TransferDetailsProps) {
const recurring = transfer.recurring;
// Currently all this is passed in to TransferStatus, but the extra details
// may be useful in the future.
const { data, error, loading } = useExplorerTransferStatusQuery({
variables: { id },
});
const status = error
? TransferStatus.STATUS_REJECTED
: data?.transfer?.transfer.status;
return (
<div className="flex gap-5 flex-wrap">
<div className="flex flex-wrap">
<TransferParticipants from={from} transfer={transfer} />
{recurring ? <TransferRepeat recurring={transfer.recurring} /> : null}
<TransferStatusView status={status} error={error} loading={loading} />
{recurring && recurring.dispatchStrategy ? (
<TransferRewards recurring={transfer.recurring} />
) : null}
@@ -0,0 +1,172 @@
import {
getScopeLabel,
getRewardTitle,
TransferRewards,
} from './blocks/transfer-rewards';
import { render } from '@testing-library/react';
import type { components } from '../../../../../types/explorer';
import type { Recurring } from './transfer-details';
import {
DispatchMetric,
DistributionStrategy,
EntityScope,
IndividualScope,
} from '@vegaprotocol/types';
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
describe('getScopeLabel', () => {
it('should return the correct label for ENTITY_SCOPE_TEAMS with teamScope', () => {
const scope = 'ENTITY_SCOPE_TEAMS';
const teamScope = ['team1', 'team2', 'team3'];
const expectedLabel = ' 3 teams';
const result = getScopeLabel(scope, teamScope);
expect(result).toEqual(expectedLabel);
});
it('should return the correct label for ENTITY_SCOPE_TEAMS without teamScope', () => {
const scope = 'ENTITY_SCOPE_TEAMS';
const teamScope = undefined;
const expectedLabel = 'All teams';
const result = getScopeLabel(scope, teamScope);
expect(result).toEqual(expectedLabel);
});
it('should return the correct label for ENTITY_SCOPE_INDIVIDUALS', () => {
const scope = 'ENTITY_SCOPE_INDIVIDUALS';
const teamScope = undefined;
const expectedLabel = 'Individuals';
const result = getScopeLabel(scope, teamScope);
expect(result).toEqual(expectedLabel);
});
it('should return an empty string for unknown scope', () => {
const scope = 'UNKNOWN_SCOPE';
const teamScope = undefined;
const expectedLabel = '';
const result = getScopeLabel(
scope as unknown as components['schemas']['vegaEntityScope'],
teamScope
);
expect(result).toEqual(expectedLabel);
});
});
describe('getRewardTitle', () => {
it('should return the correct title for ENTITY_SCOPE_TEAMS', () => {
const scope = 'ENTITY_SCOPE_TEAMS';
const expectedTitle = 'Game';
const result = getRewardTitle(scope);
expect(result).toEqual(expectedTitle);
});
it('should return the correct title for other scopes', () => {
const scope = 'ENTITY_SCOPE_INDIVIDUALS';
const expectedTitle = 'Reward metrics';
const result = getRewardTitle(scope);
expect(result).toEqual(expectedTitle);
});
});
describe('TransferRewards', () => {
it('should render nothing if recurring dispatchStrategy is not provided', () => {
const { container } = render(
<TransferRewards recurring={null as unknown as Recurring} />
);
expect(container.firstChild).toBeNull();
});
it('should render nothing if recurring.dispatchStrategy is not provided', () => {
const { container } = render(
<TransferRewards recurring={{} as unknown as Recurring} />
);
expect(container.firstChild).toBeNull();
});
it('should render the reward details correctly', () => {
const recurring = {
dispatchStrategy: {
metric: DispatchMetric.DISPATCH_METRIC_AVERAGE_POSITION,
assetForMetric: '123',
entityScope: EntityScope.ENTITY_SCOPE_TEAMS,
individualScope: IndividualScope.INDIVIDUAL_SCOPE_IN_TEAM,
teamScope: [],
distributionStrategy:
DistributionStrategy.DISTRIBUTION_STRATEGY_PRO_RATA,
lockPeriod: 'lockPeriod',
markets: ['market1', 'market2'],
stakingRequirement: '1',
windowLength: 'windowLength',
notionalTimeWeightedAveragePositionRequirement:
'notionalTimeWeightedAveragePositionRequirement',
rankTable: [
{ startRank: 1, shareRatio: 0.2 },
{ startRank: 2, shareRatio: 0.3 },
],
nTopPerformers: 'nTopPerformers',
},
};
const { getByText } = render(
<MemoryRouter>
<MockedProvider>
<TransferRewards recurring={recurring} />
</MockedProvider>
</MemoryRouter>
);
expect(getByText('Game')).toBeInTheDocument();
expect(getByText('Scope')).toBeInTheDocument();
expect(getByText('Asset for metric')).toBeInTheDocument();
expect(getByText('Metric')).toBeInTheDocument();
expect(getByText('Reward lock')).toBeInTheDocument();
expect(getByText('Markets in scope')).toBeInTheDocument();
expect(getByText('Staking requirement')).toBeInTheDocument();
expect(getByText('Window length')).toBeInTheDocument();
expect(getByText('Notional TWAP')).toBeInTheDocument();
expect(getByText('Elligible team members:')).toBeInTheDocument();
expect(getByText('Distribution strategy')).toBeInTheDocument();
expect(getByText('Start rank')).toBeInTheDocument();
expect(getByText('Share of reward pool')).toBeInTheDocument();
});
it('should not render a rank table if recurring.dispatchStrategy.rankTable is not provided', () => {
const recurring = {
dispatchStrategy: {
entityScope: EntityScope.ENTITY_SCOPE_INDIVIDUALS,
individualScope: IndividualScope.INDIVIDUAL_SCOPE_ALL,
teamScope: ['team1', 'team2', 'team3'],
distributionStrategy:
DistributionStrategy.DISTRIBUTION_STRATEGY_PRO_RATA,
lockPeriod: 'lockPeriod',
markets: ['market1', 'market2'],
stakingRequirement: 'stakingRequirement',
windowLength: 'windowLength',
notionalTimeWeightedAveragePositionRequirement:
'notionalTimeWeightedAveragePositionRequirement',
nTopPerformers: 'nTopPerformers',
},
};
const { container } = render(
<MemoryRouter>
<MockedProvider>
<TransferRewards recurring={recurring} />
</MockedProvider>
</MemoryRouter>
);
expect(container.querySelector('table')).toBeNull();
});
});
@@ -12,6 +12,8 @@ import { ProposalSignatureBundleNewAsset } from './proposal/signature-bundle-new
import { ProposalSignatureBundleUpdateAsset } from './proposal/signature-bundle-update';
import { MarketLink } from '../../links';
import { formatNumber } from '@vegaprotocol/utils';
import { TransferDetails } from './transfer/transfer-details';
import { proposalToTransfer } from '../lib/proposal-to-transfer';
export type Proposal = components['schemas']['v1ProposalSubmission'];
export type ProposalTerms = components['schemas']['vegaProposalTerms'];
@@ -104,6 +106,12 @@ export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => {
? ProposalSignatureBundleNewAsset
: ProposalSignatureBundleUpdateAsset;
let transfer, from;
if (proposal.terms?.newTransfer?.changes) {
transfer = proposalToTransfer(proposal.terms?.newTransfer.changes);
from = proposal.terms.newTransfer.changes.source;
}
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
@@ -149,14 +157,26 @@ export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => {
</>
) : null}
</TableWithTbody>
<ProposalSummary
id={deterministicId}
rationale={proposal.rationale}
terms={proposal?.terms}
/>
{proposalRequiresSignatureBundle(proposal) && (
<SignatureBundleComponent id={deterministicId} tx={tx} />
)}
{transfer && (
<div className="mt-8">
<TransferDetails
transfer={transfer}
from={from || ''}
id={deterministicId}
/>
</div>
)}
</>
);
};
@@ -13,6 +13,7 @@ import {
SPECIAL_CASE_NETWORK_ID,
} from '../../links/party-link/party-link';
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
import Hash from '../../links/hash';
type Transfer = components['schemas']['commandsv1Transfer'];
@@ -60,7 +61,7 @@ export const TxDetailsTransfer = ({
}
const from = txData.submitter;
const id = txSignatureToDeterministicId(txData.signature.value);
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
@@ -71,7 +72,7 @@ export const TxDetailsTransfer = ({
<TableRow modifier="bordered" data-testid="id">
<TableCell {...sharedHeaderProps}>{t('Transfer ID')}</TableCell>
<TableCell>
{txSignatureToDeterministicId(txData.signature.value)}
<Hash text={id} />
</TableCell>
</TableRow>
<TxDetailsShared
@@ -105,7 +106,7 @@ export const TxDetailsTransfer = ({
</TableRow>
) : null}
</TableWithTbody>
<TransferDetails from={from} transfer={transfer} />
<TransferDetails from={from} transfer={transfer} id={id} />
</>
);
};
@@ -0,0 +1,29 @@
import type { components } from '../../../../types/explorer';
type TransferProposal = components['schemas']['vegaNewTransferConfiguration'];
type ActualTransfer = components['schemas']['commandsv1Transfer'];
/**
* Converts a governance proposal for a transfer in to a transfer command that the
* TransferDetails component can then render. The types are very similar, but do not
* map precisely to each other due to some missing fields and some different field
* names.
*
* @param proposal Governance proposal for a transfer
* @returns transfer a Transfer object as if it had been submitted
*/
export function proposalToTransfer(proposal: TransferProposal): ActualTransfer {
return {
amount: proposal.amount,
asset: proposal.asset,
// On a transfer, 'from' is determined by the submitter, so there is no 'from' field
// fromAccountType does exist and is just named differently on the proposal
fromAccountType: proposal.sourceType,
oneOff: proposal.oneOff,
recurring: proposal.recurring,
// There is no reference applied on governance initiated transfers
reference: '',
to: proposal.destination,
toAccountType: proposal.destinationType,
};
}
@@ -19,6 +19,7 @@ import {
useFundingRate,
useMarketTradingMode,
useExternalTwap,
getQuoteName,
} from '@vegaprotocol/markets';
import { MarketState as State } from '@vegaprotocol/types';
import { HeaderStat } from '../../components/header';
@@ -41,6 +42,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
const asset = getAsset(market);
const quoteUnit = getQuoteName(market);
return (
<>
@@ -60,6 +62,8 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
<Last24hVolume
marketId={market.id}
positionDecimalPlaces={market.positionDecimalPlaces}
marketDecimals={market.decimalPlaces}
quoteUnit={quoteUnit}
/>
</HeaderStat>
<HeaderStatMarketTradingMode
@@ -5,7 +5,7 @@ import {
useDataGridEvents,
} from '@vegaprotocol/datagrid';
import type { MarketMaybeWithData } from '@vegaprotocol/markets';
import { useColumnDefs } from './use-column-defs';
import { useMarketsColumnDefs } from './use-column-defs';
import type { DataGridStore } from '../../stores/datagrid-store-slice';
import { type StateCreator, create } from 'zustand';
import { persist } from 'zustand/middleware';
@@ -50,7 +50,7 @@ export const useMarketsStore = create<DataGridSlice>()(
);
export const MarketListTable = (props: Props) => {
const columnDefs = useColumnDefs();
const columnDefs = useMarketsColumnDefs();
const gridStore = useMarketsStore((store) => store.gridStore);
const updateGridStore = useMarketsStore((store) => store.updateGridStore);
@@ -7,21 +7,31 @@ import type {
} from '@vegaprotocol/datagrid';
import { COL_DEFS, SetFilter } from '@vegaprotocol/datagrid';
import * as Schema from '@vegaprotocol/types';
import { addDecimalsFormatNumber, toBigNum } from '@vegaprotocol/utils';
import {
addDecimalsFormatNumber,
formatNumber,
toBigNum,
} from '@vegaprotocol/utils';
import { ButtonLink, Tooltip } from '@vegaprotocol/ui-toolkit';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import type {
MarketFieldsFragment,
MarketMaybeWithData,
MarketMaybeWithDataAndCandles,
} from '@vegaprotocol/markets';
import { MarketActionsDropdown } from './market-table-actions';
import { calcCandleVolume, getAsset } from '@vegaprotocol/markets';
import {
calcCandleVolume,
calcCandleVolumePrice,
getAsset,
getQuoteName,
} from '@vegaprotocol/markets';
import { MarketCodeCell } from './market-code-cell';
import { useT } from '../../lib/use-t';
const { MarketTradingMode, AuctionTrigger } = Schema;
export const useColumnDefs = () => {
export const useMarketsColumnDefs = () => {
const t = useT();
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
return useMemo<ColDef[]>(
@@ -158,11 +168,25 @@ export const useColumnDefs = () => {
}: ValueFormatterParams<MarketMaybeWithDataAndCandles, 'candles'>) => {
const candles = data?.candles;
const vol = candles ? calcCandleVolume(candles) : '0';
const quoteName = getQuoteName(data as MarketFieldsFragment);
const volPrice =
candles &&
calcCandleVolumePrice(
candles,
data.decimalPlaces,
data.positionDecimalPlaces
);
const volume =
data && vol && vol !== '0'
? addDecimalsFormatNumber(vol, data.positionDecimalPlaces)
: '0.00';
return volume;
const volumePrice =
volPrice && formatNumber(volPrice, data?.decimalPlaces);
return volumePrice
? `${volume} (${volumePrice} ${quoteName})`
: volume;
},
},
{
@@ -1,4 +1,4 @@
import { formatNumber } from '@vegaprotocol/utils';
import { getNumberFormat } from '@vegaprotocol/utils';
import sortBy from 'lodash/sortBy';
import omit from 'lodash/omit';
import { useReferralProgramQuery } from './__generated__/CurrentReferralProgram';
@@ -107,7 +107,9 @@ export const useReferralProgram = () => {
discountFactor: Number(t.referralDiscountFactor),
discount: BigNumber(t.referralDiscountFactor).times(100).toFixed(2) + '%',
minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
volume: formatNumber(t.minimumRunningNotionalTakerVolume, 0),
volume: getNumberFormat(0).format(
Number(t.minimumRunningNotionalTakerVolume)
),
epochs: Number(t.minimumEpochs),
};
});
@@ -14,9 +14,9 @@ import {
import { useVegaWallet } from '@vegaprotocol/wallet';
import {
addDecimalsFormatNumber,
formatNumber,
getDateFormat,
getDateTimeFormat,
getNumberFormat,
getUserLocale,
removePaginationWrapper,
} from '@vegaprotocol/utils';
@@ -323,7 +323,7 @@ export const Statistics = ({
}
description={<QUSDTooltip />}
>
{formatNumber(totalCommissionValue, 0)}
{getNumberFormat(0).format(Number(totalCommissionValue))}
</StatTile>
);
@@ -563,8 +563,8 @@ export const RefereesTable = ({
)
.map((r) => ({
...r,
volume: formatNumber(r.volume, 0),
commission: formatNumber(r.commission, 0),
volume: getNumberFormat(0).format(r.volume),
commission: getNumberFormat(0).format(r.commission),
}))
.reverse()}
/>
@@ -1,6 +1,6 @@
import { Link } from 'react-router-dom';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '@vegaprotocol/utils';
import { getNumberFormat } from '@vegaprotocol/utils';
import { type useTeams } from '../../lib/hooks/use-teams';
import { useT } from '../../lib/use-t';
import { Table } from '../table';
@@ -15,7 +15,8 @@ export const CompetitionsLeaderboard = ({
}) => {
const t = useT();
const num = (n?: number | string) => (!n ? '-' : formatNumber(n, 0));
const num = (n?: number | string) =>
!n ? '-' : getNumberFormat(0).format(Number(n));
if (!data || data.length === 0) {
return <Splash>{t('Could not find any teams')}</Splash>;
@@ -81,6 +81,7 @@ export const Settings = () => {
intent={Intent.Primary}
onClick={() => {
localStorage.clear();
sessionStorage.clear();
window.location.reload();
}}
>
@@ -37,7 +37,7 @@ def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page):
# 6002-MDET-004
expect(page.get_by_test_id("market-change")).to_have_text("Change (24h)0.00%0.00")
# 6002-MDET-005
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)-")
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)- (- BTC)")
# 6002-MDET-008
expect(page.get_by_test_id("market-settlement-asset")).to_have_text(
"Settlement assettDAI"
+6 -6
View File
@@ -94,7 +94,7 @@ export const LiquidityTable = ({
return `${addDecimalsFormatNumberQuantum(
value,
assetDecimalPlaces ?? 0,
quantum ?? 1
quantum ?? 0
)}`;
};
@@ -165,7 +165,7 @@ export const LiquidityTable = ({
return `${addDecimalsFormatNumberQuantum(
newValue,
assetDecimalPlaces ?? 0,
quantum ?? 1
quantum ?? 0
)}`;
};
@@ -227,7 +227,7 @@ export const LiquidityTable = ({
addDecimalsFormatNumberQuantum(
pendingCommitmentAmount,
assetDecimalPlaces ?? 0,
quantum ?? 1
quantum ?? 0
);
if (
@@ -238,7 +238,7 @@ export const LiquidityTable = ({
addDecimalsFormatNumberQuantum(
currentCommitmentAmount,
assetDecimalPlaces ?? 0,
quantum ?? 1
quantum ?? 0
);
return (
@@ -286,7 +286,7 @@ export const LiquidityTable = ({
addDecimalsFormatNumberQuantum(
pendingCommitmentAmount,
assetDecimalPlaces ?? 0,
quantum ?? 1
quantum ?? 0
);
if (
@@ -297,7 +297,7 @@ export const LiquidityTable = ({
addDecimalsFormatNumberQuantum(
currentCommitmentAmount,
assetDecimalPlaces ?? 0,
quantum ?? 1
quantum ?? 0
);
return (
+4 -3
View File
@@ -1,7 +1,7 @@
import { DepthChart } from 'pennant';
import throttle from 'lodash/throttle';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { addDecimal, formatNumber } from '@vegaprotocol/utils';
import { addDecimal, getNumberFormat } from '@vegaprotocol/utils';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { marketDepthProvider } from './market-depth-provider';
@@ -216,12 +216,13 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
const volumeFormat = useCallback(
(volume: number) =>
formatNumber(volume, market?.positionDecimalPlaces || 0),
getNumberFormat(market?.positionDecimalPlaces || 0).format(volume),
[market?.positionDecimalPlaces]
);
const priceFormat = useCallback(
(price: number) => formatNumber(price, market?.decimalPlaces || 0),
(price: number) =>
getNumberFormat(market?.decimalPlaces || 0).format(price),
[market?.decimalPlaces]
);
@@ -1,5 +1,9 @@
import { calcCandleVolume } from '../../market-utils';
import { addDecimalsFormatNumber, isNumeric } from '@vegaprotocol/utils';
import { calcCandleVolume, calcCandleVolumePrice } from '../../market-utils';
import {
addDecimalsFormatNumber,
formatNumber,
isNumeric,
} from '@vegaprotocol/utils';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import { useCandles } from '../../hooks';
import { useT } from '../../use-t';
@@ -9,13 +13,17 @@ interface Props {
positionDecimalPlaces?: number;
formatDecimals?: number;
initialValue?: string;
marketDecimals?: number;
quoteUnit?: string;
}
export const Last24hVolume = ({
marketId,
marketDecimals,
positionDecimalPlaces,
formatDecimals,
initialValue,
quoteUnit,
}: Props) => {
const t = useT();
const { oneDayCandles, fiveDaysCandles } = useCandles({
@@ -28,6 +36,11 @@ export const Last24hVolume = ({
(!oneDayCandles || oneDayCandles?.length === 0)
) {
const candleVolume = calcCandleVolume(fiveDaysCandles);
const candleVolumePrice = calcCandleVolumePrice(
fiveDaysCandles,
marketDecimals,
positionDecimalPlaces
);
const candleVolumeValue =
candleVolume && isNumeric(positionDecimalPlaces)
? addDecimalsFormatNumber(
@@ -42,8 +55,8 @@ export const Last24hVolume = ({
<div>
<span className="flex flex-col">
{t(
'24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}}',
{ candleVolumeValue }
'24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}} ({{candleVolumePrice}} {{quoteUnit}})',
{ candleVolumeValue, candleVolumePrice, quoteUnit }
)}
</span>
</div>
@@ -57,10 +70,18 @@ export const Last24hVolume = ({
? calcCandleVolume(oneDayCandles)
: initialValue;
const candleVolumePrice = oneDayCandles
? calcCandleVolumePrice(
oneDayCandles,
marketDecimals,
positionDecimalPlaces
)
: initialValue;
return (
<Tooltip
description={t(
'The total number of contracts traded in the last 24 hours.'
'The total number of contracts traded in the last 24 hours. (Total value of contracts traded in the last 24 hours)'
)}
>
<span>
@@ -70,7 +91,12 @@ export const Last24hVolume = ({
positionDecimalPlaces,
formatDecimals
)
: '-'}
: '-'}{' '}
(
{candleVolumePrice && isNumeric(positionDecimalPlaces)
? formatNumber(candleVolumePrice, formatDecimals)
: '-'}{' '}
{quoteUnit})
</span>
</Tooltip>
);
@@ -155,6 +155,7 @@ export const MarketVolumeInfoPanel = ({ market }: MarketInfoProps) => {
<Last24hVolume
marketId={market.id}
positionDecimalPlaces={market.positionDecimalPlaces}
marketDecimals={market.decimalPlaces}
/>
),
openInterest: dash(data?.openInterest),
@@ -1,6 +1,7 @@
import * as Schema from '@vegaprotocol/types';
import type { Market, MarketMaybeWithDataAndCandles } from './markets-provider';
import {
calcCandleVolumePrice,
calcTradedFactor,
filterAndSortMarkets,
sumFeesFactors,
@@ -145,3 +146,31 @@ describe('sumFeesFactors', () => {
).toEqual(0.6);
});
});
describe('calcCandleVolumePrice', () => {
it('calculates the volume price', () => {
const candles = [
{
volume: '1000',
high: '100',
low: '10',
open: '15',
close: '90',
periodStart: '2022-05-18T13:08:27.693537312Z',
},
{
volume: '1000',
high: '100',
low: '10',
open: '15',
close: '90',
periodStart: '2022-05-18T14:08:27.693537312Z',
},
];
const marketDecimals = 3;
const positionDecimalPlaces = 2;
expect(
calcCandleVolumePrice(candles, marketDecimals, positionDecimalPlaces)
).toEqual('2');
});
});
+46 -1
View File
@@ -1,4 +1,8 @@
import { formatNumberPercentage, toBigNum } from '@vegaprotocol/utils';
import {
addDecimal,
formatNumberPercentage,
toBigNum,
} from '@vegaprotocol/utils';
import { MarketState, MarketTradingMode } from '@vegaprotocol/types';
import BigNumber from 'bignumber.js';
import orderBy from 'lodash/orderBy';
@@ -147,10 +151,51 @@ export const calcCandleHigh = (candles: Candle[]): string | undefined => {
.toString();
};
/**
* The total number of contracts traded in the last 24 hours.
*
* @param candles
* @returns the volume of a given set of candles
*/
export const calcCandleVolume = (candles: Candle[]): string | undefined =>
candles &&
candles.reduce((acc, c) => new BigNumber(acc).plus(c.volume).toString(), '0');
/**
* The total number of contracts traded in the last 24 hours. (Total value of contracts traded in the last 24 hours)
* The volume is calculated as the sum of the product of the volume and the high price of each candle.
* The result is formatted using positionDecimalPlaces to account for the position size.
* The result is formatted using marketDecimals to account for the market precision.
*
* @param candles
* @param marketDecimals
* @param positionDecimalPlaces
* @returns the volume (in quote price) of a given set of candles
*/
export const calcCandleVolumePrice = (
candles: Candle[],
marketDecimals: number = 1,
positionDecimalPlaces: number = 1
): string | undefined =>
candles &&
candles.reduce(
(acc, c) =>
new BigNumber(acc)
.plus(
BigNumber(addDecimal(c.volume, positionDecimalPlaces)).times(
addDecimal(c.high, marketDecimals)
)
)
.toString(),
'0'
);
/**
* Calculates the traded factor of a given market.
*
* @param m
* @returns
*/
export const calcTradedFactor = (m: MarketMaybeWithDataAndCandles) => {
const volume = Number(calcCandleVolume(m.candles || []) || 0);
const price = m.data?.markPrice ? Number(m.data.markPrice) : 0;
+58 -6
View File
@@ -14,6 +14,32 @@ export type Scalars = {
Timestamp: any;
};
/** Margins for a hypothetical position not related to any existing party */
export type AbstractMarginLevels = {
__typename?: 'AbstractMarginLevels';
/** Asset for the current margins */
asset: Asset;
/**
* If the margin of the party is greater than this level, then collateral will be released from the margin account into
* the general account of the party for the given asset.
*/
collateralReleaseLevel: Scalars['String'];
/** This is the minimum margin required for a party to place a new order on the network, expressed as unsigned integer */
initialLevel: Scalars['String'];
/** Minimal margin for the position to be maintained in the network (unsigned integer) */
maintenanceLevel: Scalars['String'];
/** Margin factor, only relevant for isolated margin mode, else 0 */
marginFactor: Scalars['String'];
/** Margin mode of the party, cross margin or isolated margin */
marginMode: MarginMode;
/** Market in which the margin is required for this party */
market: Market;
/** When in isolated margin, the required order margin level, otherwise, 0 */
orderMarginLevel: Scalars['String'];
/** If the margin is between maintenance and search, the network will initiate a collateral search, expressed as unsigned integer */
searchLevel: Scalars['String'];
};
/** An account record */
export type AccountBalance = {
__typename?: 'AccountBalance';
@@ -359,6 +385,8 @@ export enum AuctionTrigger {
export type BatchProposal = {
__typename?: 'BatchProposal';
/** Terms of all the proposals in the batch */
batchTerms?: Maybe<BatchProposalTerms>;
/** RFC3339Nano time and date when the proposal reached the network */
datetime: Scalars['Timestamp'];
/** Details of the rejection reason */
@@ -389,10 +417,10 @@ export type BatchProposal = {
votes: ProposalVotes;
};
/** The rationale for the proposal */
/** The terms for the batch proposal */
export type BatchProposalTerms = {
__typename?: 'BatchProposalTerms';
/** Actual changes being introduced by the proposal - actions the proposal triggers if passed and enacted. */
/** Actual changes being introduced by the batch proposal - actions the proposal triggers if passed and enacted. */
changes: Array<Maybe<BatchProposalTermsChange>>;
/**
* RFC3339Nano time and date when voting closes for this proposal.
@@ -531,6 +559,22 @@ export type CompositePriceConfiguration = {
decayWeight: Scalars['String'];
};
export type CompositePriceSource = {
__typename?: 'CompositePriceSource';
/** The source of the price */
PriceSource: Scalars['String'];
/** The last time the price source was updated in RFC3339Nano */
lastUpdated: Scalars['Timestamp'];
/** The current value of the composite source price */
price: Scalars['String'];
};
export type CompositePriceState = {
__typename?: 'CompositePriceState';
/** Underlying state of the composite price */
priceSources?: Maybe<Array<CompositePriceSource>>;
};
export enum CompositePriceType {
/** Composite price is set to the last trade (legacy) */
COMPOSITE_PRICE_TYPE_LAST_TRADE = 'COMPOSITE_PRICE_TYPE_LAST_TRADE',
@@ -2165,9 +2209,9 @@ export type MarginEdge = {
export type MarginEstimate = {
__typename?: 'MarginEstimate';
/** Margin level estimate assuming no slippage */
bestCase: MarginLevels;
bestCase: AbstractMarginLevels;
/** Margin level estimate assuming slippage cap is applied */
worstCase: MarginLevels;
worstCase: AbstractMarginLevels;
};
/** Margins for a given a party */
@@ -2439,6 +2483,8 @@ export type MarketData = {
liquidityProviderSla?: Maybe<Array<LiquidityProviderSLA>>;
/** The mark price (an unsigned integer) */
markPrice: Scalars['String'];
/** State of the underlying internal composite price */
markPriceState?: Maybe<CompositePriceState>;
/** The methodology used for the calculation of the mark price */
markPriceType: CompositePriceType;
/** Market of the associated mark price */
@@ -3053,6 +3099,8 @@ export type ObservableMarketData = {
liquidityProviderSla?: Maybe<Array<ObservableLiquidityProviderSLA>>;
/** The mark price (an unsigned integer) */
markPrice: Scalars['String'];
/** State of the underlying internal composite price */
markPriceState?: Maybe<CompositePriceState>;
/** The methodology used to calculated mark price */
markPriceType: CompositePriceType;
/** The market growth factor for the last market time window */
@@ -4021,6 +4069,8 @@ export type PerpetualData = {
fundingRate?: Maybe<Scalars['String']>;
/** Internal composite price used as input to the internal VWAP */
internalCompositePrice: Scalars['String'];
/** The internal state of the underlying internal composite price */
internalCompositePriceState?: Maybe<CompositePriceState>;
/** The methodology used to calculated internal composite price for perpetual markets */
internalCompositePriceType: CompositePriceType;
/** Time-weighted average price calculated from data points for this period from the internal data source. */
@@ -4031,6 +4081,8 @@ export type PerpetualData = {
seqNum: Scalars['Int'];
/** Time at which the funding period started */
startTime: Scalars['Timestamp'];
/** The last value from the external oracle */
underlyingIndexPrice: Scalars['String'];
};
export type PerpetualProduct = {
@@ -4328,7 +4380,7 @@ export type ProposalDetail = {
__typename?: 'ProposalDetail';
/** Batch proposal ID that is provided by Vega once proposal reaches the network */
batchId?: Maybe<Scalars['ID']>;
/** Terms of the proposal for a batch proposal */
/** Terms of all the proposals in the batch */
batchTerms?: Maybe<BatchProposalTerms>;
/** RFC3339Nano time and date when the proposal reached the Vega network */
datetime: Scalars['Timestamp'];
@@ -4354,7 +4406,7 @@ export type ProposalDetail = {
requiredParticipation: Scalars['String'];
/** State of the proposal */
state: ProposalState;
/** Terms of the proposal for proposal */
/** Terms of the proposal */
terms?: Maybe<ProposalTerms>;
};
+21 -5
View File
@@ -23,7 +23,6 @@ describe('number utils', () => {
{ v: new BigNumber(123000), d: 1, o: '12,300.0' },
{ v: new BigNumber(123001), d: 2, o: '1,230.01' },
{ v: new BigNumber(123001000), d: 2, o: '1,230,010.00' },
{ v: '100000000000000000001', d: 18, o: '100.000000000000000001' },
])(
'formats with addDecimalsFormatNumber given number correctly',
({ v, d, o }) => {
@@ -32,10 +31,27 @@ describe('number utils', () => {
);
it.each([
{ v: '1234000000000000000', d: 18, q: '1000000000000000000', o: '1.23' }, //vega
{ v: '1235000000000000000', d: 18, q: '1000000000000000000', o: '1.24' }, //vega
{ v: '1230012', d: 6, q: '1000000', o: '1.23' }, // USDT
{ v: '1234560000000000000', d: 18, q: '500000000000000', o: '1.2346' }, // WEth
{ v: new BigNumber(123000), d: 5, o: '1.23', q: 0.1 },
{ v: new BigNumber(123000), d: 3, o: '123.00', q: 0.1 },
{ v: new BigNumber(123000), d: 1, o: '12,300.00', q: 0.1 },
{ v: new BigNumber(123001000), d: 2, o: '1,230,010.00', q: 0.1 },
{ v: new BigNumber(123001), d: 2, o: '1,230.01', q: 100 },
{ v: new BigNumber(123001), d: 2, o: '1,230.01', q: 0.1 },
{ v: new BigNumber(123001), d: 2, o: '1,230.01', q: 1 },
{
v: BigNumber('123456789123456789'),
d: 10,
o: '12,345,678.91234568',
q: '0.00003846',
},
{
v: BigNumber('123456789123456789'),
d: 10,
o: '12,345,678.91234568',
q: '1',
},
// USDT / USDC
{ v: new BigNumber(12345678), d: 6, o: '12.35', q: 1000000 },
])(
'formats with addDecimalsFormatNumberQuantum given number correctly',
({ v, d, o, q }) => {
+36 -60
View File
@@ -1,4 +1,5 @@
import { BigNumber } from 'bignumber.js';
import isNil from 'lodash/isNil';
import memoize from 'lodash/memoize';
import { getUserLocale } from '../get-user-locale';
@@ -52,36 +53,36 @@ export function removeDecimal(
return new BigNumber(value || 0).times(times).toFixed(0);
}
export const getDecimalSeparator = memoize(
() =>
new Intl.NumberFormat(getUserLocale())
.formatToParts(1.1)
.find((part) => part.type === 'decimal')?.value ?? '.'
);
export const getGroupFormat = memoize(() => {
const parts = new Intl.NumberFormat(getUserLocale()).formatToParts(
100000000000.1
);
const groupSeparator = parts.find((part) => part.type === 'group')?.value;
const groupSize =
(groupSeparator &&
parts.reverse().find((part) => part.type === 'integer')?.value.length) ||
0;
return {
groupSize,
groupSeparator,
};
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
export const getNumberFormat = memoize((digits: number) => {
if (isNil(digits) || digits < 0) {
return new Intl.NumberFormat(getUserLocale());
}
return new Intl.NumberFormat(getUserLocale(), {
minimumFractionDigits: Math.min(Math.max(0, digits), MIN_FRACTION_DIGITS),
maximumFractionDigits: Math.min(Math.max(0, digits), MAX_FRACTION_DIGITS),
});
});
const getFormat = memoize(() => ({
decimalSeparator: getDecimalSeparator(),
...getGroupFormat(),
}));
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
export const getFixedNumberFormat = memoize((digits: number) => {
if (isNil(digits) || digits < 0) {
return new Intl.NumberFormat(getUserLocale());
}
return new Intl.NumberFormat(getUserLocale(), {
minimumFractionDigits: Math.min(Math.max(0, digits), MAX_FRACTION_DIGITS),
maximumFractionDigits: Math.min(Math.max(0, digits), MAX_FRACTION_DIGITS),
});
});
/**
* formatNumber will format the number with maximum number of decimals
* trailing zeros are removed but min(MIN_FRACTION_DIGITS, formatDecimals) decimal places will be kept
export const getDecimalSeparator = memoize(
() =>
getNumberFormat(1)
.formatToParts(1.1)
.find((part) => part.type === 'decimal')?.value
);
/** formatNumber will format the number with fixed decimals
* @param rawValue - should be a number that is not outside the safe range fail as in https://mikemcl.github.io/bignumber.js/#toN
* @param formatDecimals - number of decimals to use
*/
@@ -89,23 +90,7 @@ export const formatNumber = (
rawValue: string | number | BigNumber,
formatDecimals = 0
) => {
const decimalPlaces = Math.min(
Math.max(0, formatDecimals),
MAX_FRACTION_DIGITS
);
const format = getFormat();
const formatted = new BigNumber(rawValue).toFormat(decimalPlaces, format);
// if there are no decimal places just return formatted value
if (!decimalPlaces) {
return formatted;
}
// minimum number of decimal places to keep when removing trailing zeros
const minimumFractionDigits = Math.min(decimalPlaces, MIN_FRACTION_DIGITS);
const parts = formatted.split(format.decimalSeparator);
parts[1] = (parts[1] || '')
.replace(/0+$/, '')
.padEnd(minimumFractionDigits, '0');
return parts.join(format.decimalSeparator);
return getNumberFormat(formatDecimals).format(Number(rawValue));
};
/** formatNumberFixed will format the number with fixed decimals
@@ -116,10 +101,7 @@ export const formatNumberFixed = (
rawValue: string | number | BigNumber,
formatDecimals = 0
) => {
return new BigNumber(rawValue).toFormat(
Math.min(Math.max(0, formatDecimals), MAX_FRACTION_DIGITS),
getFormat()
);
return getFixedNumberFormat(formatDecimals).format(Number(rawValue));
};
export const quantumDecimalPlaces = (
@@ -149,14 +131,9 @@ export const addDecimalsFormatNumberQuantum = (
if (isNaN(Number(quantum))) {
return addDecimalsFormatNumber(rawValue, decimalPlaces);
}
const numberDP = Math.ceil(
Math.abs(Math.log10(toBigNum(quantum, decimalPlaces).toNumber()))
);
return addDecimalsFormatNumber(
rawValue,
decimalPlaces,
Math.max(MIN_FRACTION_DIGITS, numberDP)
);
const quantumValue = addDecimal(quantum, decimalPlaces);
const numberDP = Math.max(0, Math.log10(100 / Number(quantumValue)));
return addDecimalsFormatNumber(rawValue, decimalPlaces, Math.ceil(numberDP));
};
export const addDecimalsFormatNumber = (
@@ -164,10 +141,9 @@ export const addDecimalsFormatNumber = (
decimalPlaces: number,
formatDecimals: number = decimalPlaces
) => {
return formatNumber(
new BigNumber(rawValue || 0).dividedBy(Math.pow(10, decimalPlaces)),
formatDecimals
);
const x = addDecimal(rawValue, decimalPlaces);
return formatNumber(x, formatDecimals);
};
export const addDecimalsFixedFormatNumber = (
+12 -12
View File
@@ -10,24 +10,24 @@ describe('formatValue', () => {
{
v: '123456789123456789',
d: 10,
o: '12,345,678.9123456789',
o: '12,345,678.91234568',
},
])('formats values correctly', ({ v, d, o }) => {
expect(formatValue(v, d)).toStrictEqual(o);
});
it.each([
{ v: 123000, d: 5, o: '1.23', q: '1' },
{ v: 123000, d: 3, o: '123.00', q: '1' },
{ v: 123000, d: 1, o: '12,300.00', q: '1' },
{ v: 123001000, d: 2, o: '1,230,010.00', q: '1' },
{ v: 123000, d: 5, o: '1.23', q: '0.1' },
{ v: 123000, d: 3, o: '123.00', q: '0.1' },
{ v: 123000, d: 1, o: '12,300.00', q: '0.1' },
{ v: 123001000, d: 2, o: '1,230,010.00', q: '0.1' },
{ v: 123001, d: 2, o: '1,230.01', q: '100' },
{ v: 123001, d: 2, o: '1,230.01', q: '1' },
{ v: 123001, d: 2, o: '1,230.01', q: '0.1' },
{
v: '123456789123456789',
d: 10,
o: '12,345,678.91235',
q: '384600',
o: '12,345,678.91234568',
q: '0.00003846',
},
])(
'formats with formatValue with quantum given number correctly',
@@ -42,15 +42,15 @@ describe('formatRange', () => {
min: 123000,
max: 12300011111,
d: 5,
o: '1.23 - 123,000.11',
q: '1000',
o: '1.23 - 123,000.11111',
q: '0.1',
},
{
min: 123000,
max: 12300011111,
d: 3,
o: '123.00 - 12,300,011.11',
q: '100',
o: '123.00 - 12,300,011.111',
q: '0.1',
},
{
min: 123000,
+3 -1
View File
@@ -11,7 +11,9 @@
"echo $NX_VEGA_URL",
"echo $NX_TENDERMINT_URL",
"echo $NX_TENDERMINT_WEBSOCKET_URL",
"echo $NX_ETHEREUM_PROVIDER_URL"
"echo $NX_ETHEREUM_PROVIDER_URL",
"echo $NX_CHARTING_LIBRARY_PATH",
"echo $NX_CHARTING_LIBRARY_HASH"
]
}
}