Compare commits

...
Author SHA1 Message Date
Madalina Raicu 3461eec2df fix(trdaing): add loss socialisation docs links 2023-03-07 13:58:20 +00:00
dexturr ac163d0194 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-07 12:08:15 +00:00
Joe Tsang e7616e64f8 test(governance): tranche service (#3099) 2023-03-07 10:42:20 +00:00
Matthew Russell adca4600c2 feat(deposits): deposit flow (#3062) 2023-03-07 08:17:02 +00:00
dexturr a8eef1cb53 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-07 06:07:42 +00:00
dexturr 58d4bd1459 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-07 00:16:09 +00:00
dexturr 26020b5f71 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-06 18:07:54 +00:00
daro-maj 16d345eb4a test(trading): revert nonparallel tests option (#3098) 2023-03-06 17:48:33 +01:00
m.ray 3b3fcab4f4 fix(trading): fills table fees display (#3071) 2023-03-06 17:24:18 +01:00
Matthew Russell f54a629179 feat(positions): positions losses and maintenance warnings (#2985) 2023-03-06 16:01:31 +00:00
dexturr 56f48671cc chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-06 12:09:18 +00:00
Bartłomiej Głownia a65ee358ff fix(market-depth): orderbook row height calculation (#3068) 2023-03-06 10:21:45 +00:00
dexturr 7b8d814724 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-06 06:07:28 +00:00
dexturr cd237dc491 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-06 00:17:15 +00:00
dexturr 9ad00adc5a chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-05 18:07:24 +00:00
Bartłomiej Głownia fab055bb34 chore(orders): fix order table rows flickering on filter change (#3088) 2023-03-05 14:08:16 +00:00
dexturr d66956d080 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-05 12:07:46 +00:00
dexturr 9848c8ffff chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-05 06:08:12 +00:00
dexturr b0e8ed62d2 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-05 00:19:08 +00:00
dexturr 289ff3b5b3 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-04 18:09:11 +00:00
dexturr 1d965187dc chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-04 12:10:27 +00:00
dexturr 8bf3615001 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-04 06:09:19 +00:00
dexturr 1b77800b62 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-04 00:16:57 +00:00
dexturr 979dc2904b chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-03 18:09:26 +00:00
m.ray 0fec3bcf3c fix(trading): update order on market trade header (#3082) (#3087) 2023-03-03 17:37:46 +00:00
Edd 4542e25ec9 feat(explorer): proposal transaction view (#3060) 2023-03-03 14:27:24 +00:00
Edd acd96fbf21 chore(smart-contracts): remove redundant ascii to hex in favour of ethers hexlify (#3066) 2023-03-03 12:21:22 +00:00
Matthew Russell 96bf12d4dd fix(trading): add a timeout to show subscriptions as failed (#3073) 2023-03-03 12:20:51 +00:00
Edd 47b81188d4 fix(explorer): update vega_networks for all explorer envs (#3078) 2023-03-03 12:20:41 +00:00
dexturr efada06256 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-03 12:10:27 +00:00
daro-maj 311ef41138 test(trading): tests parallel (#3075) 2023-03-03 11:42:12 +01:00
Maciek 5dd0cafeca chore(trading): catch ethereum errors and capture to sentry (#3064) 2023-03-03 08:39:20 +01:00
dexturr f3d6aa1129 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-03 06:09:30 +00:00
dexturr 93dbc32f22 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-03 01:56:44 +00:00
dexturr fd1827ef67 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-02 18:09:24 +00:00
Sam Keen 9c9948e4c1 feat(governance): final wallet not connected views (#3037) 2023-03-02 14:36:04 +00:00
Sam Keen 79b0868ff7 feat(governance): remove graph from tranches page (#3049) 2023-03-02 14:35:46 +00:00
94 changed files with 3673 additions and 828 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
NX_TENDERMINT_URL=https://tm.n00.stagnet3.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n00.stagnet3.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_VEGA_NETWORKS='{"SANDBOX":"https://sandbox.explorer.vega.xyz","STAGNET1":"https://stagnet1.token.vega.xyz", "STAGNET3":"https://stagnet3.explorer.vega.xyz","VALIDATOR_TESTNET":"https://validator-testnet.fairground.wtf","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
NX_VEGA_ENV=STAGNET3
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
-2
View File
@@ -1,9 +1,7 @@
# App configuration variables
NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_VEGA_ENV=CUSTOM
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
# App flags
-2
View File
@@ -2,9 +2,7 @@
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/tm
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet-network.json
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_VEGA_ENV=DEVNET
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_BLOCK_EXPLORER=https://be.devnet1.vega.xyz/rest
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://dev.token.vega.xyz
-2
View File
@@ -2,9 +2,7 @@
NX_TENDERMINT_URL=https://be.explorer.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.explorer.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/mainnet-network.json
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_VEGA_ENV=MAINNET
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_BLOCK_EXPLORER=https://be.explorer.vega.xyz/rest/
NX_ETHERSCAN_URL=https://etherscan.io
NX_VEGA_GOVERNANCE_URL=https://token.vega.xyz
-2
View File
@@ -2,9 +2,7 @@
NX_TENDERMINT_URL=https://tm.be.mainnet-mirror.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/mirror-network.json
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
NX_VEGA_ENV=MIRROR
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_BLOCK_EXPLORER=https://be.mainnet-mirror.vega.xyz/rest/
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://mainnet-mirror.token.vega.xyz
-1
View File
@@ -5,7 +5,6 @@ NX_VEGA_EXPLORER_URL=https://sandbox.explorer.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_TENDERMINT_URL=https://tm.sandbox.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.sandbox.vega.xyz/websocket
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
-1
View File
@@ -4,7 +4,6 @@ NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet1-network.json
NX_VEGA_ENV=STAGNET1
NX_VEGA_EXPLORER_URL=https://stagnet1.explorer.vega.xyz
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_VEGA_TOKEN_URL=https://stagnet1.token.vega.xyz
NX_VEGA_WALLET_URL=http://localhost:1789
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.xyz
-2
View File
@@ -1,9 +1,7 @@
NX_TENDERMINT_URL=https://tm.n00.stagnet3.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.stagnet3.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_VEGA_ENV=STAGNET3
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_BLOCK_EXPLORER=https://be.stagnet3.vega.xyz/rest
NX_VEGA_GOVERNANCE_URL=https://stagnet3.token.vega.xyz
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega-dev-releases/releases/
+1 -4
View File
@@ -3,11 +3,8 @@ NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_BLOCK_EXPLORER=https://be.testnet.vega.xyz/rest
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_VEGA_ENV=TESTNET
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_VEGA_NETWORKS={}
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://token.fairground.wtf
NX_VEGA_GOVERNANCE_URL=https://token.fairground.wtf
-2
View File
@@ -3,10 +3,8 @@ NX_VEGA_ENV=VALIDATOR_TESTNET
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/validator-testnet-network.json
NX_VEGA_URL=https://api.validators-testnet.vega.xyz/graphql
NX_VEGA_REST=https://api.validators-testnet.vega.xyz/
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_VEGA_GOVERNANCE_URL=https://validator-testnet.governance.vega.xyz
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.xyz
-1
View File
@@ -2,6 +2,5 @@
NX_TENDERMINT_URL=http://localhost:26607/
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26607/websocket
NX_VEGA_ENV=CUSTOM
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_BLOCK_EXPLORER=
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
+5
View File
@@ -0,0 +1,5 @@
function ReactMarkdown({ children }) {
return <div>{children}</div>;
}
export default ReactMarkdown;
@@ -4,13 +4,14 @@ import { ENV } from '../../../config/env';
import Hash from '../hash';
export type ProposalLinkProps = {
id: string;
text?: string;
};
/**
* Given a proposal ID, generates an external link over to
* the Governance page for more information
*/
const ProposalLink = ({ id }: ProposalLinkProps) => {
const ProposalLink = ({ id, text }: ProposalLinkProps) => {
const { data } = useExplorerProposalQuery({
variables: { id },
});
@@ -20,7 +21,7 @@ const ProposalLink = ({ id }: ProposalLinkProps) => {
return (
<ExternalLink href={`${base}/proposals/${id}`}>
<Hash text={label} />
{text ? text : <Hash text={label} />}
</ExternalLink>
);
};
@@ -0,0 +1,7 @@
query ExplorerProposalStatus($id: ID!) {
proposal(id: $id) {
id
state
rejectionReason
}
}
@@ -0,0 +1,19 @@
query ExplorerNewAssetSignatureBundle($id: ID!) {
erc20ListAssetBundle(assetId: $id) {
signatures
nonce
}
asset(id: $id) {
status
}
}
query ExplorerUpdateAssetSignatureBundle($id: ID!) {
erc20SetAssetLimitsBundle(proposalId: $id) {
signatures
nonce
}
asset(id: $id) {
status
}
}
@@ -0,0 +1,50 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerProposalStatusQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
export type ExplorerProposalStatusQuery = { __typename?: 'Query', proposal?: { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | null };
export const ExplorerProposalStatusDocument = gql`
query ExplorerProposalStatus($id: ID!) {
proposal(id: $id) {
id
state
rejectionReason
}
}
`;
/**
* __useExplorerProposalStatusQuery__
*
* To run a query within a React component, call `useExplorerProposalStatusQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerProposalStatusQuery` 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 } = useExplorerProposalStatusQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useExplorerProposalStatusQuery(baseOptions: Apollo.QueryHookOptions<ExplorerProposalStatusQuery, ExplorerProposalStatusQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerProposalStatusQuery, ExplorerProposalStatusQueryVariables>(ExplorerProposalStatusDocument, options);
}
export function useExplorerProposalStatusLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerProposalStatusQuery, ExplorerProposalStatusQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerProposalStatusQuery, ExplorerProposalStatusQueryVariables>(ExplorerProposalStatusDocument, options);
}
export type ExplorerProposalStatusQueryHookResult = ReturnType<typeof useExplorerProposalStatusQuery>;
export type ExplorerProposalStatusLazyQueryHookResult = ReturnType<typeof useExplorerProposalStatusLazyQuery>;
export type ExplorerProposalStatusQueryResult = Apollo.QueryResult<ExplorerProposalStatusQuery, ExplorerProposalStatusQueryVariables>;
@@ -0,0 +1,98 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerNewAssetSignatureBundleQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
export type ExplorerNewAssetSignatureBundleQuery = { __typename?: 'Query', erc20ListAssetBundle?: { __typename?: 'Erc20ListAssetBundle', signatures: string, nonce: string } | null, asset?: { __typename?: 'Asset', status: Types.AssetStatus } | null };
export type ExplorerUpdateAssetSignatureBundleQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
export type ExplorerUpdateAssetSignatureBundleQuery = { __typename?: 'Query', erc20SetAssetLimitsBundle: { __typename?: 'ERC20SetAssetLimitsBundle', signatures: string, nonce: string }, asset?: { __typename?: 'Asset', status: Types.AssetStatus } | null };
export const ExplorerNewAssetSignatureBundleDocument = gql`
query ExplorerNewAssetSignatureBundle($id: ID!) {
erc20ListAssetBundle(assetId: $id) {
signatures
nonce
}
asset(id: $id) {
status
}
}
`;
/**
* __useExplorerNewAssetSignatureBundleQuery__
*
* To run a query within a React component, call `useExplorerNewAssetSignatureBundleQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerNewAssetSignatureBundleQuery` 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 } = useExplorerNewAssetSignatureBundleQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useExplorerNewAssetSignatureBundleQuery(baseOptions: Apollo.QueryHookOptions<ExplorerNewAssetSignatureBundleQuery, ExplorerNewAssetSignatureBundleQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerNewAssetSignatureBundleQuery, ExplorerNewAssetSignatureBundleQueryVariables>(ExplorerNewAssetSignatureBundleDocument, options);
}
export function useExplorerNewAssetSignatureBundleLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerNewAssetSignatureBundleQuery, ExplorerNewAssetSignatureBundleQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerNewAssetSignatureBundleQuery, ExplorerNewAssetSignatureBundleQueryVariables>(ExplorerNewAssetSignatureBundleDocument, options);
}
export type ExplorerNewAssetSignatureBundleQueryHookResult = ReturnType<typeof useExplorerNewAssetSignatureBundleQuery>;
export type ExplorerNewAssetSignatureBundleLazyQueryHookResult = ReturnType<typeof useExplorerNewAssetSignatureBundleLazyQuery>;
export type ExplorerNewAssetSignatureBundleQueryResult = Apollo.QueryResult<ExplorerNewAssetSignatureBundleQuery, ExplorerNewAssetSignatureBundleQueryVariables>;
export const ExplorerUpdateAssetSignatureBundleDocument = gql`
query ExplorerUpdateAssetSignatureBundle($id: ID!) {
erc20SetAssetLimitsBundle(proposalId: $id) {
signatures
nonce
}
asset(id: $id) {
status
}
}
`;
/**
* __useExplorerUpdateAssetSignatureBundleQuery__
*
* To run a query within a React component, call `useExplorerUpdateAssetSignatureBundleQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerUpdateAssetSignatureBundleQuery` 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 } = useExplorerUpdateAssetSignatureBundleQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useExplorerUpdateAssetSignatureBundleQuery(baseOptions: Apollo.QueryHookOptions<ExplorerUpdateAssetSignatureBundleQuery, ExplorerUpdateAssetSignatureBundleQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerUpdateAssetSignatureBundleQuery, ExplorerUpdateAssetSignatureBundleQueryVariables>(ExplorerUpdateAssetSignatureBundleDocument, options);
}
export function useExplorerUpdateAssetSignatureBundleLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerUpdateAssetSignatureBundleQuery, ExplorerUpdateAssetSignatureBundleQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerUpdateAssetSignatureBundleQuery, ExplorerUpdateAssetSignatureBundleQueryVariables>(ExplorerUpdateAssetSignatureBundleDocument, options);
}
export type ExplorerUpdateAssetSignatureBundleQueryHookResult = ReturnType<typeof useExplorerUpdateAssetSignatureBundleQuery>;
export type ExplorerUpdateAssetSignatureBundleLazyQueryHookResult = ReturnType<typeof useExplorerUpdateAssetSignatureBundleLazyQuery>;
export type ExplorerUpdateAssetSignatureBundleQueryResult = Apollo.QueryResult<ExplorerUpdateAssetSignatureBundleQuery, ExplorerUpdateAssetSignatureBundleQueryVariables>;
@@ -0,0 +1,70 @@
import { t } from '@vegaprotocol/i18n';
import { Lozenge } from '@vegaprotocol/ui-toolkit';
import type { components } from '../../../../../types/explorer';
import type { ExplorerProposalStatusQuery } from './__generated__/Proposal';
import { useExplorerProposalStatusQuery } from './__generated__/Proposal';
type Terms = components['schemas']['vegaProposalTerms'];
export function format(date: string | undefined, def: string) {
if (!date) {
return def;
}
return new Date().toLocaleDateString() || def;
}
export function getDate(
data: ExplorerProposalStatusQuery | undefined,
terms: Terms
): string {
const DEFAULT = t('Unknown');
if (!data?.proposal?.state) {
return DEFAULT;
}
switch (data.proposal.state) {
case 'STATE_DECLINED':
return `${t('Rejected on')}: ${format(terms.closingTimestamp, DEFAULT)}`;
case 'STATE_ENACTED':
return `${t('Vote passed on')}: ${format(
terms.enactmentTimestamp,
DEFAULT
)}`;
case 'STATE_FAILED':
return `${t('Failed on')}: ${format(terms.validationTimestamp, DEFAULT)}`;
case 'STATE_OPEN':
return `${t('Open until')}: ${format(terms.closingTimestamp, DEFAULT)}`;
case 'STATE_PASSED':
return `${t('Passed on')}: ${format(terms.closingTimestamp, DEFAULT)}`;
case 'STATE_REJECTED':
return `${t('Rejected on submission')}`;
case 'STATE_WAITING_FOR_NODE_VOTE':
return `${t('Opening')}...`;
default:
return DEFAULT;
}
}
interface ProposalDateProps {
id: string;
terms: Terms;
}
/**
* Shows the most relevant date for the proposal summary view. Depending on the
* state returned by GraphQL, we show either the validation, closing or enactment
* timestamp
*/
export const ProposalDate = ({ terms, id }: ProposalDateProps) => {
const { data } = useExplorerProposalStatusQuery({
variables: {
id,
},
});
return (
<Lozenge className="font-sans text-xs float-right">
{getDate(data, terms)}
</Lozenge>
);
};
@@ -0,0 +1,117 @@
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import type { IconProps } from '@vegaprotocol/ui-toolkit';
import { useExplorerProposalStatusQuery } from './__generated__/Proposal';
import type { ExplorerProposalStatusQuery } from './__generated__/Proposal';
import type * as Apollo from '@apollo/client';
import type * as Types from '@vegaprotocol/types';
import { t } from '@vegaprotocol/i18n';
type ProposalQueryResult = Apollo.QueryResult<
ExplorerProposalStatusQuery,
Types.Exact<{
id: string;
}>
>;
interface ProposalStatusIconProps {
id: string;
}
type IconAndLabel = {
icon: IconProps['name'];
label: string;
};
/**
* Select an icon for a given query result. Tolerates queries that don't return
* any data
*
* @param data a data result from useExplorerProposalStatusQuery
* @returns Icon name
*/
export function getIconAndLabelForStatus(
res: ProposalQueryResult
): IconAndLabel {
const DEFAULT: IconAndLabel = {
icon: 'error',
label: t('Proposal state unknown'),
};
if (res.loading) {
return {
icon: 'more',
label: t('Loading data'),
};
}
if (!res?.data?.proposal || res.error) {
return {
icon: 'error',
label: res.error?.message || DEFAULT.label,
};
}
switch (res.data.proposal.state) {
case 'STATE_DECLINED':
return {
icon: 'stop',
label: t('Proposal did not have enough participation to be valid'),
};
case 'STATE_ENACTED':
return {
icon: 'tick-circle',
label: t('Vote passed and the proposal has been enacted'),
};
case 'STATE_FAILED':
return {
icon: 'thumbs-down',
label: t('Proposal became invalid and was not processed'),
};
case 'STATE_OPEN':
return {
// A checklist to indicate in progress
icon: 'form',
label: t('Voting is in progress'),
};
case 'STATE_PASSED':
return {
icon: 'thumbs-up',
label: t(
'Voting is complete and this proposal was approved. It is not yet enacted.'
),
};
case 'STATE_REJECTED':
return {
icon: 'disable',
label: t('The proposal was invalid'),
};
case 'STATE_WAITING_FOR_NODE_VOTE':
return {
// A sparkly thing indicating it's new
icon: 'clean',
label: t('Proposal is being checked by validators'),
};
default:
return DEFAULT;
}
}
/**
*/
export const ProposalStatusIcon = ({ id }: ProposalStatusIconProps) => {
const { icon, label } = getIconAndLabelForStatus(
useExplorerProposalStatusQuery({
variables: {
id,
},
})
);
return (
<div className="float-left mr-3">
<Tooltip description={<p>{label}</p>}>
<Icon name={icon} />
</Tooltip>
</div>
);
};
@@ -0,0 +1,42 @@
import { Loader } from '@vegaprotocol/ui-toolkit';
import { BundleError } from './signature-bundle/bundle-error';
import { BundleExists } from './signature-bundle/bundle-exists';
import { useExplorerNewAssetSignatureBundleQuery } from './__generated__/SignatureBundle';
export interface ProposalSignatureBundleByTypeProps {
id: string;
}
/**
* If a proposal needs a signature bundle, AND that signature bundle exists
* AND that proposal is a New Asset Proposal, render an overview of the
* signature bundle. Or an error.
*
* There is an almost identical component, ProposalSignatureBundleUpdateAsset
*/
export const ProposalSignatureBundleNewAsset = ({
id,
}: ProposalSignatureBundleByTypeProps) => {
const { data, error, loading } = useExplorerNewAssetSignatureBundleQuery({
variables: {
id,
},
});
if (loading) {
return <Loader />;
}
if (data?.erc20ListAssetBundle?.signatures) {
return (
<BundleExists
signatures={data.erc20ListAssetBundle.signatures}
nonce={data.erc20ListAssetBundle.nonce}
status={data.asset?.status}
proposalId={id}
/>
);
} else {
return <BundleError status={data?.asset?.status} error={error} />;
}
};
@@ -0,0 +1,39 @@
import { Loader } from '@vegaprotocol/ui-toolkit';
import type { ProposalSignatureBundleByTypeProps } from './signature-bundle-new';
import { BundleError } from './signature-bundle/bundle-error';
import { BundleExists } from './signature-bundle/bundle-exists';
import { useExplorerUpdateAssetSignatureBundleQuery } from './__generated__/SignatureBundle';
/**
* If a proposal needs a signature bundle, AND that signature bundle exists
* AND that proposal is a Asset Limits Proposal, render an overview of the
* signature bundle. Or an error.
*
* There is an almost identical component, ProposalSignatureBundleNewAsset
*/
export const ProposalSignatureBundleUpdateAsset = ({
id,
}: ProposalSignatureBundleByTypeProps) => {
const { data, error, loading } = useExplorerUpdateAssetSignatureBundleQuery({
variables: {
id,
},
});
if (loading) {
return <Loader />;
}
if (data?.erc20SetAssetLimitsBundle?.signatures) {
return (
<BundleExists
signatures={data.erc20SetAssetLimitsBundle.signatures}
nonce={data.erc20SetAssetLimitsBundle.nonce}
status={data.asset?.status}
proposalId={id}
/>
);
} else {
return <BundleError status={data?.asset?.status} error={error} />;
}
};
@@ -0,0 +1,31 @@
import { ProposalSignatureBundleNewAsset } from './signature-bundle-new';
import { ProposalSignatureBundleUpdateAsset } from './signature-bundle-update';
export function format(date: string | undefined, def: string) {
if (!date) {
return def;
}
return new Date().toLocaleDateString() || def;
}
interface ProposalSignatureBundleProps {
id: string;
type: 'NewAsset' | 'UpdateAsset';
}
/**
* Some proposals, if enacted, generate a signature bundle.
* The queries have to be split due to the way the API returns
* errors, hence this slightly redundant feeling switcher.
*/
export const ProposalSignatureBundle = ({
id,
type,
}: ProposalSignatureBundleProps) => {
return type === 'NewAsset' ? (
<ProposalSignatureBundleNewAsset id={id} />
) : (
<ProposalSignatureBundleUpdateAsset id={id} />
);
};
@@ -0,0 +1,67 @@
import type { ApolloError } from '@apollo/client';
import { MockedProvider } from '@apollo/client/testing';
import { render } from '@testing-library/react';
import { AssetStatus } from '@vegaprotocol/types';
import { MemoryRouter } from 'react-router-dom';
import { BundleError } from './bundle-error';
describe('Bundle Error', () => {
const NON_ENABLED_STATUS: AssetStatus[] = [
AssetStatus.STATUS_PENDING_LISTING,
AssetStatus.STATUS_PROPOSED,
AssetStatus.STATUS_REJECTED,
];
const ENABLED_STATUS: AssetStatus[] = [AssetStatus.STATUS_ENABLED];
it.each(NON_ENABLED_STATUS)(
'shows the apollo error if not enabled and a message is provided',
(status) => {
const screen = render(
<MemoryRouter>
<MockedProvider>
<BundleError
error={{ message: 'test-error-message' } as ApolloError}
status={status}
/>
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('test-error-message')).toBeInTheDocument();
}
);
it.each(NON_ENABLED_STATUS)(
'shows some fallback error message if the bundle has not been used and there is no error',
(status) => {
const screen = render(
<MemoryRouter>
<MockedProvider>
<BundleError status={status} />
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('No bundle for proposal ID')).toBeInTheDocument();
}
);
it.each(ENABLED_STATUS)(
'hides ProposalLink if the status is enabled',
(status) => {
const screen = render(
<MemoryRouter>
<MockedProvider>
<BundleError
error={{ message: 'irrelevant ' } as ApolloError}
status={status}
/>
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('Asset already enabled')).toBeInTheDocument();
}
);
});
@@ -0,0 +1,34 @@
import type { ApolloError } from '@apollo/client';
import type { AssetStatus } from '@vegaprotocol/types';
import { t } from '@vegaprotocol/i18n';
import Hash from '../../../../links/hash';
import { IconForBundleStatus } from './bundle-icon';
export interface BundleErrorProps {
status?: AssetStatus;
error?: ApolloError;
}
/**
* Renders if a proposal signature bundle cannot be found.
* It is also possible that a data node has dropped the bundle
* from its retention so there is a backup case where we check
* the status - if it's already enabled, pretend this isn't an error
*/
export const BundleError = ({ status, error }: BundleErrorProps) => {
return (
<div className="w-auto max-w-lg border-2 border-solid border-vega-light-100 dark:border-vega-dark-200 p-5 mt-5">
<IconForBundleStatus status={status} />
<h1 className="text-xl pb-1">{t('No signature bundle found')}</h1>
<p>
{status === 'STATUS_ENABLED' ? (
t('Asset already enabled')
) : (
<Hash text={error ? error.message : t('No bundle for proposal ID')} />
)}
</p>
</div>
);
};
@@ -0,0 +1,64 @@
import { MockedProvider } from '@apollo/client/testing';
import { render } from '@testing-library/react';
import { AssetStatus } from '@vegaprotocol/types';
import { MemoryRouter } from 'react-router-dom';
import { BundleExists } from './bundle-exists';
describe('Bundle Exists', () => {
const NON_ENABLED_STATUS: AssetStatus[] = [
AssetStatus.STATUS_PENDING_LISTING,
AssetStatus.STATUS_PROPOSED,
AssetStatus.STATUS_REJECTED,
];
const ENABLED_STATUS: AssetStatus[] = [AssetStatus.STATUS_ENABLED];
const MOCK_SIGNATURES =
'0x1760ce4efec01d5bb9fe57c0660a39a27e0b5a1bd39b4d3fc0452bf142a4ef733baaf4dbedd74cd676c759f96ac7f412ec05e136bbff8a81d9f0c2428df8e099004c4d166ece0527d86e202386e8758580790d0a46f6429baaa268b4bf5c11c9e3402e175a9022ae8295e543853c01383ef17cd58458ddfc9c706fca0ecffa0d3d0160eb5234e63a78b9649428ca7659eb693fdde86edfc6c2707ef2e8fbf4c76a9f0eebe183da571a58837e2fa995d7b10955ecf04c138dc0ce964f17c3de1f5c6d0060ec132cc515cf974ead1da38e2ff8d4b870335865e8d4d8c982182bddea18bb513e2d37fd32de7fedc0c4f694e6bcdcf20f8547f19e7d9d25ce32c6ca9ea51e01ad3b92475778d9da7251d3943071f59107c9cd7ad9dd1923c06e88d3352869d919a591bf30732bad2c3fcf30a9f664dbb7a9c65a64875a48cbeb5741bd0a853901';
const MOCK_NONCE =
'18250011763873610289536200551900545467959221115607409799241178172533618346952';
const MOCK_PROPOSAL_ID =
'285923fed8c66ffb416b163e8ec72d3a87b9b8e2570e7ee7fe97d7092a918bc8';
const PROPOSAL_LINK_TEXT = 'Visit our Governance site to submit this';
it.each(NON_ENABLED_STATUS)(
'shows a handy ProposalLink if the status is anything except enabled',
(status) => {
const screen = render(
<MemoryRouter>
<MockedProvider>
<BundleExists
nonce={MOCK_NONCE}
proposalId={MOCK_PROPOSAL_ID}
signatures={MOCK_SIGNATURES}
status={status}
/>
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText(PROPOSAL_LINK_TEXT)).toBeInTheDocument();
}
);
it.each(ENABLED_STATUS)(
'hides ProposalLink if the status is enabled',
(status) => {
const screen = render(
<MemoryRouter>
<MockedProvider>
<BundleExists
nonce={MOCK_NONCE}
proposalId={MOCK_PROPOSAL_ID}
signatures={MOCK_SIGNATURES}
status={status}
/>
</MockedProvider>
</MemoryRouter>
);
expect(screen.queryAllByText(PROPOSAL_LINK_TEXT)).toEqual([]);
}
);
});
@@ -0,0 +1,46 @@
import { t } from '@vegaprotocol/i18n';
import type { AssetStatus } from '@vegaprotocol/types';
import ProposalLink from '../../../../links/proposal-link/proposal-link';
import { IconForBundleStatus } from './bundle-icon';
import { ProposalSignatureBundleDetails } from './details';
export interface BundleExistsProps {
signatures: string;
nonce: string;
status?: AssetStatus;
proposalId: string;
}
/**
* If a proposal needs a signature bundle, AND that signature bundle exists,
* this component renders that signature bundle.
*
*/
export const BundleExists = ({
signatures,
nonce,
status,
proposalId,
}: BundleExistsProps) => {
return (
<div className="w-auto max-w-lg border-2 border-solid border-vega-light-100 dark:border-vega-dark-200 p-5 mt-5">
<IconForBundleStatus status={status} />
<h1 className="text-xl pb-1">
{status === 'STATUS_ENABLED'
? t('Asset added to bridge')
: t('Signature bundle generated')}
</h1>
<ProposalSignatureBundleDetails signatures={signatures} nonce={nonce} />
{status !== 'STATUS_ENABLED' ? (
<p className="mt-5">
<ProposalLink
id={proposalId}
text={t('Visit our Governance site to submit this')}
/>
</p>
) : null}
</div>
);
};
@@ -0,0 +1,33 @@
import { render } from '@testing-library/react';
import { AssetStatus } from '@vegaprotocol/types';
import { IconForBundleStatus } from './bundle-icon';
describe('Bundle status icon', () => {
const NON_ENABLED_STATUS: AssetStatus[] = [
AssetStatus.STATUS_PENDING_LISTING,
AssetStatus.STATUS_PROPOSED,
AssetStatus.STATUS_REJECTED,
];
const ENABLED_STATUS: AssetStatus[] = [AssetStatus.STATUS_ENABLED];
it.each(NON_ENABLED_STATUS)(
'show a sparkle icon if the bundle is unused',
(status) => {
const screen = render(<IconForBundleStatus status={status} />);
const i = screen.getByRole('img');
expect(i).toHaveAttribute('aria-label');
expect(i.getAttribute('aria-label')).toMatch(/clean/);
}
);
it.each(ENABLED_STATUS)(
'shows a tick if the bundle is already used',
(status) => {
const screen = render(<IconForBundleStatus status={status} />);
const i = screen.getByRole('img');
expect(i).toHaveAttribute('aria-label');
expect(i.getAttribute('aria-label')).toMatch(/tick-circle/);
}
);
});
@@ -0,0 +1,17 @@
import type { AssetStatus } from '@vegaprotocol/types';
import type { IconName } from '@vegaprotocol/ui-toolkit';
import { Icon } from '@vegaprotocol/ui-toolkit';
export interface IconForBundleStatusProps {
status?: AssetStatus;
}
/**
* Naively select an icon for an asset. If it is enabled, we show a tick - anything
* else is assumed to be 'in progress'. There should only be a signature bundle or the
* asset should not exist
*/
export const IconForBundleStatus = ({ status }: IconForBundleStatusProps) => {
const i: IconName = status === 'STATUS_ENABLED' ? 'tick-circle' : 'clean';
return <Icon className="float-left mt-2 mr-3" name={i} />;
};
@@ -0,0 +1,41 @@
import { t } from '@vegaprotocol/i18n';
export interface ProposalSignatureBundleDetailsProps {
signatures: string;
nonce: string;
}
export const ProposalSignatureBundleDetails = ({
signatures,
nonce,
}: ProposalSignatureBundleDetailsProps) => {
return (
<details className="mt-5">
<summary>{t('Signature bundle details')}</summary>
<div className="ml-4">
<h2 className="text-lg mt-2 mb-2">{t('Signatures')}</h2>
<p>
<textarea
className="font-mono bg-neutral-300 text-[11px] leading-3 text-gray-900 w-full p-2 max-w-[615px]"
readOnly={true}
rows={12}
cols={120}
value={signatures}
/>
</p>
<h2 className="text-lg mt-5 mb-2">{t('Nonce')}</h2>
<p>
<textarea
className="font-mono bg-neutral-300 text-[11px] leading-3 text-gray-900 w-full p-2 max-w-[615px]"
readOnly={true}
rows={2}
cols={120}
value={nonce}
/>
</p>
</div>
</details>
);
};
@@ -0,0 +1,90 @@
import type { ProposalTerms } from '../tx-proposal';
import { useState } from 'react';
import type { components } from '../../../../../types/explorer';
import { JsonViewerDialog } from '../../../dialogs/json-viewer-dialog';
import ProposalLink from '../../../links/proposal-link/proposal-link';
import truncate from 'lodash/truncate';
import { ProposalStatusIcon } from './proposal-status-icon';
import ReactMarkdown from 'react-markdown';
import { ProposalDate } from './proposal-date';
import { t } from '@vegaprotocol/i18n';
type Rationale = components['schemas']['vegaProposalRationale'];
type ProposalTermsDialog = {
open: boolean;
title: string;
content: unknown;
};
interface ProposalSummaryProps {
id: string;
rationale?: Rationale;
terms?: ProposalTerms;
}
/**
* Effectively a 'preview' for what the proposal is about, and a link to the
* Token site for full breakdown of votes
*/
export const ProposalSummary = ({
id,
rationale,
terms,
}: ProposalSummaryProps) => {
const [dialog, setDialog] = useState<ProposalTermsDialog>({
open: false,
title: '',
content: null,
});
const openDialog = () => {
if (!terms) return;
setDialog({
open: true,
title: rationale?.title || t('Proposal details'),
content: terms ? terms : {},
});
};
const md =
rationale && rationale.description
? truncate(rationale.description, {
// Limits the description to roughly 5 lines, maximum
length: 350,
})
: '';
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?.description && (
<p className="pt-2 text-sm leading-tight">
<ReactMarkdown
className="react-markdown-container"
skipHtml={true}
disallowedElements={['img']}
linkTarget="_blank"
>
{md}
</ReactMarkdown>
</p>
)}
<p className="pt-5">
<button className="underline max-md:hidden mr-5" onClick={openDialog}>
{t('View terms')}
</button>{' '}
<ProposalLink id={id} text={t('Full details')} />
{terms && <ProposalDate terms={terms} id={id} />}
</p>
<JsonViewerDialog
open={dialog.open}
onChange={(isOpen) => setDialog({ ...dialog, open: isOpen })}
title={dialog.title}
content={dialog.content}
/>
</div>
);
};
@@ -55,7 +55,7 @@ export const TxDetailsShared = ({
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Hash')}</TableCell>
<TableCell>
<Hash text={txData.hash} />
<Hash text={txData.hash.toLowerCase()} />
</TableCell>
</TableRow>
<TableRow modifier="bordered">
@@ -24,6 +24,7 @@ import { TxDetailsProtocolUpgrade } from './tx-details-protocol-upgrade';
import { TxDetailsIssueSignatures } from './tx-issue-signatures';
import { TxDetailsNodeAnnounce } from './tx-node-announce';
import { TxDetailsStateVariable } from './tx-state-variable-proposal';
import { TxProposal } from './tx-proposal';
import { TxDetailsTransfer } from './tx-transfer';
interface TxDetailsWrapperProps {
@@ -88,6 +89,8 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
return TxDetailsOrderAmend;
case 'Validator Heartbeat':
return TxDetailsHeartbeat;
case 'Proposal':
return TxProposal;
case 'Vote on Proposal':
return TxProposalVote;
case 'Batch Market Instructions':
@@ -0,0 +1,115 @@
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { sharedHeaderProps, TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import type { components } from '../../../../types/explorer';
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
import has from 'lodash/has';
import { ProposalSummary } from './proposal/summary';
import Hash from '../../links/hash';
import { ProposalSignatureBundle } from './proposal/signature-bundle';
import { t } from '@vegaprotocol/i18n';
export type Proposal = components['schemas']['v1ProposalSubmission'];
export type ProposalTerms = components['schemas']['vegaProposalTerms'];
interface TxProposalProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* Detects if a given proposal requires a signature bundle to be emitted by the validators for
* the proposal to be completed after it is accepted by the governance vote
*
* @param t The details of the proposal
* @returns boolean True if a signature bundle is required. Used to fetch a signature bundle
*/
export function proposalRequiresSignatureBundle(proposal?: Proposal): boolean {
if (!proposal?.terms) {
return false;
}
return !!['newAsset', 'updateAsset'].filter((requiredIfExists) =>
has(proposal.terms, requiredIfExists)
);
}
/**
* Given a proposal, returns a nice text label for the proposal type
*
* @param terms The details of the proposal
* @returns string Proposal type
*/
export function proposalTypeLabel(terms?: ProposalTerms): string {
if (has(terms, 'newAsset')) {
return t('New asset proposal');
} else if (has(terms, 'updateAsset')) {
return t('Update asset proposal');
} else if (has(terms, 'newMarket')) {
return t('New market proposal');
} else if (has(terms, 'updateMarket')) {
return t('Update market proposal');
} else if (has(terms, 'updateNetworkParameter')) {
return t('Update network parameter');
} else if (has(terms, 'newFreeform')) {
return t('Freeform proposal');
}
// The list above contains all currently known types. This will be triggered if a new
// unrecognised proposal type is added.
return t('Governance proposal');
}
/**
* A proposal. It's more simplistic than man other views because there are already other, better
* ways to view things about a proposal!
*
*/
export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => {
if (!txData || !txData.command.proposalSubmission) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
let deterministicId = '';
const proposal: Proposal = txData.command.proposalSubmission;
const sig = txData?.signature?.value;
if (sig) {
deterministicId = txSignatureToDeterministicId(sig);
}
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
<TableCell>{proposalTypeLabel(proposal.terms)}</TableCell>
</TableRow>
{/* TODO: Disable type row */}
<TxDetailsShared
txData={txData}
pubKey={pubKey}
blockData={blockData}
hideTypeRow={true}
/>
<TableRow modifier="bordered">
<TableCell>{t('Proposal ID')}</TableCell>
<TableCell>
<Hash text={deterministicId} />
</TableCell>
</TableRow>
</TableWithTbody>
<ProposalSummary
id={deterministicId}
rationale={proposal.rationale}
terms={proposal?.terms}
/>
{proposalRequiresSignatureBundle(proposal) && (
<ProposalSignatureBundle
id={deterministicId}
type={proposal.terms?.newAsset ? 'NewAsset' : 'UpdateAsset'}
/>
)}
</>
);
};
@@ -9,7 +9,7 @@ import type {
// Note: Long enough that there is a truncated output and a full output
const pubKey =
'67755549e43e95f0697f83b2bf419c6ccc18eee32a8a61b8ba6f59471b86fbef';
const hash = '7416753A30622A9E24A06F0172D6C33A95186B36806D96345C6DC5A23FA3F283';
const hash = '7416753a30622a9e24a06f0172d6c33a95186b36806d96345c6dc5a23fa3f283';
const height = '52987';
const txData: BlockExplorerTransactionResult = {
+9
View File
@@ -2,3 +2,12 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.react-markdown-container a {
color: #ff077f;
text-decoration: underline;
}
.react-markdown-container a:before {
content: '🔗 ';
}
@@ -0,0 +1,83 @@
export const trancheData = {
'0': {
tranche_id: 0,
users: [],
initial_balance: 0,
current_balance: 0,
cliff_start: 0,
duration: 0,
},
'1': {
tranche_id: 1,
users: [],
initial_balance: 0,
current_balance: 0,
cliff_start: 1670422330,
duration: 1670453883,
},
'107': {
tranche_id: 107,
users: [],
initial_balance: 0,
current_balance: 0,
cliff_start: 0,
duration: 0,
},
'110': {
tranche_id: 110,
users: [],
initial_balance: 0,
current_balance: 0,
cliff_start: 0,
duration: 0,
},
'154': {
tranche_id: 154,
users: [],
initial_balance: 0,
current_balance: 0,
cliff_start: 0,
duration: 0,
},
'2': {
tranche_id: 2,
users: [
'0x8716c75bb3abe54b959adc529e6355f0422454ed',
'0xc704da0e96a6b910ffa8d3f9f667d3d35db8e5a1',
'0xbb82d4d6e4381ae98f8c33629285867b54d91a03',
'0x82ffe4ed818a6c7d4f0f044998611f24ec916c43',
'0xb0594132540ebc14f17863f8296f4546a7afe4e7',
'0x9015a3db9a922521007a9b495cafda14d8d4128e',
'0x5426f7f717fdbe331bb5a99b908ad2fb75dff711',
'0xf73e7ad8aa300a7f86bcc4d55e6c4ea25546e057',
],
initial_balance: 111300000000000000000,
current_balance: 111297025049610000000,
cliff_start: 1677578461,
duration: 15209600,
},
'3': {
tranche_id: 3,
users: [
'0xbb82d4d6e4381ae98f8c33629285867b54d91a03',
'0x82ffe4ed818a6c7d4f0f044998611f24ec916c43',
'0xb0594132540ebc14f17863f8296f4546a7afe4e7',
'0x9015a3db9a922521007a9b495cafda14d8d4128e',
'0x77a0b7e247b7b8ec99e068a24c401783d6c4dfff',
'0x5426f7f717fdbe331bb5a99b908ad2fb75dff711',
'0xc704da0e96a6b910ffa8d3f9f667d3d35db8e5a1',
],
initial_balance: 21000000000000000000,
current_balance: 21000000000000000000,
cliff_start: 1677578461,
duration: 18628800,
},
'66': {
tranche_id: 66,
users: [],
initial_balance: 0,
current_balance: 0,
cliff_start: 0,
duration: 0,
},
};
@@ -0,0 +1,97 @@
import { trancheData } from '../../fixtures/mocks/tranches';
const tranches = trancheData;
context(
'Tranches page - verify elements on the page',
{ tags: '@smoke' },
function () {
before('visit homepage', function () {
cy.intercept('GET', '**/tranches/stats', { tranches });
cy.visit('/');
});
it('Able to navigate to tranches page', function () {
cy.navigate_to('supply');
cy.url().should('include', '/token/tranches');
cy.get('h1').should('contain.text', 'Vesting tranches');
});
// 1005-VEST-001
// 1005-VEST-002
it('Able to view tranches', function () {
cy.getByTestId('tranche-item')
.should('have.length', 2)
.first()
.within(() => {
cy.get('a')
.should('have.text', 'Tranche 2') // 1005-VEST-003
.and('have.attr', 'href', '/token/tranches/2');
cy.get('span').eq(1).should('have.text', '111.30'); // 1005-VEST-005
cy.contains('Unlocking starts') // 1005-VEST-008
.parent()
.should('contain.text', '28 Feb 2023');
cy.contains('Fully unlocked')
.parent()
.should('contain.text', '23 Aug 2023');
cy.getByTestId('progress-bar').should('exist');
cy.getByTestId('currency-locked') // 1005-VEST-006
.invoke('text')
.should('not.be.empty');
cy.getByTestId('currency-unlocked') // 1005-VEST-007
.invoke('text')
.should('not.be.empty');
});
});
it('Able to see individual tranche data', function () {
cy.get('[href="/token/tranches/2"]').click();
cy.getByTestId('redeemed-tranche-tokens').within(() => {
cy.get('span').eq(1).should('have.text', 0);
});
cy.getByTestId('key-value-table').within(() => {
cy.getByTestId('link')
.should('have.length', 8)
.each((ethLink) => {
cy.wrap(ethLink)
.should('have.attr', 'href')
.and('contain', 'https://sepolia.etherscan.io/address/');
});
cy.getByTestId('redeem-link')
.should('have.length', 8)
.each((redeemLink) => {
cy.wrap(redeemLink)
.should('have.attr', 'href')
.and('contain', '/token/redeem/');
});
});
});
it('Able to view tranches with less than 10 vega', function () {
cy.navigate_to('supply');
cy.getByTestId('show-all-tranches').click();
cy.getByTestId('tranche-item')
.should('have.length', 8)
.first()
.within(() => {
cy.get('a')
.should('have.text', 'Tranche 0')
.and('have.attr', 'href', '/token/tranches/0');
cy.get('span').eq(1).should('have.text', '0.00');
cy.contains('Unlocking starts')
.parent()
.should('contain.text', '01 Jan 1970');
cy.contains('Fully unlocked')
.parent()
.should('contain.text', '01 Jan 1970');
cy.getByTestId('progress-bar').should('exist');
cy.getByTestId('currency-locked')
.invoke('text')
.should('not.be.empty');
cy.getByTestId('currency-unlocked')
.invoke('text')
.should('not.be.empty');
});
});
}
);
@@ -1,13 +1,15 @@
const connectButton = '[data-testid="connect-to-eth-btn"]';
const lockedTokensInVestingContract = '6,499,972.30';
context(
'Vesting Page - verify elements on page',
{ tags: '@smoke' },
function () {
before('navigate to vesting page', function () {
cy.visit('/').navigate_to('vesting');
});
describe('with wallets disconnected', function () {
before('navigate to vesting page', function () {
cy.visit('/').navigate_to('vesting');
});
it('should have vesting tab highlighted', function () {
cy.verify_tab_highlighted('token');
});
@@ -16,10 +18,7 @@ context(
cy.verify_page_header('Vesting');
});
it('should have connect Eth wallet info', function () {
cy.get(connectButton).should('be.visible');
});
// 1005-VEST-018
it('should have connect Eth wallet button', function () {
cy.get(connectButton)
.should('be.visible')
@@ -27,18 +26,101 @@ context(
});
});
describe('with eth wallet connected', function () {
describe('With Eth wallet connected', function () {
before('connect eth wallet', function () {
cy.ethereum_wallet_connect();
cy.visit('/');
cy.getByTestId('view-connected-eth-btn').click();
});
// 1005-VEST-001
// 1005-VEST-002
it('Able to view tranches', function () {
cy.navigate_to('supply');
cy.url().should('include', '/token/tranches');
cy.get('h1').should('contain.text', 'Vesting tranches');
// 1005-VEST-020 1005-VEST-021
it('Tokens in vesting contract for eth wallet is displayed on wallet window', function () {
cy.getByTestId('vega-in-vesting-contract').within(() => {
cy.getByTestId('currency-title')
.should('contain.text', 'VEGA')
.and('contain.text', 'In vesting contract');
cy.getByTestId('currency-value').should(
'have.text',
lockedTokensInVestingContract
);
cy.getByTestId('currency-locked').should(
'have.text',
lockedTokensInVestingContract
);
cy.getByTestId('currency-unlocked').should('have.text', '0.00');
});
});
// 1005-VEST-022 1005-VEST-023
it('Tokens amount displayed in vesting page', function () {
cy.getByTestId(
'redemption-description',
Cypress.env('txTimeout')
).should('exist');
const redemptionText =
'The connected Ethereum wallet (0xEe7D…d94F) has 6,499,972.30 $VEGA tokens in 1 tranche(s) of the vesting contract.';
cy.getByTestId('redemption-description').should(
'have.text',
redemptionText
);
cy.getByTestId('vesting-table').within(() => {
cy.getByTestId('key-value-table-row')
.eq(0)
.within(() => {
cy.get('dt').should('have.text', 'Vesting VEGA');
cy.get('dd').should('have.text', lockedTokensInVestingContract);
});
cy.getByTestId('key-value-table-row')
.eq(1)
.within(() => {
cy.get('dt').should('have.text', 'Locked');
cy.get('dd').should('have.text', lockedTokensInVestingContract);
});
cy.getByTestId('key-value-table-row')
.eq(2)
.within(() => {
cy.get('dt').should('have.text', 'Unlocked');
cy.get('dd').should('have.text', '0.00');
});
cy.getByTestId('key-value-table-row')
.eq(3)
.within(() => {
cy.get('dt').should('have.text', 'Associated');
cy.get('dd').should('have.text', '0.00');
});
});
});
// 1005-VEST-024 1005-VEST-025 1005-VEST-026 1005-VEST-036 1005-VEST-037
it('Tokens locked in individual tranches are displayed', function () {
cy.getByTestId('tranche-table-footer').should(
'have.text',
'All the tokens in this tranche are locked and must be assigned to a tranche before they can be redeemed.'
);
cy.getByTestId('tranche-item').within(() => {
cy.get('a')
.should('have.text', 'Tranche 0')
.and('have.attr', 'href', '/token/tranches/0');
cy.get('span')
.eq(1)
.should('have.text', lockedTokensInVestingContract);
cy.contains('Unlocking starts') // 1005-VEST-008
.parent()
.should('contain.text', '01 Jan 1970');
cy.contains('Fully unlocked')
.parent()
.should('contain.text', '01 Jan 1970');
cy.getByTestId('progress-bar').should('exist');
cy.getByTestId('currency-locked') // 1005-VEST-006
.should('have.text', lockedTokensInVestingContract);
cy.getByTestId('currency-unlocked') // 1005-VEST-007
.invoke('text')
.should('not.be.empty');
cy.getByTestId('tranche-item-footer').should(
'have.text',
'All the tokens in this tranche are locked and can not be redeemed yet.'
);
});
cy.connectVegaWallet();
});
});
}
@@ -11,7 +11,7 @@ export const EthConnectPrompt = () => {
const { appDispatch } = useAppState();
return (
<Button
variant="primary"
variant="default"
onClick={() =>
appDispatch({
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
@@ -336,6 +336,7 @@
"withdrawLpNoneDeposited": "You have no SLP tokens deposited or rewards accumulated",
"withdrawAllLpSuccessCalloutTitle": "Your SLP tokens and rewards have been sent to your Ethereum address",
"Dissociate VEGA tokens": "Disassociate $VEGA tokens",
"DisassociateVegaTokensFromWallet": "Disassociate $VEGA tokens from your Vega wallet",
"Early Investors": "Early Investors",
"Team": "Team",
"Community": "Community",
@@ -482,6 +483,7 @@
"withdrawFormSubmitButtonIdle": "Withdraw {{amount}} {{symbol}} tokens",
"withdrawFormSubmitButtonPending": "Preparing",
"withdrawalsTitle": "Withdrawals",
"withdrawalsSubtitle": "Connect your Vega wallet to complete a withdrawal. These withdrawals will be completed with an Ethereum transaction.",
"withdrawalsText": "These withdrawals need to be completed with an Ethereum transaction.",
"withdrawalsNone": "You don't have any pending withdrawals.",
"withdrawalsCompleteButton": "Finish withdrawal",
@@ -552,9 +554,6 @@
"resourceNotFound": "Resource Not Found",
"Or": "Or",
"addTokenToWallet": "Show this token in your Ethereum wallet",
"chartBelow": "*Certain activities, for example community incentive tokens, do not have specific dates to be credited and so an approximation has been used. ",
"chartTitle": "Token unlocking schedule",
"chartAbove": "This chart shows the approximate* schedule for how tokens will unlock from vesting tranches over time.",
"date": "Date",
"noEthereumProviderError": "No Ethereum browser extension detected, install MetaMask on desktop or visit from a dApp browser on mobile",
"unsupportedChainIdError": "You're connected to an unsupported network",
@@ -84,6 +84,7 @@ const RedemptionRouter = () => {
fill={true}
variant="primary"
onClick={() => navigate(`${RoutesConfig.REDEEM}/${account}`)}
data-testid="view-connected-eth-btn"
>
{t('View connected Eth Wallet')}
</Button>
@@ -15,7 +15,12 @@ export const DisassociateContainer = () => {
<>
<Heading title={t('pageTitleDisassociate')} />
{!account ? (
<EthConnectPrompt />
<>
<p className="mb-8">{t('DisassociateVegaTokensFromWallet')}</p>
<div className="max-w-[400px]">
<EthConnectPrompt />
</div>
</>
) : (
<DisassociatePage address={account} vegaKey={pubKey ?? ''} />
)}
@@ -73,6 +73,7 @@ export const Tranche = () => {
className="underline"
title={t('View vesting information')}
to={`${Routes.REDEEM}/${user}`}
data-testid="redeem-link"
>
{t('View vesting information')}
</RouterLink>
@@ -1,15 +1,12 @@
import { useWeb3React } from '@web3-react/core';
import React from 'react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SubHeading } from '../../components/heading';
import { TrancheItem } from '../redemption/tranche-item';
import { TrancheLabel } from './tranche-label';
import { VestingChart } from './vesting-chart';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import { useEthereumConfig } from '@vegaprotocol/web3';
import type { Tranche } from '../../lib/tranches/tranches-store';
import { TrancheItem } from '../redemption/tranche-item';
import { TrancheLabel } from './tranche-label';
import { useTranches } from '../../lib/tranches/tranches-store';
import type { Tranche } from '../../lib/tranches/tranches-store';
const trancheMinimum = 10;
@@ -18,7 +15,7 @@ const shouldShowTranche = (t: Tranche) =>
export const Tranches = () => {
const tranches = useTranches((state) => state.tranches);
const [showAll, setShowAll] = React.useState<boolean>(false);
const [showAll, setShowAll] = useState<boolean>(false);
const { t } = useTranslation();
const { chainId } = useWeb3React();
const { config } = useEthereumConfig();
@@ -30,10 +27,6 @@ export const Tranches = () => {
return (
<section>
<SubHeading title={t('chartTitle')} />
<p>{t('chartAbove')}</p>
<VestingChart />
<p>{t('chartBelow')}</p>
{tranches?.length ? (
<ul role="list">
{(showAll ? tranches : filteredTranches).map((tranche) => {
@@ -55,8 +48,12 @@ export const Tranches = () => {
) : (
<p>{t('No tranches')}</p>
)}
<section className="text-center mt-4">
<ButtonLink onClick={() => setShowAll(!showAll)}>
<ButtonLink
data-testid="show-all-tranches"
onClick={() => setShowAll(!showAll)}
>
{showAll
? t(
'Showing tranches with <{{trancheMinimum}} VEGA, click to hide these tranches',
@@ -1,149 +0,0 @@
import { format, startOfMonth } from 'date-fns';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
Area,
AreaChart,
Label,
Legend,
ReferenceLine,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts';
import colors from 'tailwindcss/colors';
import { DATE_FORMAT_LONG } from '../../lib/date-formats';
import data from './data.json';
import { theme } from '@vegaprotocol/tailwindcss-config';
const Colors = theme.colors;
const ORDER = ['community', 'publicSale', 'earlyInvestors', 'team'];
export const VestingChart = () => {
const { t } = useTranslation();
const currentDate = React.useMemo(
() => format(startOfMonth(new Date()), DATE_FORMAT_LONG),
[]
);
return (
<div style={{ marginBottom: 25 }}>
<ResponsiveContainer height={400} width="100%">
<AreaChart data={data}>
<defs>
{[
['pink', Colors.vega.pink.DEFAULT],
['green', Colors.vega.green.DEFAULT],
['orange', Colors.warning],
['yellow', Colors.vega.yellow.DEFAULT],
].map(([key, color]) => (
<linearGradient key={key} id={key} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={color} stopOpacity={0.85} />
<stop offset="100%" stopColor={color} stopOpacity={0} />
</linearGradient>
))}
</defs>
<Tooltip
contentStyle={{ backgroundColor: colors.black }}
separator=":"
// @ts-ignore formatter doesnt seem to allow returning JSX but nonetheless it works
formatter={(value: number) => {
return (
<div
style={{
display: 'flex',
textAlign: 'right',
}}
>
{Intl.NumberFormat().format(value)}
</div>
);
}}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
itemSorter={(label: any) => {
return ORDER.indexOf(label.dataKey) + 1;
}}
/>
<YAxis type="number" width={100}>
<Label
angle={270}
value={t('VEGA').toString()}
position="left"
offset={-5}
fill={colors.white}
/>
</YAxis>
<XAxis dataKey="date">
<Label
value={t('date').toString()}
position="bottom"
offset={5}
fill={colors.white}
/>
</XAxis>
<ReferenceLine
x={currentDate}
stroke={colors.white}
strokeWidth={2}
label={{
position: 'left',
value: currentDate,
fill: colors.white,
}}
/>
<Area
dot={false}
type="linear"
dataKey="team"
stroke={Colors.vega.pink.DEFAULT}
fill="url(#pink)"
yAxisId={0}
strokeWidth={2}
fillOpacity={0.85}
stackId="1"
name={t('Team')}
/>
<Area
dot={false}
type="monotone"
dataKey="earlyInvestors"
stroke={Colors.vega.green.DEFAULT}
fill="url(#green)"
yAxisId={0}
strokeWidth={2}
fillOpacity={0.85}
stackId="1"
name={t('Early Investors')}
/>
<Area
dot={false}
type="monotone"
dataKey="publicSale"
stroke={Colors.vega.yellow.DEFAULT}
fill="url(#yellow)"
yAxisId={0}
strokeWidth={2}
stackId="1"
fillOpacity={0.85}
name={t('Public Sale')}
/>
<Area
dot={false}
type="monotone"
dataKey="community"
stroke={Colors.warning}
fill="url(#orange)"
yAxisId={0}
strokeWidth={2}
fillOpacity={0.85}
stackId="1"
name={t('Community')}
/>
<Legend wrapperStyle={{ position: 'relative' }} />
</AreaChart>
</ResponsiveContainer>
</div>
);
};
@@ -17,12 +17,14 @@ import type { RouteChildProps } from '../index';
const Withdrawals = ({ name }: RouteChildProps) => {
useDocumentTitle(name);
const { t } = useTranslation();
const { pubKey } = useVegaWallet();
return (
<>
<Heading title={t('withdrawalsTitle')} />
{!pubKey && <p className="mb-8">{t('withdrawalsSubtitle')}</p>}
<VegaWalletContainer>
{(currVegaKey) => <WithdrawPendingContainer />}
{() => <WithdrawPendingContainer />}
</VegaWalletContainer>
</>
);
File diff suppressed because it is too large Load Diff
@@ -41,6 +41,7 @@ const completeWithdrawalBtn = 'complete-withdrawal';
const submitTransferBtn = '[type="submit"]';
const transferForm = 'transfer-form';
const depositSubmit = 'deposit-submit';
const approveSubmit = 'approve-submit';
// Because the tests are run on a live network to optimize time, the tests are interdependent and must be run in the given order.
describe('capsule - without MultiSign', { tags: '@slow' }, () => {
@@ -73,13 +74,13 @@ describe('capsule - without MultiSign', { tags: '@slow' }, () => {
cy.getByTestId('deposit-button').click();
connectEthereumWallet('Unknown');
cy.get(assetSelectField, txTimeout).select(btcName, { force: true });
cy.getByTestId('approve-warning').should(
cy.getByTestId('approve-default').should(
'contain.text',
`Deposits of ${btcSymbol} not approved`
`Before you can make a deposit of your chosen asset, ${btcSymbol}, you need to approve its use in your Ethereum wallet`
);
cy.getByTestId(depositSubmit).click();
cy.getByTestId('dialog-title').should('contain.text', 'Approve complete');
cy.get('[data-testid="Return to deposit"]').click();
cy.getByTestId(approveSubmit).click();
cy.getByTestId('approve-pending').should('exist');
cy.getByTestId('approve-confirmed').should('exist');
cy.get(amountField).clear().type('10');
cy.getByTestId(depositSubmit).click();
cy.getByTestId(toastContent, txTimeout).should(
+14 -7
View File
@@ -31,7 +31,13 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
it('handles empty fields', () => {
cy.getByTestId('deposit-submit').click();
cy.getByTestId(formFieldError).should('contain.text', 'Required');
cy.getByTestId(formFieldError).should('have.length', 2);
// once Ethereum wallet is connected and key selected the only field that will
// error is the asset select
cy.getByTestId(formFieldError).should('have.length', 1);
cy.get('[data-testid="input-error-text"][aria-describedby="asset"]').should(
'have.length',
1
);
});
it('unable to select assets not enabled', () => {
@@ -41,12 +47,13 @@ describe('deposit form validation', { tags: '@smoke' }, () => {
cy.get(assetSelectField + ' option:contains(Asset 4)').should('not.exist');
});
it('invalid public key', () => {
cy.get(toAddressField)
.clear()
.type('INVALID_DEPOSIT_TO_ADDRESS')
.next(`[data-testid="${formFieldError}"]`)
.should('have.text', 'Invalid Vega key');
it('invalid public key when entering address manually', () => {
cy.getByTestId('enter-pubkey-manually').click();
cy.get(toAddressField).clear().type('INVALID_DEPOSIT_TO_ADDRESS');
cy.get(`[data-testid="${formFieldError}"][aria-describedby="to"]`).should(
'have.text',
'Invalid Vega key'
);
});
it('invalid amount', () => {
@@ -151,7 +151,7 @@ describe('ethereum wallet', { tags: '@smoke' }, () => {
cy.getByTestId('Deposits').click();
cy.getByTestId('deposit-button').click();
connectEthereumWallet('MetaMask');
cy.get('#ethereum-address').should('have.value', ethWalletAddress);
cy.getByTestId('ethereum-address').should('have.text', ethWalletAddress);
cy.getByTestId('disconnect-ethereum-wallet')
.should('have.text', 'Disconnect')
.click();
@@ -111,11 +111,11 @@ export const TradeMarketHeader = ({
</div>
</HeaderStat>
) : null}
<MarketProposalNotification marketId={market?.id} />
<MarketLiquiditySupplied
marketId={market?.id}
assetDecimals={asset?.decimals || 0}
/>
<MarketProposalNotification marketId={market?.id} />
</Header>
);
};
@@ -177,22 +177,14 @@ export const useEthereumTransactionToasts = () => {
store.remove,
]);
const [dismissTx, deleteTx] = useEthTransactionStore((state) => [
state.dismiss,
state.delete,
]);
const dismissTx = useEthTransactionStore((state) => state.dismiss);
const onClose = useCallback(
(tx: EthStoredTxState) => () => {
const safeToDelete = isFinal(tx);
if (safeToDelete) {
deleteTx(tx.id);
} else {
dismissTx(tx.id);
}
dismissTx(tx.id);
removeToast(`eth-${tx.id}`);
},
[deleteTx, dismissTx, removeToast]
[dismissTx, removeToast]
);
const fromEthTransaction = useCallback(
+3
View File
@@ -104,6 +104,7 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
/>
<AgGridColumn
headerName={t('Total')}
type="rightAligned"
field="deposited"
headerTooltip={t(
'This is the total amount of collateral used plus the amount available in your general account.'
@@ -121,6 +122,7 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
/>
<AgGridColumn
headerName={t('Used')}
type="rightAligned"
field="used"
headerTooltip={t(
'This is the amount of collateral used from your general account.'
@@ -139,6 +141,7 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
<AgGridColumn
headerName={t('Available')}
field="available"
type="rightAligned"
headerTooltip={t(
'This is the amount of collateral available in your general account.'
)}
+1 -2
View File
@@ -61,10 +61,9 @@ export const SetFilter = forwardRef((props: IFilterParams, ref) => {
<div className="ag-filter-body-wrapper">
<fieldset className="ag-simple-filter-body-wrapper">
{Object.keys(props.colDef.filterParams.set).map((key) => (
<label className="flex">
<label className="flex" key={key}>
<input
type="checkbox"
key={key}
value={key}
className="mr-1"
checked={value.includes(key)}
@@ -22,7 +22,11 @@ export const ExpirySelector = ({
const dateFormatted = formatForInput(date);
const minDate = formatForInput(date);
return (
<FormGroup label={t('Expiry time/date')} labelFor="expiration">
<FormGroup
label={t('Expiry time/date')}
labelFor="expiration"
compact={true}
>
<Input
data-testid="date-picker-field"
id="expiration"
@@ -26,7 +26,11 @@ export const SideSelector = ({ value, onSelect }: SideSelectorProps) => {
};
return (
<FormGroup label={t('Direction')} labelFor="order-side-toggle">
<FormGroup
label={t('Direction')}
labelFor="order-side-toggle"
compact={true}
>
<Toggle
id="order-side-toggle"
name="order-side"
@@ -119,7 +119,11 @@ export const TimeInForceSelector = ({
};
return (
<FormGroup label={t('Time in force')} labelFor="select-time-in-force">
<FormGroup
label={t('Time in force')}
labelFor="select-time-in-force"
compact={true}
>
<Select
id="select-time-in-force"
value={value}
@@ -74,7 +74,7 @@ export const TypeSelector = ({
};
return (
<FormGroup label={t('Order type')} labelFor="order-type">
<FormGroup label={t('Order type')} labelFor="order-type" compact={true}>
<Toggle
id="order-type"
name="order-type"
@@ -0,0 +1,215 @@
import type { Asset } from '@vegaprotocol/assets';
import { useEnvironment } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import { ExternalLink, Intent, Notification } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '@vegaprotocol/utils';
import type { EthStoredTxState } from '@vegaprotocol/web3';
import { EthTxStatus, useEthTransactionStore } from '@vegaprotocol/web3';
import BigNumber from 'bignumber.js';
import type { DepositBalances } from './use-deposit-balances';
interface ApproveNotificationProps {
isActive: boolean;
selectedAsset?: Asset;
onApprove: () => void;
approved: boolean;
balances: DepositBalances | null;
amount: string;
approveTxId: number | null;
}
export const ApproveNotification = ({
isActive,
selectedAsset,
onApprove,
amount,
balances,
approved,
approveTxId,
}: ApproveNotificationProps) => {
const tx = useEthTransactionStore((state) => {
return state.transactions.find((t) => t?.id === approveTxId);
});
if (!isActive) {
return null;
}
if (!selectedAsset) {
return null;
}
if (!balances) {
return null;
}
const approvePrompt = (
<div className="mb-4">
<Notification
intent={Intent.Warning}
testId="approve-default"
message={t(
`Before you can make a deposit of your chosen asset, ${selectedAsset?.symbol}, you need to approve its use in your Ethereum wallet`
)}
buttonProps={{
size: 'sm',
text: `Approve ${selectedAsset?.symbol}`,
action: onApprove,
dataTestId: 'approve-submit',
}}
/>
</div>
);
const reApprovePrompt = (
<div className="mb-4">
<Notification
intent={Intent.Warning}
testId="reapprove-default"
message={t(
`Approve again to deposit more than ${formatNumber(
balances.allowance.toString()
)}`
)}
buttonProps={{
size: 'sm',
text: `Approve ${selectedAsset?.symbol}`,
action: onApprove,
dataTestId: 'reapprove-submit',
}}
/>
</div>
);
const approvalFeedback = (
<ApprovalTxFeedback
tx={tx}
selectedAsset={selectedAsset}
allowance={balances.allowance}
/>
);
// always show requested and pending states
if (
tx &&
[EthTxStatus.Requested, EthTxStatus.Pending, EthTxStatus.Complete].includes(
tx.status
)
) {
return approvalFeedback;
}
if (!approved) {
return approvePrompt;
}
if (new BigNumber(amount).isGreaterThan(balances.allowance)) {
return reApprovePrompt;
}
if (
tx &&
tx.status === EthTxStatus.Error &&
// @ts-ignore tx.error not typed correctly
tx.error.code === 'ACTION_REJECTED'
) {
return approvePrompt;
}
return approvalFeedback;
};
const ApprovalTxFeedback = ({
tx,
selectedAsset,
allowance,
}: {
tx: EthStoredTxState | undefined;
selectedAsset: Asset;
allowance?: BigNumber;
}) => {
const { ETHERSCAN_URL } = useEnvironment();
if (!tx) return null;
const txLink = tx.txHash && (
<ExternalLink href={`${ETHERSCAN_URL}/tx/${tx.txHash}`}>
{t('View on Etherscan')}
</ExternalLink>
);
if (tx.status === EthTxStatus.Error) {
return (
<div className="mb-4">
<Notification
intent={Intent.Danger}
testId="approve-error"
message={
<p>
{t('Approval failed')} {txLink}
</p>
}
/>
</div>
);
}
if (tx.status === EthTxStatus.Requested) {
return (
<div className="mb-4">
<Notification
intent={Intent.Warning}
testId="approve-requested"
message={t(
`Go to your Ethereum wallet and approve the transaction to enable the use of ${selectedAsset?.symbol}`
)}
/>
</div>
);
}
if (tx.status === EthTxStatus.Pending) {
return (
<div className="mb-4">
<Notification
intent={Intent.Primary}
testId="approve-pending"
message={
<>
<p>
{t(
`Your ${selectedAsset?.symbol} is being confirmed by the Ethereum network. When this is complete, you can continue your deposit`
)}{' '}
</p>
{txLink && <p>{txLink}</p>}
</>
}
/>
</div>
);
}
if (tx.status === EthTxStatus.Confirmed) {
return (
<div className="mb-4">
<Notification
intent={Intent.Success}
testId="approve-confirmed"
message={
<>
<p>
{t(
`You can now make deposits in ${
selectedAsset?.symbol
}, up to a maximum of ${formatNumber(
allowance?.toString() || 0
)}`
)}
</p>
{txLink && <p>{txLink}</p>}
</>
}
/>
</div>
);
}
return null;
};
+1 -9
View File
@@ -4,18 +4,11 @@ import { DepositManager } from './deposit-manager';
import { t } from '@vegaprotocol/i18n';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import { enabledAssetsProvider } from '@vegaprotocol/assets';
import type { DepositDialogStylePropsSetter } from './deposit-dialog';
/**
* Fetches data required for the Deposit page
*/
export const DepositContainer = ({
assetId,
setDialogStyleProps,
}: {
assetId?: string;
setDialogStyleProps?: DepositDialogStylePropsSetter;
}) => {
export const DepositContainer = ({ assetId }: { assetId?: string }) => {
const { VEGA_ENV } = useEnvironment();
const { data, loading, error } = useDataProvider({
dataProvider: enabledAssetsProvider,
@@ -28,7 +21,6 @@ export const DepositContainer = ({
assetId={assetId}
assets={data}
isFaucetable={VEGA_ENV !== Networks.MAINNET}
setDialogStyleProps={setDialogStyleProps}
/>
) : (
<Splash>
+2 -32
View File
@@ -1,8 +1,6 @@
import { create } from 'zustand';
import { t } from '@vegaprotocol/i18n';
import type { Intent } from '@vegaprotocol/ui-toolkit';
import { Dialog } from '@vegaprotocol/ui-toolkit';
import { useCallback, useState } from 'react';
import { DepositContainer } from './deposit-container';
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
@@ -24,22 +22,6 @@ export const useDepositDialog = create<State & Actions>((set) => ({
close: () => set(() => ({ assetId: undefined, isOpen: false })),
}));
export type DepositDialogStyleProps = {
title: string;
icon?: JSX.Element;
intent?: Intent;
};
export type DepositDialogStylePropsSetter = (
props?: DepositDialogStyleProps
) => void;
const DEFAULT_STYLE: DepositDialogStyleProps = {
title: t('Deposit'),
intent: undefined,
icon: undefined,
};
export const DepositDialog = () => {
const { assetId, isOpen, open, close } = useDepositDialog();
const assetDetailsDialogOpen = useAssetDetailsDialogStore(
@@ -48,25 +30,13 @@ export const DepositDialog = () => {
const connectWalletDialogIsOpen = useWeb3ConnectStore(
(state) => state.isOpen
);
const [dialogStyleProps, _setDialogStyleProps] = useState(DEFAULT_STYLE);
const setDialogStyleProps: DepositDialogStylePropsSetter =
useCallback<DepositDialogStylePropsSetter>(
(props) =>
props
? _setDialogStyleProps(props)
: _setDialogStyleProps(DEFAULT_STYLE),
[_setDialogStyleProps]
);
return (
<Dialog
open={isOpen && !(connectWalletDialogIsOpen || assetDetailsDialogOpen)}
onChange={(isOpen) => (isOpen ? open() : close())}
{...dialogStyleProps}
title={t('Deposit')}
>
<DepositContainer
assetId={assetId}
setDialogStyleProps={setDialogStyleProps}
/>
<DepositContainer assetId={assetId} />
</Dialog>
);
};
+40 -23
View File
@@ -7,6 +7,7 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
import { useWeb3React } from '@web3-react/core';
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import type { DepositBalances } from './use-deposit-balances';
jest.mock('@vegaprotocol/wallet');
jest.mock('@vegaprotocol/web3');
@@ -38,27 +39,34 @@ function generateAsset(): AssetFieldsFragment {
let asset: AssetFieldsFragment;
let props: DepositFormProps;
let balances: DepositBalances;
const MOCK_ETH_ADDRESS = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
const MOCK_VEGA_KEY =
'70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680';
beforeEach(() => {
asset = generateAsset();
balances = {
balance: new BigNumber(5),
max: new BigNumber(20),
allowance: new BigNumber(30),
deposited: new BigNumber(10),
};
props = {
assets: [asset],
selectedAsset: undefined,
onSelectAsset: jest.fn(),
balance: new BigNumber(5),
balances,
submitApprove: jest.fn(),
submitDeposit: jest.fn(),
requestFaucet: jest.fn(),
max: new BigNumber(20),
deposited: new BigNumber(10),
allowance: new BigNumber(30),
submitFaucet: jest.fn(),
onDisconnect: jest.fn(),
approveTxId: null,
faucetTxId: null,
isFaucetable: true,
};
(useVegaWallet as jest.Mock).mockReturnValue({ pubKey: null });
(useVegaWallet as jest.Mock).mockReturnValue({ pubKey: null, pubKeys: [] });
(useWeb3React as jest.Mock).mockReturnValue({
isActive: true,
account: MOCK_ETH_ADDRESS,
@@ -71,7 +79,8 @@ describe('Deposit form', () => {
render(<DepositForm {...props} />);
// Assert default values (including) from/to provided by useVegaWallet and useWeb3React
expect(screen.getByLabelText('From (Ethereum address)')).toHaveValue(
expect(screen.getByText('From (Ethereum address)')).toBeInTheDocument();
expect(screen.getByTestId('ethereum-address')).toHaveTextContent(
MOCK_ETH_ADDRESS
);
expect(screen.getByLabelText('Asset')).toHaveValue('');
@@ -145,7 +154,7 @@ describe('Deposit form', () => {
fireEvent.submit(screen.getByTestId('deposit-form'));
expect(
await screen.findByText('Amount is above deposit limit')
await screen.findByText('Amount is above lifetime deposit limit')
).toBeInTheDocument();
});
@@ -153,9 +162,12 @@ describe('Deposit form', () => {
render(
<DepositForm
{...props}
balance={new BigNumber(100)}
max={new BigNumber(100)}
deposited={new BigNumber(10)}
balances={{
...balances,
balance: BigNumber(100),
max: new BigNumber(100),
deposited: new BigNumber(10),
}}
/>
);
@@ -166,7 +178,7 @@ describe('Deposit form', () => {
fireEvent.submit(screen.getByTestId('deposit-form'));
expect(
await screen.findByText('Amount is above approved amount.')
await screen.findByText('Amount is above approved amount')
).toBeInTheDocument();
});
@@ -214,14 +226,17 @@ describe('Deposit form', () => {
render(
<DepositForm
{...props}
allowance={new BigNumber(0)}
balances={{
...balances,
allowance: new BigNumber(0),
}}
selectedAsset={asset}
/>
);
expect(screen.queryByLabelText('Amount')).not.toBeInTheDocument();
expect(screen.getByTestId('approve-warning')).toHaveTextContent(
`Deposits of ${asset.symbol} not approved`
expect(screen.getByTestId('approve-default')).toHaveTextContent(
`Before you can make a deposit of your chosen asset, ${asset.symbol}, you need to approve its use in your Ethereum wallet`
);
fireEvent.click(
@@ -253,10 +268,12 @@ describe('Deposit form', () => {
render(
<DepositForm
{...props}
allowance={new BigNumber(100)}
balance={balance}
max={max}
deposited={deposited}
balances={{
allowance: new BigNumber(100),
balance,
max,
deposited,
}}
selectedAsset={asset}
/>
);
@@ -320,10 +337,10 @@ describe('Deposit form', () => {
expect(
screen.queryByRole('button', { name: 'Connect' })
).not.toBeInTheDocument();
const fromInput = screen.getByLabelText('From (Ethereum address)');
expect(fromInput).toHaveValue(MOCK_ETH_ADDRESS);
expect(fromInput).toBeDisabled();
expect(fromInput).toHaveAttribute('readonly');
expect(screen.getByText('From (Ethereum address)')).toBeInTheDocument();
expect(screen.getByTestId('ethereum-address')).toHaveTextContent(
MOCK_ETH_ADDRESS
);
});
it('prevents submission if you are on the wrong chain', () => {
+159 -146
View File
@@ -1,4 +1,4 @@
import type { Asset } from '@vegaprotocol/assets';
import type { Asset, AssetFieldsFragment } from '@vegaprotocol/assets';
import { AssetOption } from '@vegaprotocol/assets';
import {
ethereumAddress,
@@ -19,13 +19,16 @@ import {
RichSelect,
Notification,
Intent,
ButtonLink,
Select,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useWeb3React } from '@web3-react/core';
import BigNumber from 'bignumber.js';
import type { ButtonHTMLAttributes } from 'react';
import type { ButtonHTMLAttributes, ReactNode } from 'react';
import { useState } from 'react';
import { useMemo } from 'react';
import type { FieldError } from 'react-hook-form';
import { useWatch } from 'react-hook-form';
import { Controller, useForm } from 'react-hook-form';
import { DepositLimits } from './deposit-limits';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
@@ -34,6 +37,9 @@ import {
useWeb3ConnectStore,
getChainName,
} from '@vegaprotocol/web3';
import type { DepositBalances } from './use-deposit-balances';
import { FaucetNotification } from './faucet-notification';
import { ApproveNotification } from './approve-notification';
interface FormFields {
asset: string;
@@ -45,38 +51,38 @@ interface FormFields {
export interface DepositFormProps {
assets: Asset[];
selectedAsset?: Asset;
balances: DepositBalances | null;
onSelectAsset: (assetId: string) => void;
balance: BigNumber | undefined;
onDisconnect: () => void;
submitApprove: () => void;
approveTxId: number | null;
submitFaucet: () => void;
faucetTxId: number | null;
submitDeposit: (args: {
assetSource: string;
amount: string;
vegaPublicKey: string;
}) => void;
requestFaucet: () => void;
max: BigNumber | undefined;
deposited: BigNumber | undefined;
allowance: BigNumber | undefined;
isFaucetable?: boolean;
}
export const DepositForm = ({
assets,
selectedAsset,
balances,
onSelectAsset,
balance,
max,
deposited,
onDisconnect,
submitApprove,
submitDeposit,
requestFaucet,
allowance,
submitFaucet,
faucetTxId,
approveTxId,
isFaucetable,
}: DepositFormProps) => {
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
const openDialog = useWeb3ConnectStore((store) => store.open);
const { isActive, account } = useWeb3React();
const { pubKey } = useVegaWallet();
const { pubKey, pubKeys: _pubKeys } = useVegaWallet();
const {
register,
handleSubmit,
@@ -91,43 +97,21 @@ export const DepositForm = ({
},
});
const amount = useWatch({ name: 'amount', control });
const onSubmit = async (fields: FormFields) => {
if (!selectedAsset || selectedAsset.source.__typename !== 'ERC20') {
throw new Error('Invalid asset');
}
if (!approved) throw new Error('Deposits not approved');
if (approved) {
submitDeposit({
assetSource: selectedAsset.source.contractAddress,
amount: fields.amount,
vegaPublicKey: fields.to,
});
} else {
submitApprove();
}
submitDeposit({
assetSource: selectedAsset.source.contractAddress,
amount: fields.amount,
vegaPublicKey: fields.to,
});
};
const maxAmount = useMemo(() => {
const maxApproved = allowance ? allowance : new BigNumber(0);
const maxAvailable = balance ? balance : new BigNumber(0);
// limits.max is a lifetime deposit limit, so the actual max value for form
// input is the max minus whats already been deposited
let maxLimit = new BigNumber(Infinity);
// A max limit of zero indicates that there is no limit
if (max && deposited && max.isGreaterThan(0)) {
maxLimit = max.minus(deposited);
}
return {
approved: maxApproved,
available: maxAvailable,
limit: maxLimit,
amount: BigNumber.minimum(maxLimit, maxApproved, maxAvailable),
};
}, [max, deposited, allowance, balance]);
const min = useMemo(() => {
// Min viable amount given asset decimals EG for WEI 0.000000000000000001
const minViableAmount = selectedAsset
@@ -137,8 +121,15 @@ export const DepositForm = ({
return minViableAmount;
}, [selectedAsset]);
const approved = allowance && allowance.isGreaterThan(0) ? true : false;
const formState = getFormState(selectedAsset, isActive, approved);
const pubKeys = useMemo(() => {
return _pubKeys ? _pubKeys.map((pk) => pk.publicKey) : [];
}, [_pubKeys]);
const approved = balances
? balances.allowance.isGreaterThan(0)
? true
: false
: false;
return (
<form
@@ -166,32 +157,23 @@ export const DepositForm = ({
render={() => {
if (isActive && account) {
return (
<>
<Input
id="ethereum-address"
value={account}
readOnly={true}
disabled={true}
{...register('from', {
validate: {
required,
ethereumAddress,
},
})}
/>
<div className="text-sm" aria-describedby="ethereum-address">
<p className="mb-1" data-testid="ethereum-address">
{account}
</p>
<DisconnectEthereumButton
onDisconnect={() => {
setValue('from', ''); // clear from value so required ethereum connection validation works
onDisconnect();
}}
/>
</>
</div>
);
}
return (
<Button
onClick={openDialog}
variant="primary"
fill={true}
type="button"
data-testid="connect-eth-wallet-btn"
>
@@ -238,7 +220,7 @@ export const DepositForm = ({
</InputError>
)}
{isFaucetable && selectedAsset && (
<UseButton onClick={requestFaucet}>
<UseButton onClick={submitFaucet}>
{t(`Get ${selectedAsset.symbol}`)}
</UseButton>
)}
@@ -255,39 +237,55 @@ export const DepositForm = ({
</button>
)}
</FormGroup>
<FaucetNotification
isActive={isActive}
selectedAsset={selectedAsset}
faucetTxId={faucetTxId}
/>
<FormGroup label={t('To (Vega key)')} labelFor="to">
<Input
{...register('to', { validate: { required, vegaPublicKey } })}
id="to"
<AddressField
pubKeys={pubKeys}
onChange={() => setValue('to', '')}
select={
<Select {...register('to')} id="to" defaultValue="">
<option value="" disabled={true}>
{t('Please select')}
</option>
{pubKeys?.length &&
pubKeys.map((pk) => (
<option key={pk} value={pk}>
{pk}
</option>
))}
</Select>
}
input={
<Input
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus={true} // focus input immediately after is shown
id="to"
type="text"
{...register('to', {
validate: {
required,
vegaPublicKey,
},
})}
/>
}
/>
{errors.to?.message && (
<InputError intent="danger" forInput="to">
{errors.to.message}
</InputError>
)}
{pubKey && (
<UseButton
onClick={() => {
setValue('to', pubKey);
clearErrors('to');
}}
>
{t('Use connected')}
</UseButton>
)}
</FormGroup>
{selectedAsset && max && deposited && (
{selectedAsset && balances && (
<div className="mb-6">
<DepositLimits
max={max}
deposited={deposited}
balance={balance}
asset={selectedAsset}
allowance={allowance}
/>
<DepositLimits {...balances} asset={selectedAsset} />
</div>
)}
{formState === 'deposit' && (
{approved && (
<FormGroup label={t('Amount')} labelFor="amount">
<Input
type="number"
@@ -299,38 +297,52 @@ export const DepositForm = ({
minSafe: (value) => minSafe(new BigNumber(min))(value),
approved: (v) => {
const value = new BigNumber(v);
if (value.isGreaterThan(maxAmount.approved)) {
if (value.isGreaterThan(balances?.allowance || 0)) {
return t('Amount is above approved amount');
}
return true;
},
limit: (v) => {
const value = new BigNumber(v);
if (value.isGreaterThan(maxAmount.limit)) {
return t('Amount is above deposit limit');
if (!balances) {
return t('Could not verify balances of account'); // this should never happen
}
let lifetimeLimit = new BigNumber(Infinity);
if (balances.max.isGreaterThan(0)) {
lifetimeLimit = balances.max.minus(balances.deposited);
}
if (value.isGreaterThan(lifetimeLimit)) {
return t('Amount is above lifetime deposit limit');
}
return true;
},
balance: (v) => {
const value = new BigNumber(v);
if (value.isGreaterThan(maxAmount.available)) {
if (value.isGreaterThan(balances?.balance || 0)) {
return t('Insufficient amount in Ethereum wallet');
}
return true;
},
maxSafe: (v) => {
return maxSafe(maxAmount.amount)(v);
return maxSafe(balances?.balance || new BigNumber(0))(v);
},
},
})}
/>
{errors.amount?.message && (
<AmountError error={errors.amount} submitApprove={submitApprove} />
<InputError intent="danger" forInput="amount">
{errors.amount.message}
</InputError>
)}
{selectedAsset && balance && (
{selectedAsset && balances && (
<UseButton
onClick={() => {
setValue('amount', balance.toFixed(selectedAsset.decimals));
setValue(
'amount',
balances.balance.toFixed(selectedAsset.decimals)
);
clearErrors('amount');
}}
>
@@ -339,59 +351,31 @@ export const DepositForm = ({
)}
</FormGroup>
)}
<FormButton selectedAsset={selectedAsset} formState={formState} />
<ApproveNotification
isActive={isActive}
approveTxId={approveTxId}
selectedAsset={selectedAsset}
onApprove={submitApprove}
balances={balances}
approved={approved}
amount={amount}
/>
<FormButton approved={approved} selectedAsset={selectedAsset} />
</form>
);
};
const AmountError = ({
error,
submitApprove,
}: {
error: FieldError;
submitApprove: () => void;
}) => {
if (error.type === 'approved') {
return (
<InputError intent="danger" forInput="amount">
{error.message}.
<button onClick={submitApprove} className="underline ml-2">
{t('Update approve amount')}
</button>
</InputError>
);
}
return (
<InputError intent="danger" forInput="amount">
{error.message}
</InputError>
);
};
interface FormButtonProps {
selectedAsset?: Asset;
formState: ReturnType<typeof getFormState>;
approved: boolean;
selectedAsset: AssetFieldsFragment | undefined;
}
const FormButton = ({ selectedAsset, formState }: FormButtonProps) => {
const FormButton = ({ approved, selectedAsset }: FormButtonProps) => {
const { isActive, chainId } = useWeb3React();
const desiredChainId = useWeb3ConnectStore((store) => store.desiredChainId);
const submitText =
formState === 'approve'
? t(`Approve ${selectedAsset ? selectedAsset.symbol : ''}`)
: t('Deposit');
const invalidChain = isActive && chainId !== desiredChainId;
return (
<>
{formState === 'approve' && (
<div className="mb-2">
<Notification
intent={Intent.Warning}
testId="approve-warning"
message={t(`Deposits of ${selectedAsset?.symbol} not approved`)}
/>
</div>
)}
{invalidChain && (
<div className="mb-2">
<Notification
@@ -408,9 +392,9 @@ const FormButton = ({ selectedAsset, formState }: FormButtonProps) => {
data-testid="deposit-submit"
variant={isActive ? 'primary' : 'default'}
fill={true}
disabled={invalidChain}
disabled={invalidChain || (selectedAsset && !approved)}
>
{submitText}
{t('Deposit')}
</Button>
</>
);
@@ -437,7 +421,7 @@ const DisconnectEthereumButton = ({
const [, , removeEagerConnector] = useLocalStorage(ETHEREUM_EAGER_CONNECT);
return (
<UseButton
<ButtonLink
onClick={() => {
connector.deactivate();
removeEagerConnector();
@@ -446,17 +430,46 @@ const DisconnectEthereumButton = ({
data-testid="disconnect-ethereum-wallet"
>
{t('Disconnect')}
</UseButton>
</ButtonLink>
);
};
const getFormState = (
selectedAsset: Asset | undefined,
isActive: boolean,
approved: boolean
) => {
if (!selectedAsset) return 'deposit';
if (!isActive) return 'deposit';
if (approved) return 'deposit';
return 'approve';
interface AddressInputProps {
pubKeys: string[] | null;
select: ReactNode;
input: ReactNode;
onChange: () => void;
}
export const AddressField = ({
pubKeys,
select,
input,
onChange,
}: AddressInputProps) => {
const [isInput, setIsInput] = useState(() => {
if (pubKeys && pubKeys.length <= 1) {
return true;
}
return false;
});
return (
<>
{isInput ? input : select}
{pubKeys && pubKeys.length > 1 && (
<button
type="button"
onClick={() => {
setIsInput((curr) => !curr);
onChange();
}}
className="ml-auto text-sm absolute top-0 right-0 underline"
data-testid="enter-pubkey-manually"
>
{isInput ? t('Select from wallet') : t('Enter manually')}
</button>
)}
</>
);
};
+1 -1
View File
@@ -35,7 +35,7 @@ export const DepositLimits = ({
},
{
key: 'MAX_LIMIT',
label: t('Maximum total deposit amount'),
label: t('Lifetime deposit allowance'),
rawValue: max,
value: <CompactNumber number={max} decimals={asset.decimals} />,
},
+23 -50
View File
@@ -5,36 +5,26 @@ import { prepend0x } from '@vegaprotocol/smart-contracts';
import sortBy from 'lodash/sortBy';
import { useSubmitApproval } from './use-submit-approval';
import { useSubmitFaucet } from './use-submit-faucet';
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { useDepositBalances } from './use-deposit-balances';
import { useDepositDialog } from './deposit-dialog';
import type { Asset } from '@vegaprotocol/assets';
import type { DepositDialogStylePropsSetter } from './deposit-dialog';
import pick from 'lodash/pick';
import type { EthTransaction } from '@vegaprotocol/web3';
import {
EthTxStatus,
useEthTransactionStore,
useBridgeContract,
useEthereumConfig,
} from '@vegaprotocol/web3';
import { t } from '@vegaprotocol/i18n';
interface DepositManagerProps {
assetId?: string;
assets: Asset[];
isFaucetable: boolean;
setDialogStyleProps?: DepositDialogStylePropsSetter;
}
const getProps = (txContent?: EthTransaction['TxContent']) =>
txContent ? pick(txContent, ['title', 'icon', 'intent']) : undefined;
export const DepositManager = ({
assetId: initialAssetId,
assets,
isFaucetable,
setDialogStyleProps,
}: DepositManagerProps) => {
const createEthTransaction = useEthTransactionStore((state) => state.create);
const { config } = useEthereumConfig();
@@ -43,26 +33,16 @@ export const DepositManager = ({
const bridgeContract = useBridgeContract();
const closeDepositDialog = useDepositDialog((state) => state.close);
const { balance, allowance, deposited, max, refresh } = useDepositBalances(
const { getBalances, reset, balances } = useDepositBalances(
asset,
isFaucetable
);
// Set up approve transaction
const approve = useSubmitApproval(asset);
const approve = useSubmitApproval(asset, getBalances);
// Set up faucet transaction
const faucet = useSubmitFaucet(asset);
const transactionInProgress = [approve.TxContent, faucet.TxContent].filter(
(t) => t.status !== EthTxStatus.Default
)[0];
useEffect(() => {
setDialogStyleProps?.(getProps(transactionInProgress));
}, [setDialogStyleProps, transactionInProgress]);
const returnLabel = t('Return to deposit');
const faucet = useSubmitFaucet(asset, getBalances);
const submitDeposit = (
args: Parameters<DepositFormProps['submitDeposit']>['0']
@@ -86,31 +66,24 @@ export const DepositManager = ({
};
return (
<>
{!transactionInProgress && (
<DepositForm
balance={balance}
selectedAsset={asset}
onSelectAsset={setAssetId}
assets={sortBy(assets, 'name')}
submitApprove={async () => {
await approve.perform();
refresh();
}}
submitDeposit={submitDeposit}
requestFaucet={async () => {
await faucet.perform();
refresh();
}}
deposited={deposited}
max={max}
allowance={allowance}
isFaucetable={isFaucetable}
/>
)}
<approve.TxContent.Content returnLabel={returnLabel} />
<faucet.TxContent.Content returnLabel={returnLabel} />
</>
<DepositForm
selectedAsset={asset}
onDisconnect={reset}
onSelectAsset={(id) => {
setAssetId(id);
// When we change asset, also clear the tracked faucet/approve transactions so
// we dont render stale UI
approve.reset();
faucet.reset();
}}
assets={sortBy(assets, 'name')}
submitApprove={approve.perform}
submitDeposit={submitDeposit}
submitFaucet={faucet.perform}
faucetTxId={faucet.id}
approveTxId={approve.id}
balances={balances}
isFaucetable={isFaucetable}
/>
);
};
@@ -0,0 +1,108 @@
import type { Asset } from '@vegaprotocol/assets';
import { useEnvironment } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import { ExternalLink, Intent, Notification } from '@vegaprotocol/ui-toolkit';
import { EthTxStatus, useEthTransactionStore } from '@vegaprotocol/web3';
interface FaucetNotificationProps {
isActive: boolean;
selectedAsset?: Asset;
faucetTxId: number | null;
}
/**
* Render a notification for the faucet transaction
*/
export const FaucetNotification = ({
isActive,
selectedAsset,
faucetTxId,
}: FaucetNotificationProps) => {
const { ETHERSCAN_URL } = useEnvironment();
const tx = useEthTransactionStore((state) => {
return state.transactions.find((t) => t?.id === faucetTxId);
});
if (!isActive) {
return null;
}
if (!selectedAsset) {
return null;
}
if (!tx) {
return null;
}
if (tx.status === EthTxStatus.Error) {
return (
<div className="mb-4">
<Notification
intent={Intent.Danger}
testId="faucet-error"
// @ts-ignore tx.error not typed correctly
message={t(`Faucet failed: ${tx.error?.reason}`)}
/>
</div>
);
}
if (tx.status === EthTxStatus.Requested) {
return (
<div className="mb-4">
<Notification
intent={Intent.Warning}
testId="faucet-requested"
message={t(
`Go to your Ethereum wallet and approve the faucet transaction for ${selectedAsset?.symbol}`
)}
/>
</div>
);
}
if (tx.status === EthTxStatus.Pending) {
return (
<div className="mb-4">
<Notification
intent={Intent.Primary}
testId="faucet-pending"
message={
<p>
{t('Faucet pending...')}{' '}
{tx.txHash && (
<ExternalLink href={`${ETHERSCAN_URL}/tx/${tx.txHash}`}>
{t('View on Etherscan')}
</ExternalLink>
)}
</p>
}
/>
</div>
);
}
if (tx.status === EthTxStatus.Confirmed) {
return (
<div className="mb-4">
<Notification
intent={Intent.Success}
testId="faucet-confirmed"
message={
<p>
{t('Faucet successful')}{' '}
{tx.txHash && (
<ExternalLink href={`${ETHERSCAN_URL}/tx/${tx.txHash}`}>
{t('View on Etherscan')}
</ExternalLink>
)}
</p>
}
/>
</div>
);
}
return null;
};
+16 -22
View File
@@ -7,21 +7,17 @@ import { useGetBalanceOfERC20Token } from './use-get-balance-of-erc20-token';
import { useGetDepositMaximum } from './use-get-deposit-maximum';
import { useGetDepositedAmount } from './use-get-deposited-amount';
import { isAssetTypeERC20 } from '@vegaprotocol/utils';
import { usePrevious } from '@vegaprotocol/react-helpers';
import { useAccountBalance } from '@vegaprotocol/accounts';
import type { Asset } from '@vegaprotocol/assets';
type DepositBalances = {
balance: BigNumber;
allowance: BigNumber;
deposited: BigNumber;
max: BigNumber;
refresh: () => void;
};
export interface DepositBalances {
balance: BigNumber; // amount in Ethereum wallet
allowance: BigNumber; // amount approved
deposited: BigNumber; // total amounted deposited over lifetime
max: BigNumber; // life time deposit cap
}
type DepositBalancesState = Omit<DepositBalances, 'refresh'>;
const initialState: DepositBalancesState = {
const initialState: DepositBalances = {
balance: new BigNumber(0),
allowance: new BigNumber(0),
deposited: new BigNumber(0),
@@ -35,7 +31,7 @@ const initialState: DepositBalancesState = {
export const useDepositBalances = (
asset: Asset | undefined,
isFaucetable: boolean
): DepositBalances => {
) => {
const tokenContract = useTokenContract(
isAssetTypeERC20(asset) ? asset.source.contractAddress : undefined,
isFaucetable
@@ -45,21 +41,14 @@ export const useDepositBalances = (
const getBalance = useGetBalanceOfERC20Token(tokenContract, asset);
const getDepositMaximum = useGetDepositMaximum(bridgeContract, asset);
const getDepositedAmount = useGetDepositedAmount(asset);
const prevAsset = usePrevious(asset);
const [state, setState] = useState<DepositBalancesState>(initialState);
useEffect(() => {
if (asset?.id !== prevAsset?.id) {
// reset values to initial state when asset changes
setState(initialState);
}
}, [asset?.id, prevAsset?.id]);
const [state, setState] = useState<DepositBalances | null>(null);
const { accountBalance } = useAccountBalance(asset?.id);
const getBalances = useCallback(async () => {
if (!asset) return;
try {
setState(null);
const [max, deposited, balance, allowance] = await Promise.all([
getDepositMaximum(),
getDepositedAmount(),
@@ -75,12 +64,17 @@ export const useDepositBalances = (
});
} catch (err) {
Sentry.captureException(err);
setState(null);
}
}, [asset, getAllowance, getBalance, getDepositMaximum, getDepositedAmount]);
const reset = useCallback(() => {
setState(null);
}, []);
useEffect(() => {
getBalances();
}, [asset, getBalances, accountBalance]);
return { ...state, refresh: getBalances };
return { balances: state, getBalances, reset };
};
+31 -19
View File
@@ -1,36 +1,48 @@
import { isAssetTypeERC20, removeDecimal } from '@vegaprotocol/utils';
import * as Sentry from '@sentry/react';
import type { Token } from '@vegaprotocol/smart-contracts';
import {
EthTxStatus,
useEthereumConfig,
useEthereumTransaction,
useEthTransactionStore,
useTokenContract,
} from '@vegaprotocol/web3';
import type { Asset } from '@vegaprotocol/assets';
import { useEffect, useState } from 'react';
export const useSubmitApproval = (asset?: Asset) => {
export const useSubmitApproval = (
asset: Asset | undefined,
getBalances: () => void
) => {
const [id, setId] = useState<number | null>(null);
const createEthTransaction = useEthTransactionStore((state) => state.create);
const tx = useEthTransactionStore((state) => {
return state.transactions.find((t) => t?.id === id);
});
const { config } = useEthereumConfig();
const contract = useTokenContract(
isAssetTypeERC20(asset) ? asset.source.contractAddress : undefined,
true
);
const transaction = useEthereumTransaction<Token, 'approve'>(
contract,
'approve'
);
// When tx is confirmed refresh balances
useEffect(() => {
if (tx?.status === EthTxStatus.Confirmed) {
getBalances();
}
}, [tx?.status, getBalances]);
return {
...transaction,
perform: async () => {
id,
reset: () => {
setId(null);
},
perform: () => {
if (!asset || !config) return;
try {
const amount = removeDecimal('1000000', asset.decimals);
await transaction.perform(
config.collateral_bridge_contract.address,
amount
);
} catch (err) {
Sentry.captureException(err);
}
const amount = removeDecimal('1000000', asset.decimals);
const id = createEthTransaction(contract, 'approve', [
config?.collateral_bridge_contract.address,
amount,
]);
setId(id);
},
};
};
+30 -15
View File
@@ -1,26 +1,41 @@
import type { TokenFaucetable } from '@vegaprotocol/smart-contracts';
import * as Sentry from '@sentry/react';
import { useEthereumTransaction, useTokenContract } from '@vegaprotocol/web3';
import {
EthTxStatus,
useEthTransactionStore,
useTokenContract,
} from '@vegaprotocol/web3';
import { isAssetTypeERC20 } from '@vegaprotocol/utils';
import type { Asset } from '@vegaprotocol/assets';
import { useEffect, useState } from 'react';
export const useSubmitFaucet = (asset?: Asset) => {
export const useSubmitFaucet = (
asset: Asset | undefined,
getBalances: () => void
) => {
const [id, setId] = useState<number | null>(null);
const createEthTransaction = useEthTransactionStore((state) => state.create);
const tx = useEthTransactionStore((state) => {
return state.transactions.find((t) => t?.id === id);
});
const contract = useTokenContract(
isAssetTypeERC20(asset) ? asset.source.contractAddress : undefined,
true
);
const transaction = useEthereumTransaction<TokenFaucetable, 'faucet'>(
contract,
'faucet'
);
// When tx is confirmed refresh balances
useEffect(() => {
if (tx?.status === EthTxStatus.Confirmed) {
getBalances();
}
}, [tx?.status, getBalances]);
return {
...transaction,
perform: async () => {
try {
await transaction.perform();
} catch (err) {
Sentry.captureException(err);
}
id,
reset: () => {
setId(null);
},
perform: () => {
const id = createEthTransaction(contract, 'faucet', []);
setId(id);
},
};
};
@@ -27,6 +27,7 @@ export const RowData = ({
highestBlock,
onBlockHeight,
}: RowDataProps) => {
const [subFailed, setSubFailed] = useState(false);
const [time, setTime] = useState<number>();
// no use of data here as we need the data nodes reference to block height
const { data, error, loading, startPolling, stopPolling } =
@@ -46,9 +47,18 @@ export const RowData = ({
} = useBlockTimeSubscription();
useEffect(() => {
// stop polling if row has errored
}, [error, stopPolling]);
const timeout = setTimeout(() => {
if (!subData) {
setSubFailed(true);
}
}, 3000);
return () => {
clearTimeout(timeout);
};
}, [subData]);
// handle polling
useEffect(() => {
const handleStartPoll = () => startPolling(POLL_INTERVAL);
const handleStopPoll = () => stopPolling();
@@ -68,6 +78,7 @@ export const RowData = ({
};
}, [startPolling, stopPolling, error]);
// measure response time
useEffect(() => {
if (!isValidUrl(url)) return;
// every time we get data measure response speed
@@ -131,6 +142,15 @@ export const RowData = ({
return false;
};
const getSubFailed = (
subError: ApolloError | undefined,
subFailed: boolean
) => {
if (subError) return true;
if (subFailed) return true;
return false;
};
return (
<>
{id !== CUSTOM_NODE_KEY && (
@@ -161,11 +181,11 @@ export const RowData = ({
</LayoutCell>
<LayoutCell
label={t('Subscription')}
isLoading={subLoading}
hasError={Boolean(subError)}
isLoading={subFailed ? false : subLoading}
hasError={getSubFailed(subError, subFailed)}
dataTestId="subscription-cell"
>
{getSubscriptionDisplayValue(subData?.busEvents, subError)}
{getSubscriptionDisplayValue(subFailed, subData?.busEvents, subError)}
</LayoutCell>
</>
);
@@ -195,10 +215,11 @@ const getBlockDisplayValue = (block?: number, error?: ApolloError) => {
};
const getSubscriptionDisplayValue = (
subFailed: boolean,
events?: { id: string }[] | null,
error?: ApolloError
) => {
if (error) {
if (subFailed || error) {
return t('No');
}
if (events?.length) {
+34 -2
View File
@@ -5,7 +5,7 @@ import * as Schema from '@vegaprotocol/types';
import type { PartialDeep } from 'type-fest';
import type { Trade } from './fills-data-provider';
import { FillsTable } from './fills-table';
import { FillsTable, getFeesBreakdown } from './fills-table';
import { generateFill } from './test-helpers';
describe('FillsTable', () => {
@@ -75,7 +75,7 @@ describe('FillsTable', () => {
'1.00 BTC',
'3.00 BTC',
'Maker',
'0.06 BTC',
'2.00 BTC',
getDateTimeFormat().format(new Date(buyerFill.createdAt)),
];
cells.forEach((cell, i) => {
@@ -176,4 +176,36 @@ describe('FillsTable', () => {
await screen.findByTestId('fee-breakdown-tooltip')
).toBeInTheDocument();
});
describe('getFeesBreakdown', () => {
it('should return correct fees breakdown for a taker', () => {
const fees = {
makerFee: '1000',
infrastructureFee: '2000',
liquidityFee: '3000',
};
const expectedBreakdown = {
infrastructureFee: '2000',
liquidityFee: '3000',
makerFee: '1000',
totalFee: '6000',
};
expect(getFeesBreakdown('TAKER', fees)).toEqual(expectedBreakdown);
});
it('should return correct fees breakdown for a maker', () => {
const fees = {
makerFee: '1000',
infrastructureFee: '2000',
liquidityFee: '3000',
};
const expectedBreakdown = {
infrastructureFee: '2000',
liquidityFee: '3000',
makerFee: '-1000',
totalFee: '4000',
};
expect(getFeesBreakdown('MAKER', fees)).toEqual(expectedBreakdown);
});
});
});
+101 -31
View File
@@ -27,6 +27,10 @@ import type {
import { forwardRef } from 'react';
import BigNumber from 'bignumber.js';
import type { Trade } from './fills-data-provider';
import type { FillFieldsFragment } from './__generated__/Fills';
const TAKER = 'TAKER';
const MAKER = 'MAKER';
export type Props = (AgGridReactProps | AgReactUiProps) & {
partyId: string;
@@ -228,66 +232,132 @@ const formatFee = (partyId: string) => {
return '-';
}
const asset = value.settlementAsset;
let feesObj;
if (data?.buyer.id === partyId) {
feesObj = data?.buyerFee;
} else if (data?.seller.id === partyId) {
feesObj = data?.sellerFee;
} else {
return '-';
}
const { fees: feesObj, role } = getRoleAndFees({ data, partyId });
if (!feesObj) return '-';
const fee = new BigNumber(feesObj.makerFee)
.plus(feesObj.infrastructureFee)
.plus(feesObj.liquidityFee);
const totalFees = addDecimalsFormatNumber(fee.toString(), asset.decimals);
const { totalFee } = getFeesBreakdown(role, feesObj);
const totalFees = addDecimalsFormatNumber(totalFee, asset.decimals);
return `${totalFees} ${asset.symbol}`;
};
};
export const getRoleAndFees = ({
data,
partyId,
}: {
data: Pick<
FillFieldsFragment,
'buyerFee' | 'sellerFee' | 'buyer' | 'seller' | 'aggressor'
>;
partyId?: string;
}) => {
let role;
let feesObj;
if (data?.buyer.id === partyId) {
role = data.aggressor === Schema.Side.SIDE_BUY ? TAKER : MAKER;
feesObj = role === TAKER ? data?.buyerFee : data.sellerFee;
} else if (data?.seller.id === partyId) {
role = data.aggressor === Schema.Side.SIDE_SELL ? TAKER : MAKER;
feesObj = role === TAKER ? data?.sellerFee : data.buyerFee;
} else {
return { role: '-', feesObj: '-' };
}
return { role, fees: feesObj };
};
const FeesBreakdownTooltip = ({
data,
value,
valueFormatted,
partyId,
}: ITooltipParams & { partyId?: string }) => {
if (!value?.settlementAsset || !data) {
return null;
}
const asset = value.settlementAsset;
let feesObj;
if (data?.buyer.id === partyId) {
feesObj = data?.buyerFee;
} else if (data?.seller.id === partyId) {
feesObj = data?.sellerFee;
} else {
return null;
}
const { role, fees: feesObj } = getRoleAndFees({ data, partyId }) ?? {};
if (!feesObj) return null;
const { infrastructureFee, liquidityFee, makerFee, totalFee } =
getFeesBreakdown(role, feesObj);
return (
<div
data-testid="fee-breakdown-tooltip"
className="max-w-sm border border-neutral-600 bg-neutral-100 dark:bg-neutral-800 px-4 py-2 z-20 rounded text-sm break-word text-black dark:text-white"
>
<dl className="grid grid-cols-3 gap-x-1">
{role === MAKER && (
<>
<p className="mb-1">{t('The maker will receive the maker fee.')}</p>
<p className="mb-1">
{t(
'If the market is in monitoring auction the maker will pay half of the infrastructure and liquidity fees.'
)}
</p>
<p className="mb-1">
{t(
'If the market is active the maker will pay zero infrastructure and liquidity fees.'
)}
</p>
</>
)}
{role === TAKER && (
<>
<p className="mb-1">{t('Fees to be paid by the taker.')}</p>
<p className="mb-1">
{t(
'If the market is in monitoring auction the taker will pay half of the infrastructure and liquidity fees.'
)}
</p>
</>
)}
<dl className="grid grid-cols-2 gap-x-1">
<dt className="col-span-1">{t('Infrastructure fee')}</dt>
<dd className="text-right col-span-2">
{addDecimalsFormatNumber(feesObj.infrastructureFee, asset.decimals)}{' '}
<dd className="text-right col-span-1">
{addDecimalsFormatNumber(infrastructureFee, asset.decimals)}{' '}
{asset.symbol}
</dd>
<dt className="col-span-1">{t('Liquidity fee')}</dt>
<dd className="text-right col-span-2">
{addDecimalsFormatNumber(feesObj.liquidityFee, asset.decimals)}{' '}
{asset.symbol}
<dd className="text-right col-span-1">
{addDecimalsFormatNumber(liquidityFee, asset.decimals)} {asset.symbol}
</dd>
<dt className="col-span-1">{t('Maker fee')}</dt>
<dd className="text-right col-span-2">
{addDecimalsFormatNumber(feesObj.makerFee, asset.decimals)}{' '}
{asset.symbol}
<dd className="text-right col-span-1">
{addDecimalsFormatNumber(makerFee, asset.decimals)} {asset.symbol}
</dd>
<dt className="col-span-1">{t('Total fees')}</dt>
<dd className="text-right col-span-2">{valueFormatted}</dd>
<dd className="text-right col-span-1">
{addDecimalsFormatNumber(totalFee, asset.decimals)} {asset.symbol}
</dd>
</dl>
</div>
);
};
export const getFeesBreakdown = (
role: string,
feesObj: {
__typename?: 'TradeFee' | undefined;
makerFee: string;
infrastructureFee: string;
liquidityFee: string;
}
) => {
const makerFee =
role === MAKER
? new BigNumber(feesObj.makerFee).times(-1).toString()
: feesObj.makerFee;
const infrastructureFee = feesObj.infrastructureFee;
const liquidityFee = feesObj.liquidityFee;
const totalFee = new BigNumber(infrastructureFee)
.plus(makerFee)
.plus(liquidityFee)
.toString();
return {
infrastructureFee,
liquidityFee,
makerFee,
totalFee,
};
};
+5 -4
View File
@@ -88,9 +88,9 @@ const getRowsToRender = (
return selectedRows;
};
// 17px of row height plus 5px gap
export const gridGap = 5;
export const rowHeight = 22;
// 17px of row height plus 4px gap
export const gridGap = 4;
export const rowHeight = 21;
// top padding to make space for header
const headerPadding = 30;
// bottom padding to make space for footer
@@ -393,7 +393,8 @@ export const Orderbook = ({
numberOfRows * rowHeight +
headerPadding +
footerPadding +
-viewportHeight
-viewportHeight -
gridGap
)
);
if (scrollTopRef.current !== priceCenterScrollOffset) {
@@ -1,7 +1,7 @@
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { truncateByChars } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { useRef, useState } from 'react';
import { useCallback, useRef, useState } from 'react';
import type {
BodyScrollEvent,
BodyScrollEndEvent,
@@ -98,38 +98,60 @@ export const OrderListManager = ({
scrolledToTop,
});
const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
if (event.top === 0) {
addNewRows();
}
};
const onBodyScrollEnd = useCallback(
(event: BodyScrollEndEvent) => {
if (event.top === 0) {
addNewRows();
}
},
[addNewRows]
);
const onBodyScroll = (event: BodyScrollEvent) => {
const onBodyScroll = useCallback((event: BodyScrollEvent) => {
scrolledToTop.current = event.top <= 0;
};
}, []);
const onFilterChanged = (event: FilterChangedEvent) => {
const updatedFilter = event.api.getFilterModel();
if (Object.keys(updatedFilter).length) {
setFilter(updatedFilter);
} else if (filter) {
setFilter(undefined);
}
};
const onFilterChanged = useCallback(
(event: FilterChangedEvent) => {
const updatedFilter = event.api.getFilterModel();
if (Object.keys(updatedFilter).length) {
setFilter(updatedFilter);
} else {
setFilter(undefined);
}
},
[setFilter]
);
const onSortChange = (event: SortChangedEvent) => {
const sort = event.columnApi
.getColumnState()
.sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0))
.reduce((acc, col) => {
if (col.sort) {
const { colId, sort } = col;
acc.push({ colId, sort });
}
return acc;
}, [] as { colId: string; sort: string }[]);
setSort(sort.length > 0 ? sort : undefined);
};
const onSortChange = useCallback(
(event: SortChangedEvent) => {
const sort = event.columnApi
.getColumnState()
.sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0))
.reduce((acc, col) => {
if (col.sort) {
const { colId, sort } = col;
acc.push({ colId, sort });
}
return acc;
}, [] as { colId: string; sort: string }[]);
setSort(sort.length > 0 ? sort : undefined);
},
[setSort]
);
const onCancel = useCallback(
(order: Order) => {
if (!order.market) return;
create({
orderCancellation: {
orderId: order.id,
marketId: order.market.id,
},
});
},
[create]
);
return (
<>
@@ -143,15 +165,7 @@ export const OrderListManager = ({
onBodyScroll={onBodyScroll}
onFilterChanged={onFilterChanged}
onSortChanged={onSortChange}
cancel={(order: Order) => {
if (!order.market) return;
create({
orderCancellation: {
orderId: order.id,
marketId: order.market.id,
},
});
}}
cancel={onCancel}
setEditOrder={setEditOrder}
onMarketClick={onMarketClick}
isReadOnly={isReadOnly}
@@ -91,6 +91,9 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
data,
node,
}: VegaValueFormatterParams<Order, 'size'>) => {
if (!data) {
return undefined;
}
if (!data?.market || !isNumeric(value)) {
return '-';
}
@@ -116,6 +119,9 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
value,
node,
}: VegaValueFormatterParams<Order, 'type'>) => {
if (!order) {
return undefined;
}
if (!value) return '-';
if (order?.peggedOrder) return t('Pegged');
if (order?.liquidityProvision) return t('Liquidity provision');
@@ -162,6 +168,9 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
value,
node,
}: VegaValueFormatterParams<Order, 'remaining'>) => {
if (!data) {
return undefined;
}
if (!data?.market || !isNumeric(value) || !isNumeric(data.size)) {
return '-';
}
@@ -184,6 +193,9 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
data,
node,
}: VegaValueFormatterParams<Order, 'price'>) => {
if (!data) {
return undefined;
}
if (
!data?.market ||
data.type === Schema.OrderType.TYPE_MARKET ||
@@ -237,6 +249,9 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
data,
value,
}: VegaICellRendererParams<Order, 'updatedAt'>) => {
if (!data) {
return undefined;
}
return (
<span data-value={value}>
{value ? getDateTimeFormat().format(new Date(value)) : '-'}
+4
View File
@@ -4,6 +4,8 @@ fragment PositionFields on Position {
unrealisedPNL
averageEntryPrice
updatedAt
positionStatus
lossSocializationAmount
market {
id
}
@@ -30,6 +32,8 @@ subscription PositionsSubscription($partyId: ID!) {
averageEntryPrice
updatedAt
marketId
lossSocializationAmount
positionStatus
}
}
+7 -3
View File
@@ -3,21 +3,21 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type PositionFieldsFragment = { __typename?: 'Position', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, market: { __typename?: 'Market', id: string } };
export type PositionFieldsFragment = { __typename?: 'Position', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, positionStatus: Types.PositionStatus, lossSocializationAmount: string, market: { __typename?: 'Market', id: string } };
export type PositionsQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type PositionsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, positionsConnection?: { __typename?: 'PositionConnection', edges?: Array<{ __typename?: 'PositionEdge', node: { __typename?: 'Position', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, market: { __typename?: 'Market', id: string } } }> | null } | null } | null };
export type PositionsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, positionsConnection?: { __typename?: 'PositionConnection', edges?: Array<{ __typename?: 'PositionEdge', node: { __typename?: 'Position', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, positionStatus: Types.PositionStatus, lossSocializationAmount: string, market: { __typename?: 'Market', id: string } } }> | null } | null } | null };
export type PositionsSubscriptionSubscriptionVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type PositionsSubscriptionSubscription = { __typename?: 'Subscription', positions: Array<{ __typename?: 'PositionUpdate', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, marketId: string }> };
export type PositionsSubscriptionSubscription = { __typename?: 'Subscription', positions: Array<{ __typename?: 'PositionUpdate', realisedPNL: string, openVolume: string, unrealisedPNL: string, averageEntryPrice: string, updatedAt?: any | null, marketId: string, lossSocializationAmount: string, positionStatus: Types.PositionStatus }> };
export type MarginFieldsFragment = { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, asset: { __typename?: 'Asset', id: string }, market: { __typename?: 'Market', id: string } };
@@ -42,6 +42,8 @@ export const PositionFieldsFragmentDoc = gql`
unrealisedPNL
averageEntryPrice
updatedAt
positionStatus
lossSocializationAmount
market {
id
}
@@ -112,6 +114,8 @@ export const PositionsSubscriptionDocument = gql`
averageEntryPrice
updatedAt
marketId
lossSocializationAmount
positionStatus
}
}
`;
@@ -6,6 +6,7 @@ import type {
MarginFieldsFragment,
} from './__generated__/Positions';
import { getMetrics, rejoinPositionData } from './positions-data-providers';
import { PositionStatus } from '@vegaprotocol/types';
const accounts = [
{
@@ -78,6 +79,8 @@ const positions: PositionFieldsFragment[] = [
__typename: 'Market',
id: '5e6035fe6a6df78c9ec44b333c231e63d357acef0a0620d2c243f5865d1dc0d8',
},
lossSocializationAmount: '0',
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
},
{
__typename: 'Position',
@@ -90,6 +93,8 @@ const positions: PositionFieldsFragment[] = [
__typename: 'Market',
id: '10c4b1114d2f6fda239b73d018bca55888b6018f0ac70029972a17fea0a6a56e',
},
lossSocializationAmount: '100',
positionStatus: PositionStatus.POSITION_STATUS_ORDERS_CLOSED,
},
];
@@ -225,6 +230,10 @@ describe('getMetrics && rejoinPositionData', () => {
expect(metrics[0].totalBalance).toEqual('926178496');
expect(metrics[0].unrealisedPNL).toEqual('43804770');
expect(metrics[0].updatedAt).toEqual('2022-07-28T14:53:54.725477Z');
expect(metrics[0].lossSocializationAmount).toEqual(
positions[0].lossSocializationAmount
);
expect(metrics[0].status).toEqual(positions[0].positionStatus);
expect(metrics[1].assetSymbol).toEqual('tDAI');
expect(metrics[1].averageEntryPrice).toEqual('840158');
@@ -248,5 +257,9 @@ describe('getMetrics && rejoinPositionData', () => {
expect(metrics[1].totalBalance).toEqual('896098819');
expect(metrics[1].unrealisedPNL).toEqual('-9112700');
expect(metrics[1].updatedAt).toEqual('2022-07-28T15:09:34.441143Z');
expect(metrics[1].lossSocializationAmount).toEqual(
positions[1].lossSocializationAmount
);
expect(metrics[1].status).toEqual(positions[1].positionStatus);
});
});
@@ -24,6 +24,7 @@ import {
PositionsSubscriptionDocument,
} from './__generated__/Positions';
import { marginsDataProvider } from './margin-data-provider';
import type { PositionStatus } from '@vegaprotocol/types';
type PositionMarginLevel = Pick<
MarginFieldsFragment,
@@ -38,6 +39,8 @@ interface PositionRejoined {
updatedAt?: string | null;
market: MarketMaybeWithData | null;
margins: PositionMarginLevel | null;
lossSocializationAmount: string | null;
status: PositionStatus;
}
export interface Position {
@@ -62,6 +65,8 @@ export interface Position {
unrealisedPNL: string;
searchPrice: string | undefined;
updatedAt: string | null;
lossSocializationAmount: string;
status: PositionStatus;
}
export interface Data {
@@ -178,6 +183,8 @@ export const getMetrics = (
? searchPrice.multipliedBy(10 ** marketDecimalPlaces).toFixed(0)
: undefined,
updatedAt: position.updatedAt || null,
lossSocializationAmount: position.lossSocializationAmount || '0',
status: position.status,
});
});
return metrics;
@@ -201,6 +208,8 @@ export const update = (
openVolume: delta.openVolume,
averageEntryPrice: delta.averageEntryPrice,
updatedAt: delta.updatedAt,
lossSocializationAmount: delta.lossSocializationAmount,
positionStatus: delta.positionStatus,
};
} else {
draft.unshift({
@@ -267,6 +276,8 @@ export const rejoinPositionData = (
market:
marketsData?.find((market) => market.id === node.market.id) || null,
margins: upgradeMarginsConnection(node.market.id, margins),
lossSocializationAmount: node.lossSocializationAmount,
status: node.positionStatus,
};
});
}
+101 -2
View File
@@ -1,8 +1,11 @@
import type { RenderResult } from '@testing-library/react';
import { act, render, screen } from '@testing-library/react';
import PositionsTable from './positions-table';
import { act, render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import PositionsTable, { OpenVolumeCell, PNLCell } from './positions-table';
import type { Position } from './positions-data-providers';
import * as Schema from '@vegaprotocol/types';
import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
import type { ICellRendererParams } from 'ag-grid-community';
const singleRow: Position = {
marketName: 'ETH/BTC (31 july 2022)',
@@ -26,6 +29,8 @@ const singleRow: Position = {
searchPrice: '0',
updatedAt: '2022-07-27T15:02:58.400Z',
marginAccountBalance: '12345600',
status: PositionStatus.POSITION_STATUS_UNSPECIFIED,
lossSocializationAmount: '0',
};
const singleRowData = [singleRow];
@@ -219,3 +224,97 @@ it('do not display close button if openVolume is zero', async () => {
const cells = screen.getAllByRole('gridcell');
expect(cells[12].textContent).toEqual('');
});
describe('PNLCell', () => {
const props = {
data: undefined,
valueFormatted: '100',
};
it('renders a dash if no data', () => {
render(<PNLCell {...(props as ICellRendererParams)} />);
expect(screen.getByText('-')).toBeInTheDocument();
});
it('renders value if no loss socialisation has occurred', () => {
const props = {
data: {
...singleRow,
lossSocialisationAmount: '0',
},
valueFormatted: '100',
};
render(<PNLCell {...(props as ICellRendererParams)} />);
expect(screen.getByText(props.valueFormatted)).toBeInTheDocument();
expect(screen.queryByRole('img')).not.toBeInTheDocument();
});
it('renders value with warning tooltip if loss socialisation occurred', async () => {
const props = {
data: {
...singleRow,
lossSocializationAmount: '500',
decimals: 2,
},
valueFormatted: '100',
};
render(<PNLCell {...(props as ICellRendererParams)} />);
const content = screen.getByText(props.valueFormatted);
expect(content).toBeInTheDocument();
expect(screen.getByRole('img')).toBeInTheDocument();
await userEvent.hover(content);
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toBeInTheDocument();
expect(
// using within as radix renders tooltip content twice
within(tooltip).getByText('Lifetime loss socialisation deductions: 5.00')
).toBeInTheDocument();
});
});
describe('OpenVolumeCell', () => {
const props = {
data: undefined,
valueFormatted: '100',
};
it('renders a dash if no data', () => {
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
expect(screen.getByText('-')).toBeInTheDocument();
});
it('renders value if no status is normal', () => {
const props = {
data: {
...singleRow,
status: PositionStatus.POSITION_STATUS_UNSPECIFIED,
},
valueFormatted: '100',
};
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
expect(screen.getByText(props.valueFormatted)).toBeInTheDocument();
expect(screen.queryByRole('img')).not.toBeInTheDocument();
});
it('renders status with warning tooltip if not normal', async () => {
const props = {
data: {
...singleRow,
status: PositionStatus.POSITION_STATUS_ORDERS_CLOSED,
},
valueFormatted: '100',
};
render(<OpenVolumeCell {...(props as ICellRendererParams)} />);
const content = screen.getByText(props.valueFormatted);
expect(content).toBeInTheDocument();
expect(screen.getByRole('img')).toBeInTheDocument();
await userEvent.hover(content);
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toBeInTheDocument();
expect(
// using within as radix renders tooltip content twice
within(tooltip).getByText(
`Status: ${PositionStatusMapping[props.data.status]}`
)
).toBeInTheDocument();
});
});
+126 -10
View File
@@ -1,6 +1,6 @@
import classNames from 'classnames';
import { forwardRef } from 'react';
import type { CSSProperties } from 'react';
import type { CSSProperties, ReactNode } from 'react';
import type { CellRendererSelectorResult } from 'ag-grid-community';
import type {
VegaValueFormatterParams,
@@ -15,25 +15,31 @@ import {
signedNumberCssClass,
signedNumberCssClassRules,
} from '@vegaprotocol/datagrid';
import {
ButtonLink,
Tooltip,
TooltipCellComponent,
Link,
ExternalLink,
Icon,
ProgressBarCell,
} from '@vegaprotocol/ui-toolkit';
import {
volumePrefix,
toBigNum,
formatNumber,
getDateTimeFormat,
addDecimalsFormatNumber,
createDocsLinks,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { AgGridColumn } from 'ag-grid-react';
import type { AgGridReact } from 'ag-grid-react';
import type { Position } from './positions-data-providers';
import * as Schema from '@vegaprotocol/types';
import {
ButtonLink,
Link,
ProgressBarCell,
TooltipCellComponent,
} from '@vegaprotocol/ui-toolkit';
import { getRowId } from './use-positions-data';
import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
import { useEnvironment } from '@vegaprotocol/environment';
interface Props extends TypedDataAgGrid<Position> {
onClose?: (data: Position) => void;
@@ -167,6 +173,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
)
);
}}
cellRenderer={OpenVolumeCell}
/>
<AgGridColumn
headerName={t('Mark price')}
@@ -311,7 +318,6 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
}}
valueFormatter={({
data,
node,
}: VegaValueFormatterParams<Position, 'marginAccountBalance'>):
| string
| undefined => {
@@ -329,7 +335,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
field="realisedPNL"
type="rightAligned"
cellClassRules={signedNumberCssClassRules}
cellClass="text-right font-mono"
cellClass="font-mono text-right"
filter="agNumberColumnFilter"
valueGetter={({
data,
@@ -348,13 +354,14 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
headerTooltip={t(
'Profit or loss is realised whenever your position is reduced to zero and the margin is released back to your collateral balance. P&L excludes any fees paid.'
)}
cellRenderer={PNLCell}
/>
<AgGridColumn
headerName={t('Unrealised PNL')}
field="unrealisedPNL"
type="rightAligned"
cellClassRules={signedNumberCssClassRules}
cellClass="text-right font-mono"
cellClass="font-mono text-right"
filter="agNumberColumnFilter"
valueGetter={({
data,
@@ -373,6 +380,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
headerTooltip={t(
'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.'
)}
cellRenderer={PNLCell}
/>
<AgGridColumn
headerName={t('Updated')}
@@ -409,3 +417,111 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
);
export default PositionsTable;
export const PNLCell = ({
valueFormatted,
data,
}: VegaICellRendererParams<Position, 'realisedPNL'>) => {
const { VEGA_DOCS_URL } = useEnvironment();
const LOSS_SOCIALIZATION_LINK =
VEGA_DOCS_URL && createDocsLinks(VEGA_DOCS_URL).LOSS_SOCIALIZATION;
if (!data) {
return <>-</>;
}
const losses = parseInt(data?.lossSocializationAmount ?? '0');
if (losses <= 0) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{valueFormatted}</>;
}
const lossesFormatted = addDecimalsFormatNumber(
data.lossSocializationAmount,
data.decimals
);
return (
<WarningCell
tooltipContent={
<>
<p className="mb-2">
{t('Lifetime loss socialisation deductions: %s', lossesFormatted)}
</p>
{VEGA_DOCS_URL && (
<ExternalLink href={LOSS_SOCIALIZATION_LINK}>
{t('Read more about loss socialisation')}
</ExternalLink>
)}
</>
}
>
{valueFormatted}
</WarningCell>
);
};
export const OpenVolumeCell = ({
valueFormatted,
data,
}: VegaICellRendererParams<Position, 'openVolume'>) => {
const { VEGA_DOCS_URL } = useEnvironment();
if (!data) {
return <>-</>;
}
if (data.status === PositionStatus.POSITION_STATUS_UNSPECIFIED) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{valueFormatted}</>;
}
const POSITION_RESOLUTION_LINK =
VEGA_DOCS_URL && createDocsLinks(VEGA_DOCS_URL).POSITION_RESOLUTION;
return (
<WarningCell
tooltipContent={
<>
<p className="mb-2">
{t('Your position was affected by market conditions')}
</p>
<p className="mb-2">
{t(
'Status: %s',
PositionStatusMapping[
PositionStatus.POSITION_STATUS_ORDERS_CLOSED
]
)}
</p>
{VEGA_DOCS_URL && (
<ExternalLink href={POSITION_RESOLUTION_LINK}>
{t('Read more about position resolution')}
</ExternalLink>
)}
</>
}
>
{valueFormatted}
</WarningCell>
);
};
const WarningCell = ({
children,
tooltipContent,
}: {
children: ReactNode;
tooltipContent: ReactNode;
}) => {
return (
<Tooltip description={tooltipContent}>
<div className="w-full flex items-center justify-between underline decoration-dashed underline-offest-2">
<span className="text-black dark:text-white mr-1">
<Icon name="warning-sign" size={3} />
</span>
<span className="text-ellipsis overflow-hidden">{children}</span>
</div>
</Tooltip>
);
};
+8 -1
View File
@@ -1,3 +1,4 @@
import { PositionStatus } from '@vegaprotocol/types';
import merge from 'lodash/merge';
import type { PartialDeep } from 'type-fest';
import type {
@@ -58,6 +59,8 @@ const positionFields: PositionFieldsFragment[] = [
id: 'market-0',
__typename: 'Market',
},
lossSocializationAmount: '0',
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
},
{
__typename: 'Position',
@@ -70,8 +73,11 @@ const positionFields: PositionFieldsFragment[] = [
id: 'market-1',
__typename: 'Market',
},
lossSocializationAmount: '0',
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
},
{
__typename: 'Position',
realisedPNL: '230000',
openVolume: '1',
unrealisedPNL: '-22519',
@@ -81,7 +87,8 @@ const positionFields: PositionFieldsFragment[] = [
id: 'market-2',
__typename: 'Market',
},
__typename: 'Position',
lossSocializationAmount: '0',
positionStatus: PositionStatus.POSITION_STATUS_UNSPECIFIED,
},
];
+3
View File
@@ -1 +1,4 @@
import '@testing-library/jest-dom';
import ResizeObserver from 'resize-observer-polyfill';
global.ResizeObserver = ResizeObserver;
+3 -5
View File
@@ -1,6 +1,6 @@
import { ethers } from 'ethers';
import { hexlify } from 'ethers/lib/utils';
import abi from '../abis/claim_abi.json';
import { asciiToHex } from '../utils';
export const UNSPENT_CODE = '0x0000000000000000000000000000000000000000';
export const SPENT_CODE = '0x0000000000000000000000000000000000000001';
@@ -71,7 +71,7 @@ export class Claim {
tranche,
expiry,
},
asciiToHex(country),
hexlify(country),
target,
].filter(Boolean)
);
@@ -109,9 +109,7 @@ export class Claim {
* @return {Promise<boolean>}
*/
async isCountryBlocked(country: string): Promise<boolean> {
const isAllowed = await this.contract.allowed_countries(
asciiToHex(country)
);
const isAllowed = await this.contract.allowed_countries(hexlify(country));
return !isAllowed;
}
}
@@ -1,17 +0,0 @@
/**
* From:
* https://github.com/ChainSafe/web3.js/blob/436e77a8eaa061fbaa183a9f73ca590c2e1d7697/packages/web3-utils/src/index.js
*/
export const asciiToHex = (str: string) => {
if (!str) return '0x00';
let hex = '';
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);
const n = code.toString(16);
hex += n.length < 2 ? '0' + n : n;
}
return '0x' + hex;
};
-1
View File
@@ -1,2 +1 @@
export * from './ascii-to-hex';
export * from './prepend-0x';
+9
View File
@@ -12,6 +12,7 @@ import type {
OrderStatus,
OrderTimeInForce,
OrderType,
PositionStatus,
ProposalRejectionReason,
ProposalState,
Side,
@@ -445,3 +446,11 @@ export const DispatchMetricLabels: DispatchMetricLabel = {
DISPATCH_METRIC_MAKER_FEES_RECEIVED: 'Price maker fees earned',
DISPATCH_METRIC_MARKET_VALUE: 'Total market Value',
};
export const PositionStatusMapping: {
[T in PositionStatus]: string;
} = {
POSITION_STATUS_CLOSED_OUT: 'Closed by network',
POSITION_STATUS_ORDERS_CLOSED: 'Maintained by network',
POSITION_STATUS_UNSPECIFIED: 'Normal',
};
@@ -9,6 +9,7 @@ export interface FormGroupProps {
hideLabel?: boolean;
labelDescription?: string;
labelAlign?: 'left' | 'right';
compact?: boolean;
}
export const FormGroup = ({
@@ -19,8 +20,16 @@ export const FormGroup = ({
labelDescription,
labelAlign = 'left',
hideLabel = false,
compact = false,
}: FormGroupProps) => {
const wrapperClasses = classNames('relative mb-2', className);
const wrapperClasses = classNames(
'relative',
{
'mb-2': compact,
'mb-4': !compact,
},
className
);
const labelClasses = classNames('block mb-2 text-sm', {
'text-right': labelAlign === 'right',
'sr-only': hideLabel,
+2
View File
@@ -12,6 +12,8 @@ export const createDocsLinks = (docsUrl: string) => ({
VEGA_WALLET_CONCEPTS_URL: `${docsUrl}/concepts/vega-wallet`,
PROPOSALS_GUIDE: `${docsUrl}/tutorials/proposals`,
NODE_OPERATORS: `${docsUrl}/node-operators`,
LOSS_SOCIALIZATION: `${docsUrl}/concepts/trading-on-vega/market-protections#loss-socialisation`,
POSITION_RESOLUTION: `${docsUrl}/concepts/trading-on-vega/market-protections#position-resolution`,
});
export const ExternalLinks = {
@@ -26,7 +26,7 @@ export interface EthStoredTxState extends EthTxState {
methodName: ContractMethod;
args: string[];
requiredConfirmations: number;
requiresConfirmation: boolean;
requiresConfirmation: boolean; // whether or not the tx needs external confirmation (IE from a subscription even)
assetId?: string;
deposit?: DepositBusEventFieldsFragment;
}
@@ -1,5 +1,5 @@
import { useEthWithdrawApprovalsManager } from './use-ethereum-withdraw-approvals-manager';
import { renderHook } from '@testing-library/react';
import { renderHook, waitFor } from '@testing-library/react';
import type { MockedResponse } from '@apollo/client/testing';
import type { ReactNode } from 'react';
import { MockedProvider } from '@apollo/client/testing';
@@ -318,5 +318,25 @@ describe('useEthWithdrawApprovalsManager', () => {
expect(update.mock.calls[0][1].message).toEqual(
'You are on the wrong network'
);
mockChainId = 111111;
});
it('catch ethereum errors', async () => {
const transaction = createWithdrawTransaction();
mockUseGetWithdrawThreshold.mockReturnValueOnce(() => {
throw new Error('call revert exception');
});
mockEthTransactionStoreState.mockReturnValue({ create });
mockEthWithdrawApprovalsStoreState.mockReturnValue({
transactions: [transaction],
update,
});
render();
await waitFor(() => {
const lastCall = update.mock.calls.pop();
expect(lastCall[1].status).toEqual(ApprovalStatus.Error);
expect(lastCall[1].message).toEqual('Something went wrong');
});
});
});
@@ -5,6 +5,7 @@ import { addDecimal } from '@vegaprotocol/utils';
import { useGetWithdrawThreshold } from './use-get-withdraw-threshold';
import { useGetWithdrawDelay } from './use-get-withdraw-delay';
import { t } from '@vegaprotocol/i18n';
import { localLoggerFactory } from '@vegaprotocol/utils';
import { CollateralBridge } from '@vegaprotocol/smart-contracts';
@@ -128,7 +129,16 @@ export const useEthWithdrawApprovalsManager = () => {
approval.signatures,
]
);
})();
})().catch((err) => {
localLoggerFactory({ application: 'web3' }).error(
'create withdrawal transaction',
err
);
update(transaction.id, {
status: ApprovalStatus.Error,
message: t('Something went wrong'),
});
});
}, [
getThreshold,
getDelay,
+1
View File
@@ -68,6 +68,7 @@
"react-hook-form": "^7.27.0",
"react-i18next": "^11.11.4",
"react-intersection-observer": "^9.2.2",
"react-markdown": "^8.0.5",
"react-router-dom": "6.3.0",
"react-syntax-highlighter": "^15.4.5",
"react-use-websocket": "^3.0.0",
+465 -2
View File
@@ -6501,6 +6501,13 @@
"@types/d3-interpolate" "^2"
"@types/d3-selection" "^2"
"@types/debug@^4.0.0":
version "4.1.7"
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==
dependencies:
"@types/ms" "*"
"@types/eslint-scope@^3.7.3":
version "3.7.4"
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16"
@@ -6734,6 +6741,11 @@
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
"@types/ms@*":
version "0.7.31"
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
"@types/node-fetch@^2.5.10", "@types/node-fetch@^2.5.7":
version "2.6.2"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da"
@@ -6812,7 +6824,7 @@
resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.26.0.tgz#a1c3809b0ad61c62cac6d4e0c56d610c910b7654"
integrity sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ==
"@types/prop-types@*":
"@types/prop-types@*", "@types/prop-types@^15.0.0":
version "15.7.5"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
@@ -9008,6 +9020,11 @@ bail@^1.0.0:
resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776"
integrity sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==
bail@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d"
integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
@@ -9733,6 +9750,11 @@ character-entities@^1.0.0:
resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.4.tgz#e12c3939b7eaf4e5b15e7ad4c5e28e1d48c5b16b"
integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==
character-entities@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22"
integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==
character-reference-invalid@^1.0.0:
version "1.1.4"
resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560"
@@ -10137,6 +10159,11 @@ comma-separated-tokens@^1.0.0:
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea"
integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==
comma-separated-tokens@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee"
integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==
commander@^2.19.0, commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
@@ -11135,7 +11162,7 @@ debug@3.2.7, debug@^3.0.0, debug@^3.1.0, debug@^3.2.7:
dependencies:
ms "^2.1.1"
debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4:
debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@@ -11172,6 +11199,13 @@ decimal.js@^10.2.1:
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.1.tgz#be75eeac4a2281aace80c1a8753587c27ef053e7"
integrity sha512-F29o+vci4DodHYT9UrR5IEbfBw9pE5eSapIJdTqXK5+6hq+t8VRxwQyKlW2i+KDKFkkJQRvFyI/QXD83h8LyQw==
decode-named-character-reference@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e"
integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==
dependencies:
character-entities "^2.0.0"
decode-uri-component@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
@@ -11304,6 +11338,11 @@ dependency-graph@0.11.0, dependency-graph@^0.11.0:
resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.11.0.tgz#ac0ce7ed68a54da22165a85e97a01d53f5eb2e27"
integrity sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==
dequal@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
des.js@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
@@ -11398,6 +11437,11 @@ diff@^4.0.1:
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
diff@^5.0.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40"
integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==
diffie-hellman@^5.0.0:
version "5.0.3"
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
@@ -13808,6 +13852,11 @@ hast-util-to-parse5@^6.0.0:
xtend "^4.0.0"
zwitch "^1.0.0"
hast-util-whitespace@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz#0ec64e257e6fc216c7d14c8a1b74d27d650b4557"
integrity sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==
hastscript@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-6.0.0.tgz#e8768d7eac56c3fdeac8a92830d58e811e5bf640"
@@ -14769,6 +14818,11 @@ is-plain-obj@^3.0.0:
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7"
integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==
is-plain-obj@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0"
integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==
is-plain-object@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
@@ -15895,6 +15949,11 @@ kleur@^3.0.3:
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
kleur@^4.0.3:
version "4.1.5"
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"
integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==
klona@^2.0.4, klona@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc"
@@ -16528,6 +16587,33 @@ mdast-util-definitions@^4.0.0:
dependencies:
unist-util-visit "^2.0.0"
mdast-util-definitions@^5.0.0:
version "5.1.2"
resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz#9910abb60ac5d7115d6819b57ae0bcef07a3f7a7"
integrity sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==
dependencies:
"@types/mdast" "^3.0.0"
"@types/unist" "^2.0.0"
unist-util-visit "^4.0.0"
mdast-util-from-markdown@^1.0.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.0.tgz#0214124154f26154a2b3f9d401155509be45e894"
integrity sha512-HN3W1gRIuN/ZW295c7zi7g9lVBllMgZE40RxCX37wrTPWXCWtpvOZdfnuK+1WNpvZje6XuJeI3Wnb4TJEUem+g==
dependencies:
"@types/mdast" "^3.0.0"
"@types/unist" "^2.0.0"
decode-named-character-reference "^1.0.0"
mdast-util-to-string "^3.1.0"
micromark "^3.0.0"
micromark-util-decode-numeric-character-reference "^1.0.0"
micromark-util-decode-string "^1.0.0"
micromark-util-normalize-identifier "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.0"
unist-util-stringify-position "^3.0.0"
uvu "^0.5.0"
mdast-util-to-hast@10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-10.0.1.tgz#0cfc82089494c52d46eb0e3edb7a4eb2aea021eb"
@@ -16542,11 +16628,32 @@ mdast-util-to-hast@10.0.1:
unist-util-position "^3.0.0"
unist-util-visit "^2.0.0"
mdast-util-to-hast@^12.1.0:
version "12.3.0"
resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz#045d2825fb04374e59970f5b3f279b5700f6fb49"
integrity sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==
dependencies:
"@types/hast" "^2.0.0"
"@types/mdast" "^3.0.0"
mdast-util-definitions "^5.0.0"
micromark-util-sanitize-uri "^1.1.0"
trim-lines "^3.0.0"
unist-util-generated "^2.0.0"
unist-util-position "^4.0.0"
unist-util-visit "^4.0.0"
mdast-util-to-string@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-1.1.0.tgz#27055500103f51637bd07d01da01eb1967a43527"
integrity sha512-jVU0Nr2B9X3MU4tSK7JP1CMkSvOj7X5l/GboG1tKRw52lLF1x2Ju92Ms9tNetCcbfX3hzlM73zYo2NKkWSfF/A==
mdast-util-to-string@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.1.1.tgz#db859050d79d48cf9896d294de06f3ede7474d16"
integrity sha512-tGvhT94e+cVnQt8JWE9/b3cUQZWS732TJxXHktvP+BYo62PpYD53Ls/6cC60rW21dW+txxiM4zMdc6abASvZKA==
dependencies:
"@types/mdast" "^3.0.0"
mdn-data@2.0.14:
version "2.0.14"
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
@@ -16680,6 +16787,201 @@ microevent.ts@~0.1.1:
resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0"
integrity sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==
micromark-core-commonmark@^1.0.1:
version "1.0.6"
resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz#edff4c72e5993d93724a3c206970f5a15b0585ad"
integrity sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==
dependencies:
decode-named-character-reference "^1.0.0"
micromark-factory-destination "^1.0.0"
micromark-factory-label "^1.0.0"
micromark-factory-space "^1.0.0"
micromark-factory-title "^1.0.0"
micromark-factory-whitespace "^1.0.0"
micromark-util-character "^1.0.0"
micromark-util-chunked "^1.0.0"
micromark-util-classify-character "^1.0.0"
micromark-util-html-tag-name "^1.0.0"
micromark-util-normalize-identifier "^1.0.0"
micromark-util-resolve-all "^1.0.0"
micromark-util-subtokenize "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.1"
uvu "^0.5.0"
micromark-factory-destination@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz#fef1cb59ad4997c496f887b6977aa3034a5a277e"
integrity sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==
dependencies:
micromark-util-character "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.0"
micromark-factory-label@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz#6be2551fa8d13542fcbbac478258fb7a20047137"
integrity sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==
dependencies:
micromark-util-character "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.0"
uvu "^0.5.0"
micromark-factory-space@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz#cebff49968f2b9616c0fcb239e96685cb9497633"
integrity sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==
dependencies:
micromark-util-character "^1.0.0"
micromark-util-types "^1.0.0"
micromark-factory-title@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz#7e09287c3748ff1693930f176e1c4a328382494f"
integrity sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==
dependencies:
micromark-factory-space "^1.0.0"
micromark-util-character "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.0"
uvu "^0.5.0"
micromark-factory-whitespace@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz#e991e043ad376c1ba52f4e49858ce0794678621c"
integrity sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==
dependencies:
micromark-factory-space "^1.0.0"
micromark-util-character "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.0"
micromark-util-character@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.1.0.tgz#d97c54d5742a0d9611a68ca0cd4124331f264d86"
integrity sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==
dependencies:
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.0"
micromark-util-chunked@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz#5b40d83f3d53b84c4c6bce30ed4257e9a4c79d06"
integrity sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==
dependencies:
micromark-util-symbol "^1.0.0"
micromark-util-classify-character@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz#cbd7b447cb79ee6997dd274a46fc4eb806460a20"
integrity sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==
dependencies:
micromark-util-character "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.0"
micromark-util-combine-extensions@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.0.0.tgz#91418e1e74fb893e3628b8d496085639124ff3d5"
integrity sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==
dependencies:
micromark-util-chunked "^1.0.0"
micromark-util-types "^1.0.0"
micromark-util-decode-numeric-character-reference@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz#dcc85f13b5bd93ff8d2868c3dba28039d490b946"
integrity sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==
dependencies:
micromark-util-symbol "^1.0.0"
micromark-util-decode-string@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-1.0.2.tgz#942252ab7a76dec2dbf089cc32505ee2bc3acf02"
integrity sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==
dependencies:
decode-named-character-reference "^1.0.0"
micromark-util-character "^1.0.0"
micromark-util-decode-numeric-character-reference "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-encode@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.0.1.tgz#2c1c22d3800870ad770ece5686ebca5920353383"
integrity sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA==
micromark-util-html-tag-name@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.1.0.tgz#eb227118befd51f48858e879b7a419fc0df20497"
integrity sha512-BKlClMmYROy9UiV03SwNmckkjn8QHVaWkqoAqzivabvdGcwNGMMMH/5szAnywmsTBUzDsU57/mFi0sp4BQO6dA==
micromark-util-normalize-identifier@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.0.0.tgz#4a3539cb8db954bbec5203952bfe8cedadae7828"
integrity sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==
dependencies:
micromark-util-symbol "^1.0.0"
micromark-util-resolve-all@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.0.0.tgz#a7c363f49a0162e931960c44f3127ab58f031d88"
integrity sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==
dependencies:
micromark-util-types "^1.0.0"
micromark-util-sanitize-uri@^1.0.0, micromark-util-sanitize-uri@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.1.0.tgz#f12e07a85106b902645e0364feb07cf253a85aee"
integrity sha512-RoxtuSCX6sUNtxhbmsEFQfWzs8VN7cTctmBPvYivo98xb/kDEoTCtJQX5wyzIYEmk/lvNFTat4hL8oW0KndFpg==
dependencies:
micromark-util-character "^1.0.0"
micromark-util-encode "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-subtokenize@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz#ff6f1af6ac836f8bfdbf9b02f40431760ad89105"
integrity sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==
dependencies:
micromark-util-chunked "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.0"
uvu "^0.5.0"
micromark-util-symbol@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.0.1.tgz#b90344db62042ce454f351cf0bebcc0a6da4920e"
integrity sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ==
micromark-util-types@^1.0.0, micromark-util-types@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.0.2.tgz#f4220fdb319205812f99c40f8c87a9be83eded20"
integrity sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==
micromark@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.1.0.tgz#eeba0fe0ac1c9aaef675157b52c166f125e89f62"
integrity sha512-6Mj0yHLdUZjHnOPgr5xfWIMqMWS12zDN6iws9SLuSz76W8jTtAv24MN4/CL7gJrl5vtxGInkkqDv/JIoRsQOvA==
dependencies:
"@types/debug" "^4.0.0"
debug "^4.0.0"
decode-named-character-reference "^1.0.0"
micromark-core-commonmark "^1.0.1"
micromark-factory-space "^1.0.0"
micromark-util-character "^1.0.0"
micromark-util-chunked "^1.0.0"
micromark-util-combine-extensions "^1.0.0"
micromark-util-decode-numeric-character-reference "^1.0.0"
micromark-util-encode "^1.0.0"
micromark-util-normalize-identifier "^1.0.0"
micromark-util-resolve-all "^1.0.0"
micromark-util-sanitize-uri "^1.0.0"
micromark-util-subtokenize "^1.0.0"
micromark-util-symbol "^1.0.0"
micromark-util-types "^1.0.1"
uvu "^0.5.0"
micromatch@^3.1.10, micromatch@^3.1.4:
version "3.1.10"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
@@ -16946,6 +17248,11 @@ move-concurrently@^1.0.1:
rimraf "^2.5.4"
run-queue "^1.0.3"
mri@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -18713,6 +19020,11 @@ property-information@^5.0.0, property-information@^5.3.0:
dependencies:
xtend "^4.0.0"
property-information@^6.0.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.2.0.tgz#b74f522c31c097b5149e3c3cb8d7f3defd986a1d"
integrity sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==
protocols@^1.4.0:
version "1.4.8"
resolved "https://registry.yarnpkg.com/protocols/-/protocols-1.4.8.tgz#48eea2d8f58d9644a4a32caae5d5db290a075ce8"
@@ -19047,6 +19359,27 @@ react-lifecycles-compat@^3.0.4:
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-markdown@^8.0.5:
version "8.0.5"
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-8.0.5.tgz#c9a70a33ca9aeeafb769c6582e7e38843b9d70ad"
integrity sha512-jGJolWWmOWAvzf+xMdB9zwStViODyyFQhNB/bwCerbBKmrTmgmA599CGiOlP58OId1IMoIRsA8UdI1Lod4zb5A==
dependencies:
"@types/hast" "^2.0.0"
"@types/prop-types" "^15.0.0"
"@types/unist" "^2.0.0"
comma-separated-tokens "^2.0.0"
hast-util-whitespace "^2.0.0"
prop-types "^15.0.0"
property-information "^6.0.0"
react-is "^18.0.0"
remark-parse "^10.0.0"
remark-rehype "^10.0.0"
space-separated-tokens "^2.0.0"
style-to-object "^0.4.0"
unified "^10.0.0"
unist-util-visit "^4.0.0"
vfile "^5.0.0"
react-refresh@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.10.0.tgz#2f536c9660c0b9b1d500684d9e52a65e7404f7e3"
@@ -19523,6 +19856,25 @@ remark-parse@8.0.3:
vfile-location "^3.0.0"
xtend "^4.0.1"
remark-parse@^10.0.0:
version "10.0.1"
resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-10.0.1.tgz#6f60ae53edbf0cf38ea223fe643db64d112e0775"
integrity sha512-1fUyHr2jLsVOkhbvPRBJ5zTKZZyD6yZzYaWCS6BPBdQ8vEMBCH+9zNCDA6tET/zHCi/jLqjCWtlJZUPk+DbnFw==
dependencies:
"@types/mdast" "^3.0.0"
mdast-util-from-markdown "^1.0.0"
unified "^10.0.0"
remark-rehype@^10.0.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-10.1.0.tgz#32dc99d2034c27ecaf2e0150d22a6dcccd9a6279"
integrity sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==
dependencies:
"@types/hast" "^2.0.0"
"@types/mdast" "^3.0.0"
mdast-util-to-hast "^12.1.0"
unified "^10.0.0"
remark-slug@^6.0.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/remark-slug/-/remark-slug-6.1.0.tgz#0503268d5f0c4ecb1f33315c00465ccdd97923ce"
@@ -19872,6 +20224,13 @@ rxjs@^7.5.1, rxjs@^7.5.5:
dependencies:
tslib "^2.1.0"
sade@^1.7.3:
version "1.8.1"
resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701"
integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==
dependencies:
mri "^1.1.0"
safe-buffer@5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
@@ -20503,6 +20862,11 @@ space-separated-tokens@^1.0.0:
resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899"
integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==
space-separated-tokens@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f"
integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==
spdx-correct@^3.0.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
@@ -20975,6 +21339,13 @@ style-to-object@0.3.0, style-to-object@^0.3.0:
dependencies:
inline-style-parser "0.1.1"
style-to-object@^0.4.0:
version "0.4.1"
resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.4.1.tgz#53cf856f7cf7f172d72939d9679556469ba5de37"
integrity sha512-HFpbb5gr2ypci7Qw+IOhnP2zOU7e77b+rzM+wTzXzfi1PrtBCX0E7Pk4wL4iTLnhzZ+JgEGAhX81ebTg/aYjQw==
dependencies:
inline-style-parser "0.1.1"
styled-jsx@5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.2.tgz#ff230fd593b737e9e68b630a694d460425478729"
@@ -21468,6 +21839,11 @@ tree-kill@1.2.2:
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
trim-lines@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338"
integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==
trim-newlines@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
@@ -21493,6 +21869,11 @@ trough@^1.0.0:
resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406"
integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==
trough@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/trough/-/trough-2.1.0.tgz#0f7b511a4fde65a46f18477ab38849b22c554876"
integrity sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==
ts-dedent@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5"
@@ -21819,6 +22200,19 @@ unified@9.2.0:
trough "^1.0.0"
vfile "^4.0.0"
unified@^10.0.0:
version "10.1.2"
resolved "https://registry.yarnpkg.com/unified/-/unified-10.1.2.tgz#b1d64e55dafe1f0b98bb6c719881103ecf6c86df"
integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==
dependencies:
"@types/unist" "^2.0.0"
bail "^2.0.0"
extend "^3.0.0"
is-buffer "^2.0.0"
is-plain-obj "^4.0.0"
trough "^2.0.0"
vfile "^5.0.0"
union-value@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
@@ -21867,16 +22261,33 @@ unist-util-generated@^1.0.0:
resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-1.1.6.tgz#5ab51f689e2992a472beb1b35f2ce7ff2f324d4b"
integrity sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg==
unist-util-generated@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-2.0.1.tgz#e37c50af35d3ed185ac6ceacb6ca0afb28a85cae"
integrity sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==
unist-util-is@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-4.1.0.tgz#976e5f462a7a5de73d94b706bac1b90671b57797"
integrity sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==
unist-util-is@^5.0.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.2.0.tgz#37eed0617b76c114fd34d44c201aa96fd928b309"
integrity sha512-Glt17jWwZeyqrFqOK0pF1Ded5U3yzJnFr8CG1GMjCWTp9zDo2p+cmD6pWbZU8AgM5WU3IzRv6+rBwhzsGh6hBQ==
unist-util-position@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-3.1.0.tgz#1c42ee6301f8d52f47d14f62bbdb796571fa2d47"
integrity sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA==
unist-util-position@^4.0.0:
version "4.0.4"
resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-4.0.4.tgz#93f6d8c7d6b373d9b825844645877c127455f037"
integrity sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==
dependencies:
"@types/unist" "^2.0.0"
unist-util-remove-position@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-2.0.1.tgz#5d19ca79fdba712301999b2b73553ca8f3b352cc"
@@ -21898,6 +22309,13 @@ unist-util-stringify-position@^2.0.0:
dependencies:
"@types/unist" "^2.0.2"
unist-util-stringify-position@^3.0.0:
version "3.0.3"
resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz#03ad3348210c2d930772d64b489580c13a7db39d"
integrity sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==
dependencies:
"@types/unist" "^2.0.0"
unist-util-visit-parents@^3.0.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz#65a6ce698f78a6b0f56aa0e88f13801886cdaef6"
@@ -21906,6 +22324,14 @@ unist-util-visit-parents@^3.0.0:
"@types/unist" "^2.0.0"
unist-util-is "^4.0.0"
unist-util-visit-parents@^5.1.1:
version "5.1.3"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb"
integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==
dependencies:
"@types/unist" "^2.0.0"
unist-util-is "^5.0.0"
unist-util-visit@2.0.3, unist-util-visit@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-2.0.3.tgz#c3703893146df47203bb8a9795af47d7b971208c"
@@ -21915,6 +22341,15 @@ unist-util-visit@2.0.3, unist-util-visit@^2.0.0:
unist-util-is "^4.0.0"
unist-util-visit-parents "^3.0.0"
unist-util-visit@^4.0.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.2.tgz#125a42d1eb876283715a3cb5cceaa531828c72e2"
integrity sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==
dependencies:
"@types/unist" "^2.0.0"
unist-util-is "^5.0.0"
unist-util-visit-parents "^5.1.1"
universalify@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
@@ -22192,6 +22627,16 @@ uuid@^8.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uvu@^0.5.0:
version "0.5.6"
resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df"
integrity sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==
dependencies:
dequal "^2.0.0"
diff "^5.0.0"
kleur "^4.0.3"
sade "^1.7.3"
v8-compile-cache-lib@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
@@ -22260,6 +22705,14 @@ vfile-message@^2.0.0:
"@types/unist" "^2.0.0"
unist-util-stringify-position "^2.0.0"
vfile-message@^3.0.0:
version "3.1.4"
resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.4.tgz#15a50816ae7d7c2d1fa87090a7f9f96612b59dea"
integrity sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==
dependencies:
"@types/unist" "^2.0.0"
unist-util-stringify-position "^3.0.0"
vfile@^4.0.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/vfile/-/vfile-4.2.1.tgz#03f1dce28fc625c625bc6514350fbdb00fa9e624"
@@ -22270,6 +22723,16 @@ vfile@^4.0.0:
unist-util-stringify-position "^2.0.0"
vfile-message "^2.0.0"
vfile@^5.0.0:
version "5.3.7"
resolved "https://registry.yarnpkg.com/vfile/-/vfile-5.3.7.tgz#de0677e6683e3380fafc46544cfe603118826ab7"
integrity sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==
dependencies:
"@types/unist" "^2.0.0"
is-buffer "^2.0.0"
unist-util-stringify-position "^3.0.0"
vfile-message "^3.0.0"
vm-browserify@^1.0.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"