Compare commits
64
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cae942d848 | ||
|
|
63bac52822 | ||
|
|
f1142ca7df | ||
|
|
f2e4816df3 | ||
|
|
19bc57e727 | ||
|
|
69769b2e9d | ||
|
|
dbf63759e8 | ||
|
|
d7c6286f84 | ||
|
|
623385aa6b | ||
|
|
f0e4479215 | ||
|
|
9c54f899de | ||
|
|
ecf7c6da69 | ||
|
|
58298ef5a0 | ||
|
|
77bcc04a57 | ||
|
|
15ab1c6486 | ||
|
|
84c1cba22b | ||
|
|
26020b5f71 | ||
|
|
16d345eb4a | ||
|
|
3b3fcab4f4 | ||
|
|
f54a629179 | ||
|
|
56f48671cc | ||
|
|
a65ee358ff | ||
|
|
7b8d814724 | ||
|
|
cd237dc491 | ||
|
|
9ad00adc5a | ||
|
|
fab055bb34 | ||
|
|
d66956d080 | ||
|
|
9848c8ffff | ||
|
|
b0e8ed62d2 | ||
|
|
289ff3b5b3 | ||
|
|
1d965187dc | ||
|
|
8bf3615001 | ||
|
|
1b77800b62 | ||
|
|
979dc2904b | ||
|
|
0fec3bcf3c | ||
|
|
4542e25ec9 | ||
|
|
acd96fbf21 | ||
|
|
96bf12d4dd | ||
|
|
47b81188d4 | ||
|
|
efada06256 | ||
|
|
311ef41138 | ||
|
|
5dd0cafeca | ||
|
|
f3d6aa1129 | ||
|
|
93dbc32f22 | ||
|
|
fd1827ef67 | ||
|
|
9c9948e4c1 | ||
|
|
79b0868ff7 | ||
|
|
a8401d4c6f | ||
|
|
c030264211 | ||
|
|
068f7b3fc3 | ||
|
|
4430f0c5f6 | ||
|
|
0a14000708 | ||
|
|
9ca6fe3cbf | ||
|
|
4f96472375 | ||
|
|
cddf2ba00b | ||
|
|
d01d6f5815 | ||
|
|
2998e165f6 | ||
|
|
886b0cb6d3 | ||
|
|
6799a06acd | ||
|
|
47a6bcff78 | ||
|
|
06dcd00b53 | ||
|
|
fd7aa5bd78 | ||
|
|
384cfbc70b | ||
|
|
9d14f603b3 |
@@ -11,7 +11,7 @@ on:
|
||||
type: choice
|
||||
options:
|
||||
- explorer-e2e
|
||||
- token-e2e
|
||||
- governance-e2e
|
||||
- trading-e2e
|
||||
tags:
|
||||
description: 'Test tags to run'
|
||||
|
||||
@@ -10,5 +10,5 @@ jobs:
|
||||
uses: ./.github/workflows/cypress-run.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
projects: '["explorer-e2e","token-e2e","trading-e2e"]'
|
||||
projects: '["explorer-e2e","governance-e2e","trading-e2e"]'
|
||||
tags: '@smoke @regression @slow'
|
||||
|
||||
@@ -42,10 +42,10 @@ jobs:
|
||||
affected=$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)
|
||||
echo -n "Affected projects: $affected"
|
||||
projects=""
|
||||
if [[ $affected == *"token"* ]]; then projects+='"token-e2e" '; fi
|
||||
if [[ $affected == *"governance"* ]]; then projects+='"governance-e2e" '; fi
|
||||
if [[ $affected == *"trading"* ]]; then projects+='"trading-e2e" '; fi
|
||||
if [[ $affected == *"explorer"* ]]; then projects+='"explorer-e2e" '; fi
|
||||
if [[ -z "$projects" ]]; then projects+='"token-e2e" "trading-e2e" "explorer-e2e" '; fi
|
||||
if [[ -z "$projects" ]]; then projects+='"governance-e2e" "trading-e2e" "explorer-e2e" '; fi
|
||||
projects=${projects%?}
|
||||
projects=[${projects// /,}]
|
||||
echo PROJECTS=$projects >> $GITHUB_ENV
|
||||
|
||||
@@ -12,6 +12,8 @@ on:
|
||||
- react-helpers
|
||||
- tailwindcss-config
|
||||
- types
|
||||
- utils
|
||||
- i18n
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
|
||||
@@ -161,7 +161,7 @@ In order to setup and run vegawallet for e2e capsule tests, in a separate termin
|
||||
bash setup-vegawallet.sh
|
||||
```
|
||||
|
||||
3. copy generated `api-token` and paste the token into `CYPRESS_VEGA_WALLET_API_TOKEN` environment variable in either `apps/token-e2e/.env` or `apps/explorer-e2e/.env` depending on which project needs testing.
|
||||
3. copy generated `api-token` and paste the token into `CYPRESS_VEGA_WALLET_API_TOKEN` environment variable in either `apps/governance-e2e/.env` or `apps/explorer-e2e/.env` depending on which project needs testing.
|
||||
|
||||
Note: The script is only needed if capsule was built for first time or fresh. To run existing wallet service for capsule:
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
# App configuration variables
|
||||
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/tm
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.be.devnet1.vega.xyz/websocket
|
||||
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
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
# App configuration variables
|
||||
NX_TENDERMINT_URL=https://be.explorer.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://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
|
||||
NX_VEGA_GOVERNANCE_URL=https://token.vega.xyz
|
||||
@@ -1,10 +1,8 @@
|
||||
# App configuration variables
|
||||
NX_TENDERMINT_URL=https://tm.be.mainnet-mirror.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_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
|
||||
|
||||
@@ -5,8 +5,7 @@ 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://tm.sandbox.vega.xyz/websocket
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.sandbox.vega.xyz/websocket
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_VEGA_GOVERNANCE_URL=https://sandbox.token.vega.xyz
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
NX_TENDERMINT_URL=https://tm.n00.stagnet3.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n00.stagnet3.vega.xyz/websocket
|
||||
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,13 +1,10 @@
|
||||
# App configuration variables
|
||||
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
|
||||
NX_BLOCK_EXPLORER=https://be.testnet.vega.xyz/rest
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.be.testnet.vega.xyz/websocket/
|
||||
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
|
||||
@@ -3,12 +3,10 @@ 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
|
||||
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.xyz/rest
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.be.validators-testnet.vega.xyz/websocket/
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -101,7 +101,7 @@ const DeterministicOrderDetails = ({
|
||||
{t('Remaining')}
|
||||
</h2>
|
||||
<h5 className="text-lg font-medium text-gray-500 mb-0">
|
||||
{o.remaining}
|
||||
<SizeInMarket size={o.remaining} marketId={o.market.id} />
|
||||
</h5>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+50
@@ -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>;
|
||||
+98
@@ -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} />
|
||||
);
|
||||
};
|
||||
+67
@@ -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();
|
||||
}
|
||||
);
|
||||
});
|
||||
+34
@@ -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>
|
||||
);
|
||||
};
|
||||
+64
@@ -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([]);
|
||||
}
|
||||
);
|
||||
});
|
||||
+46
@@ -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>
|
||||
);
|
||||
};
|
||||
+33
@@ -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/);
|
||||
}
|
||||
);
|
||||
});
|
||||
+17
@@ -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 = {
|
||||
|
||||
@@ -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: '🔗 ';
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ module.exports = defineConfig({
|
||||
supportFile: './src/support/index.js',
|
||||
video: false,
|
||||
videoUploadOnPasses: false,
|
||||
videosFolder: '../../dist/cypress/apps/token-e2e/videos',
|
||||
screenshotsFolder: '../../dist/cypress/apps/token-e2e/screenshots',
|
||||
videosFolder: '../../dist/cypress/apps/governance-e2e/videos',
|
||||
screenshotsFolder: '../../dist/cypress/apps/governance-e2e/screenshots',
|
||||
chromeWebSecurity: false,
|
||||
viewportWidth: 1440,
|
||||
viewportHeight: 900,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 ?? ''} />
|
||||
)}
|
||||
|
||||
@@ -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,6 +48,7 @@ export const Tranches = () => {
|
||||
) : (
|
||||
<p>{t('No tranches')}</p>
|
||||
)}
|
||||
|
||||
<section className="text-center mt-4">
|
||||
<ButtonLink onClick={() => setShowAll(!showAll)}>
|
||||
{showAll
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# App configuration variables
|
||||
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_NETWORKS='{"TESTNET":"https://multisig-signer.fairground.wtf","MAINNET":"https://multisig-signer.vega.xyz"}'
|
||||
NX_VEGA_ENV=VALIDATOR_TESTNET
|
||||
@@ -104,7 +104,7 @@
|
||||
"tranche_end": "2023-04-06T00:00:00.000Z",
|
||||
"total_added": "14099",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "14099",
|
||||
"locked_amount": "13755.4000425627232917",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "30",
|
||||
@@ -2761,7 +2761,7 @@
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "86666.297",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "66243.522619730595190252",
|
||||
"locked_amount": "64879.699162507033901597",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "86666.297",
|
||||
@@ -2827,7 +2827,7 @@
|
||||
"tranche_end": "2023-06-01T00:00:00.000Z",
|
||||
"total_added": "2500",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "1263.57600732600725",
|
||||
"locked_amount": "1184.67738858363875",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "2500",
|
||||
@@ -2948,7 +2948,7 @@
|
||||
"tranche_end": "2023-09-01T00:00:00.000Z",
|
||||
"total_added": "17500",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "17498.8903985507255",
|
||||
"locked_amount": "16952.60322312802",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "12500",
|
||||
@@ -3214,8 +3214,8 @@
|
||||
"tranche_start": "2023-02-01T00:00:00.000Z",
|
||||
"tranche_end": "2023-08-01T00:00:00.000Z",
|
||||
"total_added": "37500",
|
||||
"total_removed": "3376.096819125",
|
||||
"locked_amount": "31696.477900552485",
|
||||
"total_removed": "3639.24714135",
|
||||
"locked_amount": "30506.46005985267",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "7500",
|
||||
@@ -3239,6 +3239,26 @@
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0x1a73c48a20c37eb5f8214ecb3c1986d0b6a24ae6472533c556ec6c264814abb6"
|
||||
},
|
||||
{
|
||||
"amount": "75.466160175",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0x7d9d6e2a37fbe030faf08b7b1b0fc5b09f1f4295c4d7f4e28e0bdac1bdd294ac"
|
||||
},
|
||||
{
|
||||
"amount": "48.814456725",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0x9e42c457fa017043dbbcb189fb3a574719ce66ce348eee850db3bd809014b1fd"
|
||||
},
|
||||
{
|
||||
"amount": "92.19037755",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0xee973a9efc7b88b292763a64b9e4a8fa4907826bc4b0d8f4099e1c2836bf725a"
|
||||
},
|
||||
{
|
||||
"amount": "46.679327775",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0xe31bf92bdb6d99596efda0366e9f57c53be81a3025ab5a6267cbf03380091800"
|
||||
},
|
||||
{
|
||||
"amount": "142.173112275",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -3299,6 +3319,30 @@
|
||||
"tranche_id": 34,
|
||||
"tx": "0x1a73c48a20c37eb5f8214ecb3c1986d0b6a24ae6472533c556ec6c264814abb6"
|
||||
},
|
||||
{
|
||||
"amount": "75.466160175",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 34,
|
||||
"tx": "0x7d9d6e2a37fbe030faf08b7b1b0fc5b09f1f4295c4d7f4e28e0bdac1bdd294ac"
|
||||
},
|
||||
{
|
||||
"amount": "48.814456725",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 34,
|
||||
"tx": "0x9e42c457fa017043dbbcb189fb3a574719ce66ce348eee850db3bd809014b1fd"
|
||||
},
|
||||
{
|
||||
"amount": "92.19037755",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 34,
|
||||
"tx": "0xee973a9efc7b88b292763a64b9e4a8fa4907826bc4b0d8f4099e1c2836bf725a"
|
||||
},
|
||||
{
|
||||
"amount": "46.679327775",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 34,
|
||||
"tx": "0xe31bf92bdb6d99596efda0366e9f57c53be81a3025ab5a6267cbf03380091800"
|
||||
},
|
||||
{
|
||||
"amount": "142.173112275",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -3337,8 +3381,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "7500",
|
||||
"withdrawn_tokens": "1100.983636425",
|
||||
"remaining_tokens": "6399.016363575"
|
||||
"withdrawn_tokens": "1364.13395865",
|
||||
"remaining_tokens": "6135.86604135"
|
||||
},
|
||||
{
|
||||
"address": "0x0B4e6fcE839B01ef43DA6F890FAC7B1Afb004600",
|
||||
@@ -3370,7 +3414,7 @@
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "129999.45",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "66183.0837404197105596",
|
||||
"locked_amount": "64820.504600498023491807",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "129999.45",
|
||||
@@ -3436,7 +3480,7 @@
|
||||
"tranche_end": "2023-09-03T00:00:00.000Z",
|
||||
"total_added": "62600",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "31898.27305936073208",
|
||||
"locked_amount": "30913.16868340943934",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "10000",
|
||||
@@ -3629,7 +3673,7 @@
|
||||
"tranche_end": "2023-09-17T00:00:00.000Z",
|
||||
"total_added": "5000",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "2739.566210045662",
|
||||
"locked_amount": "2660.8837519025875",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "5000",
|
||||
@@ -3840,7 +3884,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "97499.58",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "8141.6415391408092444606",
|
||||
"locked_amount": "6805.0783328659465926588",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "97499.58",
|
||||
@@ -3873,7 +3917,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "135173.4239508",
|
||||
"total_removed": "98230.390980249184455396",
|
||||
"locked_amount": "11128.218388231931879326063704",
|
||||
"locked_amount": "9301.36723326549382327399362",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "135173.4239508",
|
||||
@@ -3919,7 +3963,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "32499.86",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "3425.0479968875506615442",
|
||||
"locked_amount": "2862.778937219710450476",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "32499.86",
|
||||
@@ -3952,7 +3996,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "10833.29",
|
||||
"total_removed": "0",
|
||||
"locked_amount": "1114.8198871078435790832",
|
||||
"locked_amount": "931.8067643157678690636",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "10833.29",
|
||||
@@ -3985,7 +4029,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "22749.93",
|
||||
"total_removed": "4720.860935375",
|
||||
"locked_amount": "4167.4457285340302852478",
|
||||
"locked_amount": "3483.3018002945028144642",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "6500",
|
||||
@@ -4136,8 +4180,8 @@
|
||||
"tranche_start": "2022-11-01T00:00:00.000Z",
|
||||
"tranche_end": "2023-05-01T00:00:00.000Z",
|
||||
"total_added": "22500",
|
||||
"total_removed": "4913.13257745",
|
||||
"locked_amount": "7581.4226519337009",
|
||||
"total_removed": "5176.28289975",
|
||||
"locked_amount": "6867.411947513811",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "7500",
|
||||
@@ -4161,6 +4205,26 @@
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0xe5b0236fdb114d861139db0bdec5bdc26b4e107ff73eb66bc4ba262a003fd872"
|
||||
},
|
||||
{
|
||||
"amount": "75.4719153",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0x27d0868963dc1e725d9b4da891a4df20b48107f1b0c803dd3f752362aa1d4335"
|
||||
},
|
||||
{
|
||||
"amount": "48.820211775",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0x1690b7e9232984baf6a441b517de44cb2c9c547f100e9e8acc8d7e144e01b2b8"
|
||||
},
|
||||
{
|
||||
"amount": "92.173112325",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0xb7cd37bdd9d1dc5374fe1690824f423396fc7dff3379d8564086784dd81593e6"
|
||||
},
|
||||
{
|
||||
"amount": "46.6850829",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0x1ddeafc5b8ca978a842bdf2edf5e5590f99923e2e540b734b9589f4bc4657b35"
|
||||
},
|
||||
{
|
||||
"amount": "142.17311235",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -4276,6 +4340,30 @@
|
||||
"tranche_id": 33,
|
||||
"tx": "0xe5b0236fdb114d861139db0bdec5bdc26b4e107ff73eb66bc4ba262a003fd872"
|
||||
},
|
||||
{
|
||||
"amount": "75.4719153",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 33,
|
||||
"tx": "0x27d0868963dc1e725d9b4da891a4df20b48107f1b0c803dd3f752362aa1d4335"
|
||||
},
|
||||
{
|
||||
"amount": "48.820211775",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 33,
|
||||
"tx": "0x1690b7e9232984baf6a441b517de44cb2c9c547f100e9e8acc8d7e144e01b2b8"
|
||||
},
|
||||
{
|
||||
"amount": "92.173112325",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 33,
|
||||
"tx": "0xb7cd37bdd9d1dc5374fe1690824f423396fc7dff3379d8564086784dd81593e6"
|
||||
},
|
||||
{
|
||||
"amount": "46.6850829",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 33,
|
||||
"tx": "0x1ddeafc5b8ca978a842bdf2edf5e5590f99923e2e540b734b9589f4bc4657b35"
|
||||
},
|
||||
{
|
||||
"amount": "142.17311235",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -4386,8 +4474,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "7500",
|
||||
"withdrawn_tokens": "4913.13257745",
|
||||
"remaining_tokens": "2586.86742255"
|
||||
"withdrawn_tokens": "5176.28289975",
|
||||
"remaining_tokens": "2323.71710025"
|
||||
},
|
||||
{
|
||||
"address": "0x2539b51EbDE65a75672aBcfE9439a706a99D18D1",
|
||||
@@ -4412,7 +4500,7 @@
|
||||
"tranche_end": "2023-06-02T00:00:00.000Z",
|
||||
"total_added": "1939928.38",
|
||||
"total_removed": "928642.9598472029154",
|
||||
"locked_amount": "494221.11463625557046169",
|
||||
"locked_amount": "463693.4479242731446593956",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "1852091.69",
|
||||
@@ -9821,7 +9909,7 @@
|
||||
"tranche_id": 11,
|
||||
"tranche_start": "2021-09-03T00:00:00.000Z",
|
||||
"tranche_end": "2022-09-03T00:00:00.000Z",
|
||||
"total_added": "54766.000000000000000003",
|
||||
"total_added": "54836.000000000000000003",
|
||||
"total_removed": "43683.21518131551",
|
||||
"locked_amount": "0",
|
||||
"deposits": [
|
||||
@@ -9850,6 +9938,11 @@
|
||||
"user": "0x94097462EF7c43D0aC732E7B18f830096D95207C",
|
||||
"tx": "0xf7e1cb4b03d0f7664b4d7688029ed8ad4c468bf4b2e6ef941c59de4dce680496"
|
||||
},
|
||||
{
|
||||
"amount": "70",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tx": "0xb6da04f1ddff19acea3425120f1c1244a55291c73f2dbff9c90349bd82c727b2"
|
||||
},
|
||||
{
|
||||
"amount": "30",
|
||||
"user": "0xf83aaA5786516F7016DF9ead2CD9F14616dA0A13",
|
||||
@@ -21114,6 +21207,106 @@
|
||||
"withdrawn_tokens": "755",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "70",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0xb6da04f1ddff19acea3425120f1c1244a55291c73f2dbff9c90349bd82c727b2"
|
||||
},
|
||||
{
|
||||
"amount": "20",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0xe3fd6d538990df7bf818b8a178ecf7135a5e34f4d07778e9d6dc637cf470e2e0"
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x07b9757e77de7a2de6e3fea89bc48801656fdcd51d9c81b92324b35aa800e0c8"
|
||||
},
|
||||
{
|
||||
"amount": "20",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x561607bb6a1c0c4c45fc7ac4bac7d8287015b80e624c32633ed5e06b4d0f1c3a"
|
||||
},
|
||||
{
|
||||
"amount": "12",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x0a36e69fa542ca075481e08073f159ea7bea4f2c73ef9b52d22d4614db19a5a0"
|
||||
},
|
||||
{
|
||||
"amount": "2",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x8ab01cde20c499bea4d28c208749cfc8139ac1bbae7b65f3fb77a903e2af1ace"
|
||||
},
|
||||
{
|
||||
"amount": "5",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x600702eb08091f59365fdf6af580b0e60c81600ae57a8fc3ad7473d67cc738dd"
|
||||
},
|
||||
{
|
||||
"amount": "10",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0xfd7edebf2b9d7359188e31fde4d44674bfa8fc119d7f9c71e673b80dc3913899"
|
||||
},
|
||||
{
|
||||
"amount": "10",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0xd055543cf69ebdb0d8862fd917723705853caa63e97e6dfa942419e707bff520"
|
||||
},
|
||||
{
|
||||
"amount": "20",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x15da44cda1d0e1d31e82e3cf7f39080b5f61eca6fa870af430c48ceb73ae3dc3"
|
||||
},
|
||||
{
|
||||
"amount": "10",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x99b34aa5c033c78b690dbe9262a3e621bc7643f60cafffeeafcc6a8885fabd74"
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "20",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0xebb86840b89a0f2e4753dd646d342faff415ce5e2fcce0eef9918dbeb8e39c2e"
|
||||
},
|
||||
{
|
||||
"amount": "3.86333155832",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x068081c78445c62a22f68a81cbfd73d1c906b35c31882e5dcbd95b158485b227"
|
||||
},
|
||||
{
|
||||
"amount": "41.64068004762",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x2794e03baf2be9d03ff553075bdbe9296e7975ee72647f08a42299cc05a4e3ab"
|
||||
},
|
||||
{
|
||||
"amount": "58.49598839406",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0xfeb269c626db43a4d16a117e47a21a148c9f911d708d97d9dfd0caf08e1c09e7"
|
||||
}
|
||||
],
|
||||
"total_tokens": "194",
|
||||
"withdrawn_tokens": "124",
|
||||
"remaining_tokens": "70"
|
||||
},
|
||||
{
|
||||
"address": "0xf83aaA5786516F7016DF9ead2CD9F14616dA0A13",
|
||||
"deposits": [
|
||||
@@ -23962,100 +24155,6 @@
|
||||
"withdrawn_tokens": "0",
|
||||
"remaining_tokens": "20"
|
||||
},
|
||||
{
|
||||
"address": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "20",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0xe3fd6d538990df7bf818b8a178ecf7135a5e34f4d07778e9d6dc637cf470e2e0"
|
||||
},
|
||||
{
|
||||
"amount": "15",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x07b9757e77de7a2de6e3fea89bc48801656fdcd51d9c81b92324b35aa800e0c8"
|
||||
},
|
||||
{
|
||||
"amount": "20",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x561607bb6a1c0c4c45fc7ac4bac7d8287015b80e624c32633ed5e06b4d0f1c3a"
|
||||
},
|
||||
{
|
||||
"amount": "12",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x0a36e69fa542ca075481e08073f159ea7bea4f2c73ef9b52d22d4614db19a5a0"
|
||||
},
|
||||
{
|
||||
"amount": "2",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x8ab01cde20c499bea4d28c208749cfc8139ac1bbae7b65f3fb77a903e2af1ace"
|
||||
},
|
||||
{
|
||||
"amount": "5",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x600702eb08091f59365fdf6af580b0e60c81600ae57a8fc3ad7473d67cc738dd"
|
||||
},
|
||||
{
|
||||
"amount": "10",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0xfd7edebf2b9d7359188e31fde4d44674bfa8fc119d7f9c71e673b80dc3913899"
|
||||
},
|
||||
{
|
||||
"amount": "10",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0xd055543cf69ebdb0d8862fd917723705853caa63e97e6dfa942419e707bff520"
|
||||
},
|
||||
{
|
||||
"amount": "20",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x15da44cda1d0e1d31e82e3cf7f39080b5f61eca6fa870af430c48ceb73ae3dc3"
|
||||
},
|
||||
{
|
||||
"amount": "10",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x99b34aa5c033c78b690dbe9262a3e621bc7643f60cafffeeafcc6a8885fabd74"
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "20",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0xebb86840b89a0f2e4753dd646d342faff415ce5e2fcce0eef9918dbeb8e39c2e"
|
||||
},
|
||||
{
|
||||
"amount": "3.86333155832",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x068081c78445c62a22f68a81cbfd73d1c906b35c31882e5dcbd95b158485b227"
|
||||
},
|
||||
{
|
||||
"amount": "41.64068004762",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0x2794e03baf2be9d03ff553075bdbe9296e7975ee72647f08a42299cc05a4e3ab"
|
||||
},
|
||||
{
|
||||
"amount": "58.49598839406",
|
||||
"user": "0xC6D7208DaDEe4F431bd0f3f11E7d4c91fF51bfb2",
|
||||
"tranche_id": 11,
|
||||
"tx": "0xfeb269c626db43a4d16a117e47a21a148c9f911d708d97d9dfd0caf08e1c09e7"
|
||||
}
|
||||
],
|
||||
"total_tokens": "124",
|
||||
"withdrawn_tokens": "124",
|
||||
"remaining_tokens": "0"
|
||||
},
|
||||
{
|
||||
"address": "0xeD96EAA7D15951ab128abF8918a4D69F0Eef9905",
|
||||
"deposits": [
|
||||
@@ -36791,8 +36890,8 @@
|
||||
"tranche_start": "2022-03-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-06-05T00:00:00.000Z",
|
||||
"total_added": "3732368.4671",
|
||||
"total_removed": "615950.831401702010493",
|
||||
"locked_amount": "783947.108408790236382843098",
|
||||
"total_removed": "618385.665327542135993",
|
||||
"locked_amount": "737036.71327714871630535621",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "1998.95815",
|
||||
@@ -36961,6 +37060,11 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "2434.8339258401255",
|
||||
"user": "0xE6CacAE56Cca8dFdB7910b5A13578719D4E57DA0",
|
||||
"tx": "0xcba878ae28c2c84219f8281dc2a57d0d9b6c99b7920900d5344c76aabe8f1e84"
|
||||
},
|
||||
{
|
||||
"amount": "16659.576193025747093",
|
||||
"user": "0xB523235B6c7C74DDB26b10E78bFb2d0Cb63Ae289",
|
||||
@@ -37335,6 +37439,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "2434.8339258401255",
|
||||
"user": "0xE6CacAE56Cca8dFdB7910b5A13578719D4E57DA0",
|
||||
"tranche_id": 1,
|
||||
"tx": "0xcba878ae28c2c84219f8281dc2a57d0d9b6c99b7920900d5344c76aabe8f1e84"
|
||||
},
|
||||
{
|
||||
"amount": "6293.20485405503",
|
||||
"user": "0xE6CacAE56Cca8dFdB7910b5A13578719D4E57DA0",
|
||||
@@ -37619,8 +37729,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "187637.95",
|
||||
"withdrawn_tokens": "146054.503703913043",
|
||||
"remaining_tokens": "41583.446296086957"
|
||||
"withdrawn_tokens": "148489.3376297531685",
|
||||
"remaining_tokens": "39148.6123702468315"
|
||||
},
|
||||
{
|
||||
"address": "0x1A71e3ED1996CAbB91bB043f880CE963D601707e",
|
||||
@@ -38140,8 +38250,8 @@
|
||||
"tranche_start": "2022-06-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-12-05T00:00:00.000Z",
|
||||
"total_added": "15870102.715470999700000001",
|
||||
"total_removed": "584996.67274505202169452",
|
||||
"locked_amount": "8079513.697843178693280892781044398053528",
|
||||
"total_removed": "589039.38568474293710452",
|
||||
"locked_amount": "7913172.44864162203140200442918599770965126",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "16249.93",
|
||||
@@ -38680,6 +38790,61 @@
|
||||
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
|
||||
"tx": "0x3c76f03b71481a44c6ba2c7cea407f5f1bac60bcaa9242af18de287f94c8ed90"
|
||||
},
|
||||
{
|
||||
"amount": "445.6540155068705",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0xb54d54fd5a6211e97495020ad17e9f045d032999ed94ea31994958304010aa85"
|
||||
},
|
||||
{
|
||||
"amount": "133.7749515525265",
|
||||
"user": "0xE6CacAE56Cca8dFdB7910b5A13578719D4E57DA0",
|
||||
"tx": "0xed77ed534414a1949b1473707782223907e0ac001f899037ff822f10bd4a358a"
|
||||
},
|
||||
{
|
||||
"amount": "261.60674662018875",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0xdbd9367c0d60c1bd9165752d8fab223aa43c7c2e85568a4e98e86ad0e0c97a01"
|
||||
},
|
||||
{
|
||||
"amount": "394.58033601838216",
|
||||
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
|
||||
"tx": "0xe479984e1fc0f4a0040603b528c841013d6da2c890361ce5f4152256ea9dd009"
|
||||
},
|
||||
{
|
||||
"amount": "525.648824",
|
||||
"user": "0xF4c75FdbAe821C6B5DBB9f001bB23cBA58D1bA37",
|
||||
"tx": "0x132a15c0b5f8c6016ac1a90437b9dca119ebc2479cde9c702327653fa3c11750"
|
||||
},
|
||||
{
|
||||
"amount": "297.58591267021375",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0x7fa0448cd21e20e3a40743d0f294b6d83618e5167b83809c1c91a82afaba57a5"
|
||||
},
|
||||
{
|
||||
"amount": "400.185839059034875",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0x1f6bdfee642200cdcd7ebd38e31827c3d6307a180e0566acccc93f2ad649e23c"
|
||||
},
|
||||
{
|
||||
"amount": "561.828496812011",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0x8fd3fb0a52476d4c1c2ecdf8eb9cd4a8899acffc8ae08b98f1a2414c21078a2b"
|
||||
},
|
||||
{
|
||||
"amount": "533.559150572712",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0x4d1d66d256c15cf4e9dac71da10e817cddd26761b348519b40b9a8066f81b2a2"
|
||||
},
|
||||
{
|
||||
"amount": "94.362938033777875",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0xa7e962b98ed06faceaf876c50efca7276439f25eeb25ec574e88767fa0c5fb30"
|
||||
},
|
||||
{
|
||||
"amount": "393.925728845198",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tx": "0x3ea8ed6b0441f5a71a645193ec134c51626bc8c9eb6442600da8482d41dd7c68"
|
||||
},
|
||||
{
|
||||
"amount": "535.4042378778335",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -40467,6 +40632,54 @@
|
||||
"tranche_id": 2,
|
||||
"tx": "0x1438fbbc780cb382033855d295fa2511deed15cd64506a62ae4e3cfe82b3678e"
|
||||
},
|
||||
{
|
||||
"amount": "445.6540155068705",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 2,
|
||||
"tx": "0xb54d54fd5a6211e97495020ad17e9f045d032999ed94ea31994958304010aa85"
|
||||
},
|
||||
{
|
||||
"amount": "261.60674662018875",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 2,
|
||||
"tx": "0xdbd9367c0d60c1bd9165752d8fab223aa43c7c2e85568a4e98e86ad0e0c97a01"
|
||||
},
|
||||
{
|
||||
"amount": "297.58591267021375",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 2,
|
||||
"tx": "0x7fa0448cd21e20e3a40743d0f294b6d83618e5167b83809c1c91a82afaba57a5"
|
||||
},
|
||||
{
|
||||
"amount": "400.185839059034875",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 2,
|
||||
"tx": "0x1f6bdfee642200cdcd7ebd38e31827c3d6307a180e0566acccc93f2ad649e23c"
|
||||
},
|
||||
{
|
||||
"amount": "561.828496812011",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 2,
|
||||
"tx": "0x8fd3fb0a52476d4c1c2ecdf8eb9cd4a8899acffc8ae08b98f1a2414c21078a2b"
|
||||
},
|
||||
{
|
||||
"amount": "533.559150572712",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 2,
|
||||
"tx": "0x4d1d66d256c15cf4e9dac71da10e817cddd26761b348519b40b9a8066f81b2a2"
|
||||
},
|
||||
{
|
||||
"amount": "94.362938033777875",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 2,
|
||||
"tx": "0xa7e962b98ed06faceaf876c50efca7276439f25eeb25ec574e88767fa0c5fb30"
|
||||
},
|
||||
{
|
||||
"amount": "393.925728845198",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
"tranche_id": 2,
|
||||
"tx": "0x3ea8ed6b0441f5a71a645193ec134c51626bc8c9eb6442600da8482d41dd7c68"
|
||||
},
|
||||
{
|
||||
"amount": "535.4042378778335",
|
||||
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
|
||||
@@ -41681,8 +41894,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "259998.8875",
|
||||
"withdrawn_tokens": "127367.16038718373625",
|
||||
"remaining_tokens": "132631.72711281626375"
|
||||
"withdrawn_tokens": "130355.869215303743",
|
||||
"remaining_tokens": "129643.018284696257"
|
||||
},
|
||||
{
|
||||
"address": "0x89051CAb67Bc7F8CC44F7e270c6EDaf1EC57676c",
|
||||
@@ -41915,6 +42128,12 @@
|
||||
"tranche_id": 2,
|
||||
"tx": "0x3c76f03b71481a44c6ba2c7cea407f5f1bac60bcaa9242af18de287f94c8ed90"
|
||||
},
|
||||
{
|
||||
"amount": "394.58033601838216",
|
||||
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
|
||||
"tranche_id": 2,
|
||||
"tx": "0xe479984e1fc0f4a0040603b528c841013d6da2c890361ce5f4152256ea9dd009"
|
||||
},
|
||||
{
|
||||
"amount": "1640.93679110232897",
|
||||
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
|
||||
@@ -42157,8 +42376,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "150551.801",
|
||||
"withdrawn_tokens": "73885.95651128121547",
|
||||
"remaining_tokens": "76665.84448871878453"
|
||||
"withdrawn_tokens": "74280.53684729959763",
|
||||
"remaining_tokens": "76271.26415270040237"
|
||||
},
|
||||
{
|
||||
"address": "0x4d982Ab0823fD2f48e934a7be2bb0a5374a26148",
|
||||
@@ -42366,6 +42585,12 @@
|
||||
"tranche_id": 2,
|
||||
"tx": "0x1887d1fb77d9fa555dd5b55da307edc91e395f3a7e0ca5f874740f5a10077cdf"
|
||||
},
|
||||
{
|
||||
"amount": "525.648824",
|
||||
"user": "0xF4c75FdbAe821C6B5DBB9f001bB23cBA58D1bA37",
|
||||
"tranche_id": 2,
|
||||
"tx": "0x132a15c0b5f8c6016ac1a90437b9dca119ebc2479cde9c702327653fa3c11750"
|
||||
},
|
||||
{
|
||||
"amount": "2820.55961",
|
||||
"user": "0xF4c75FdbAe821C6B5DBB9f001bB23cBA58D1bA37",
|
||||
@@ -42578,8 +42803,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "200000",
|
||||
"withdrawn_tokens": "98152.824242",
|
||||
"remaining_tokens": "101847.175758"
|
||||
"withdrawn_tokens": "98678.473066",
|
||||
"remaining_tokens": "101321.526934"
|
||||
},
|
||||
{
|
||||
"address": "0x1b956E6c00E238194B331eddEFF72Cb5f28A8d01",
|
||||
@@ -43039,6 +43264,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "133.7749515525265",
|
||||
"user": "0xE6CacAE56Cca8dFdB7910b5A13578719D4E57DA0",
|
||||
"tranche_id": 2,
|
||||
"tx": "0xed77ed534414a1949b1473707782223907e0ac001f899037ff822f10bd4a358a"
|
||||
},
|
||||
{
|
||||
"amount": "345.7620519411935",
|
||||
"user": "0xE6CacAE56Cca8dFdB7910b5A13578719D4E57DA0",
|
||||
@@ -43305,8 +43536,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "12362.05",
|
||||
"withdrawn_tokens": "5949.173638923777",
|
||||
"remaining_tokens": "6412.876361076223"
|
||||
"withdrawn_tokens": "6082.9485904763035",
|
||||
"remaining_tokens": "6279.1014095236965"
|
||||
},
|
||||
{
|
||||
"address": "0xb091D456d0dFCB94dcba6f355379056C5bb995fC",
|
||||
@@ -43937,8 +44168,8 @@
|
||||
"tranche_start": "2021-11-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-05-05T00:00:00.000Z",
|
||||
"total_added": "14597706.0446472999",
|
||||
"total_removed": "5577113.077460725181649906",
|
||||
"locked_amount": "1737510.231376472448763806568635843",
|
||||
"total_removed": "5581251.054619088719392406",
|
||||
"locked_amount": "1583945.05221249870701141180921643",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "129284.449",
|
||||
@@ -44167,6 +44398,46 @@
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tx": "0x904f971f68cc70ba987688b8b26bcbd4e16ab1964b095de013e364f3ae7530c6"
|
||||
},
|
||||
{
|
||||
"amount": "617.72306195194457525",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tx": "0xcc545119eac3fe838555697dae55ce68273cb4c5f414c8983bac9f7ef8338f2c"
|
||||
},
|
||||
{
|
||||
"amount": "362.02846600644321975",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tx": "0xbecb2d5ad72cdfe972b450c388f111d9262c39896c71acceecb5f672e50c3ce9"
|
||||
},
|
||||
{
|
||||
"amount": "413.27700835287819875",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tx": "0xa266ac6aa7627d396b9d1c668f80e26d95de431e9ed861922df4472e9158204b"
|
||||
},
|
||||
{
|
||||
"amount": "554.781448662443509",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tx": "0x5ca51119e0c93558ed046062407daab6e538468aa5c8a231ba41de34e53d1970"
|
||||
},
|
||||
{
|
||||
"amount": "778.22874658413131",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tx": "0x36fe352f02dc3716d4530a39337d348f5e6e493d48c1c8d72bfb533f36cdebcf"
|
||||
},
|
||||
{
|
||||
"amount": "740.226257938888461",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tx": "0x36f0caba9152dedc13a67b7d1aee4d50bf7ac78a7f9649baf62a71af6887c5d2"
|
||||
},
|
||||
{
|
||||
"amount": "131.273001887669473",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tx": "0x8636472fc307ddde44f9a3deca4a1f79fc49ae3741a3025c2b1dbc2cffc7e927"
|
||||
},
|
||||
{
|
||||
"amount": "540.43916697913899575",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tx": "0xafba0370359a26f6fb9cceebc691c927bc820834384ffa51cd9565240e3a2a59"
|
||||
},
|
||||
{
|
||||
"amount": "800.792718381487871",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
@@ -47146,6 +47417,54 @@
|
||||
"tranche_id": 3,
|
||||
"tx": "0x904f971f68cc70ba987688b8b26bcbd4e16ab1964b095de013e364f3ae7530c6"
|
||||
},
|
||||
{
|
||||
"amount": "617.72306195194457525",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tranche_id": 3,
|
||||
"tx": "0xcc545119eac3fe838555697dae55ce68273cb4c5f414c8983bac9f7ef8338f2c"
|
||||
},
|
||||
{
|
||||
"amount": "362.02846600644321975",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tranche_id": 3,
|
||||
"tx": "0xbecb2d5ad72cdfe972b450c388f111d9262c39896c71acceecb5f672e50c3ce9"
|
||||
},
|
||||
{
|
||||
"amount": "413.27700835287819875",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tranche_id": 3,
|
||||
"tx": "0xa266ac6aa7627d396b9d1c668f80e26d95de431e9ed861922df4472e9158204b"
|
||||
},
|
||||
{
|
||||
"amount": "554.781448662443509",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tranche_id": 3,
|
||||
"tx": "0x5ca51119e0c93558ed046062407daab6e538468aa5c8a231ba41de34e53d1970"
|
||||
},
|
||||
{
|
||||
"amount": "778.22874658413131",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tranche_id": 3,
|
||||
"tx": "0x36fe352f02dc3716d4530a39337d348f5e6e493d48c1c8d72bfb533f36cdebcf"
|
||||
},
|
||||
{
|
||||
"amount": "740.226257938888461",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tranche_id": 3,
|
||||
"tx": "0x36f0caba9152dedc13a67b7d1aee4d50bf7ac78a7f9649baf62a71af6887c5d2"
|
||||
},
|
||||
{
|
||||
"amount": "131.273001887669473",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tranche_id": 3,
|
||||
"tx": "0x8636472fc307ddde44f9a3deca4a1f79fc49ae3741a3025c2b1dbc2cffc7e927"
|
||||
},
|
||||
{
|
||||
"amount": "540.43916697913899575",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
"tranche_id": 3,
|
||||
"tx": "0xafba0370359a26f6fb9cceebc691c927bc820834384ffa51cd9565240e3a2a59"
|
||||
},
|
||||
{
|
||||
"amount": "800.792718381487871",
|
||||
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
|
||||
@@ -49662,8 +49981,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "359123.469575",
|
||||
"withdrawn_tokens": "316010.1011302183066365",
|
||||
"remaining_tokens": "43113.3684447816933635"
|
||||
"withdrawn_tokens": "320148.078288581844379",
|
||||
"remaining_tokens": "38975.391286418155621"
|
||||
},
|
||||
{
|
||||
"address": "0xBdd412797c1B78535Afc5F71503b91fAbD0160fB",
|
||||
@@ -51007,7 +51326,7 @@
|
||||
"tranche_end": "2023-04-05T00:00:00.000Z",
|
||||
"total_added": "5778205.3912159303",
|
||||
"total_removed": "3077573.36197477479025324",
|
||||
"locked_amount": "369597.397251054113600109778074383",
|
||||
"locked_amount": "308922.8662088976060115537634935795",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "552496.6455",
|
||||
@@ -53078,8 +53397,8 @@
|
||||
"tranche_start": "2022-06-05T00:00:00.000Z",
|
||||
"tranche_end": "2023-06-05T00:00:00.000Z",
|
||||
"total_added": "472355.6199999996",
|
||||
"total_removed": "34992.4553336210685",
|
||||
"locked_amount": "124220.90056009124187109393607304",
|
||||
"total_removed": "35108.9309805010685",
|
||||
"locked_amount": "116787.680300232033164283987519012",
|
||||
"deposits": [
|
||||
{
|
||||
"amount": "3000",
|
||||
@@ -59733,6 +60052,21 @@
|
||||
"user": "0x454D87e666a106F3545B7cde91F32aE8B9616a72",
|
||||
"tx": "0xcefe8932dc5a8ef8bbbb4f4ac1ec86c2de0e064cab91c381e222411c4661d34a"
|
||||
},
|
||||
{
|
||||
"amount": "40.986757988",
|
||||
"user": "0xf751033D4e6864a88Be93E36258246F26AEf577c",
|
||||
"tx": "0x2f4a8c1de411b2a4c858e06633748bd342c23e0ad30910fc30d562f59c86de36"
|
||||
},
|
||||
{
|
||||
"amount": "41.084474888",
|
||||
"user": "0xD27929d68ac0E5fd5C919A5eb5968C1D06D3Fb83",
|
||||
"tx": "0xe3f5009fa2adff80e693976889890af035747c6cba3a68ce54295c9e5cb5e51f"
|
||||
},
|
||||
{
|
||||
"amount": "34.404414004",
|
||||
"user": "0x7C8D2D8BcFffcD48dBcb65C5Bb7588B66dcD22bb",
|
||||
"tx": "0x0f8f46d485aa188ed4dc215d004a6edb9af15f9cf22a0fb5544cd61a84357872"
|
||||
},
|
||||
{
|
||||
"amount": "57.900076104",
|
||||
"user": "0x96d882E908C06cD697Ea07266Fb8D14ae129A50b",
|
||||
@@ -73835,6 +74169,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "34.404414004",
|
||||
"user": "0x7C8D2D8BcFffcD48dBcb65C5Bb7588B66dcD22bb",
|
||||
"tranche_id": 5,
|
||||
"tx": "0x0f8f46d485aa188ed4dc215d004a6edb9af15f9cf22a0fb5544cd61a84357872"
|
||||
},
|
||||
{
|
||||
"amount": "21.74094368",
|
||||
"user": "0x7C8D2D8BcFffcD48dBcb65C5Bb7588B66dcD22bb",
|
||||
@@ -73873,8 +74213,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "400",
|
||||
"withdrawn_tokens": "265.545192792",
|
||||
"remaining_tokens": "134.454807208"
|
||||
"withdrawn_tokens": "299.949606796",
|
||||
"remaining_tokens": "100.050393204"
|
||||
},
|
||||
{
|
||||
"address": "0xc56F9f1d5f124655FA9CB8f85C7701E13a2fBF4D",
|
||||
@@ -78324,6 +78664,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "40.986757988",
|
||||
"user": "0xf751033D4e6864a88Be93E36258246F26AEf577c",
|
||||
"tranche_id": 5,
|
||||
"tx": "0x2f4a8c1de411b2a4c858e06633748bd342c23e0ad30910fc30d562f59c86de36"
|
||||
},
|
||||
{
|
||||
"amount": "108.551141552",
|
||||
"user": "0xf751033D4e6864a88Be93E36258246F26AEf577c",
|
||||
@@ -78338,8 +78684,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "400",
|
||||
"withdrawn_tokens": "255.898008624",
|
||||
"remaining_tokens": "144.101991376"
|
||||
"withdrawn_tokens": "296.884766612",
|
||||
"remaining_tokens": "103.115233388"
|
||||
},
|
||||
{
|
||||
"address": "0x667ee9816155807e4120dba069CC42aED064399b",
|
||||
@@ -81370,6 +81716,12 @@
|
||||
}
|
||||
],
|
||||
"withdrawals": [
|
||||
{
|
||||
"amount": "41.084474888",
|
||||
"user": "0xD27929d68ac0E5fd5C919A5eb5968C1D06D3Fb83",
|
||||
"tranche_id": 5,
|
||||
"tx": "0xe3f5009fa2adff80e693976889890af035747c6cba3a68ce54295c9e5cb5e51f"
|
||||
},
|
||||
{
|
||||
"amount": "39.564687976",
|
||||
"user": "0xD27929d68ac0E5fd5C919A5eb5968C1D06D3Fb83",
|
||||
@@ -81426,8 +81778,8 @@
|
||||
}
|
||||
],
|
||||
"total_tokens": "400",
|
||||
"withdrawn_tokens": "258.68491882",
|
||||
"remaining_tokens": "141.31508118"
|
||||
"withdrawn_tokens": "299.769393708",
|
||||
"remaining_tokens": "100.230606292"
|
||||
},
|
||||
{
|
||||
"address": "0xF5037DDA4A660d67560200f45380FF8364e35540",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -20,46 +20,78 @@ describe('positions', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('Positions').click();
|
||||
validatePositionsDisplayed();
|
||||
});
|
||||
|
||||
it('renders position among some graphql errors', () => {
|
||||
const errors = [
|
||||
{
|
||||
message: 'no market data for market: market-2',
|
||||
path: ['market', 'data'],
|
||||
extensions: {
|
||||
code: 13,
|
||||
type: 'Internal',
|
||||
describe('renders position among some graphql errors', () => {
|
||||
it('rows should be displayed despite errors', () => {
|
||||
const errors = [
|
||||
{
|
||||
message:
|
||||
'no market data for market: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a',
|
||||
path: ['marketsConnection', 'edges'],
|
||||
extensions: {
|
||||
code: 13,
|
||||
type: 'Internal',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const marketData = marketsDataQuery();
|
||||
const edges = marketData.marketsConnection?.edges.map((market) => {
|
||||
const replace =
|
||||
market.node.data?.market.id === 'market-2' ? null : market.node.data;
|
||||
return { ...market, node: { ...market.node, data: replace } };
|
||||
});
|
||||
const overrides = {
|
||||
...marketData,
|
||||
marketsConnection: { ...marketData.marketsConnection, edges },
|
||||
};
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'MarketsData', overrides, errors);
|
||||
});
|
||||
cy.visit('/#/markets/market-0');
|
||||
const emptyCells = [
|
||||
'notional',
|
||||
'markPrice',
|
||||
'liquidationPrice',
|
||||
'currentLeverage',
|
||||
'averageEntryPrice',
|
||||
];
|
||||
cy.getByTestId('tab-positions').within(() => {
|
||||
cy.get('[row-id="market-2"]').within(() => {
|
||||
emptyCells.forEach((cell) => {
|
||||
cy.get(`[col-id="${cell}"]`).should('contain.text', '-');
|
||||
];
|
||||
const marketData = marketsDataQuery();
|
||||
const edges = marketData.marketsConnection?.edges.map((market) => {
|
||||
const replace =
|
||||
market.node.data?.market.id === 'market-2' ? null : market.node.data;
|
||||
return { ...market, node: { ...market.node, data: replace } };
|
||||
});
|
||||
const overrides = {
|
||||
...marketData,
|
||||
marketsConnection: { ...marketData.marketsConnection, edges },
|
||||
};
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'MarketsData', overrides, errors);
|
||||
});
|
||||
cy.visit('/#/markets/market-0');
|
||||
const emptyCells = [
|
||||
'notional',
|
||||
'markPrice',
|
||||
'liquidationPrice',
|
||||
'currentLeverage',
|
||||
'averageEntryPrice',
|
||||
];
|
||||
cy.getByTestId('tab-positions').within(() => {
|
||||
cy.get('[row-id="market-2"]').within(() => {
|
||||
emptyCells.forEach((cell) => {
|
||||
cy.get(`[col-id="${cell}"]`).should('contain.text', '-');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
it('error message should be displayed', () => {
|
||||
const errors = [
|
||||
{
|
||||
message:
|
||||
'no market data for asset: 9c55fb644c6f7de5422d40d691a62bffd5898384c70135bab29ba1e3e2e5280a',
|
||||
path: ['assets', 'edges'],
|
||||
extensions: {
|
||||
code: 13,
|
||||
type: 'Internal',
|
||||
},
|
||||
},
|
||||
];
|
||||
const marketData = marketsDataQuery();
|
||||
const edges = marketData.marketsConnection?.edges.map((market) => {
|
||||
const replace =
|
||||
market.node.data?.market.id === 'market-2' ? null : market.node.data;
|
||||
return { ...market, node: { ...market.node, data: replace } };
|
||||
});
|
||||
const overrides = {
|
||||
...marketData,
|
||||
marketsConnection: { ...marketData.marketsConnection, edges },
|
||||
};
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'MarketsData', overrides, errors);
|
||||
});
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.get('.pointer-events-none.absolute.inset-0').contains(
|
||||
'Something went wrong:'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sorting by ag-grid columns should work well', () => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
|
||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/validator-testnet-network.json
|
||||
NX_VEGA_ENV=VALIDATOR_TESTNET
|
||||
NX_VEGA_EXPLORER_URL=https://dev.explorer.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_VEGA_TOKEN_URL=https://token.fairground.wtf
|
||||
NX_VEGA_EXPLORER_URL=https://validator-testnet.explorer.vega.xyz
|
||||
NX_VEGA_NETWORKS={\"STAGNET3\":\"https://stagnet3.console.vega.xyz\",\"STAGNET1\":\"https://stagnet1.console.vega.xyz\",\"TESTNET\":\"https://console.fairground.wtf\"}
|
||||
NX_VEGA_TOKEN_URL=https://validator-testnet.governance.fairground.wtf
|
||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ export const Banner = () => {
|
||||
<AnnouncementBanner>
|
||||
<div className="grid grid-cols-[auto_1fr] gap-4 font-alpha calt uppercase text-center text-lg text-white">
|
||||
<button
|
||||
className="flex items-center"
|
||||
onClick={() => update({ shouldDisplayAnnouncementBanner: false })}
|
||||
>
|
||||
<Icon name="cross" className="w-6 h-6" ariaLabel="dismiss" />
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
.pre-loader {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.loader-item {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: black;
|
||||
}
|
||||
.pre-loader-center {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.pre-loader-wrapper {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@for $i from 0 through 16 {
|
||||
.loader-item:nth-child(#{$i}) {
|
||||
@if $i % 2 == 0 {
|
||||
animation-delay: #{$i * 50 * random(5)}ms;
|
||||
animation-direction: reverse;
|
||||
} @else {
|
||||
animation-delay: #{$i * -50 * random(5)}ms;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
}
|
||||
}
|
||||
.loader-item {
|
||||
animation: flickering 0.4s linear alternate infinite;
|
||||
}
|
||||
@keyframes flickering {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
25% {
|
||||
opacity: 1;
|
||||
}
|
||||
26% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
html.dark {
|
||||
.pre-loader {
|
||||
.loader-item {
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.'
|
||||
)}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import type { Market, MarketData } from '@vegaprotocol/market-list';
|
||||
import {
|
||||
@@ -26,7 +27,7 @@ export const DealTicketFeeDetails = ({
|
||||
marketData,
|
||||
}: DealTicketFeeDetailsProps) => {
|
||||
const feeDetails = useFeeDealTicketDetails(order, market, marketData);
|
||||
const details = getFeeDetailsValues(feeDetails);
|
||||
const details = useMemo(() => getFeeDetailsValues(feeDetails), [feeDetails]);
|
||||
return (
|
||||
<div>
|
||||
{details.map(({ label, value, labelDescription, symbol }) => (
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
usePersistedOrderStore,
|
||||
usePersistedOrderStoreSubscription,
|
||||
} from '@vegaprotocol/orders';
|
||||
import { OrderType } from '@vegaprotocol/types';
|
||||
|
||||
export type TransactionStatus = 'default' | 'pending';
|
||||
|
||||
@@ -82,7 +83,12 @@ export const DealTicket = ({
|
||||
const order = watch();
|
||||
|
||||
watch((orderData) => {
|
||||
setPersistedOrder(orderData as DealTicketFormFields);
|
||||
const persistable = !(
|
||||
orderData.type === OrderType.TYPE_LIMIT && orderData.price === ''
|
||||
);
|
||||
if (persistable) {
|
||||
setPersistedOrder(orderData as DealTicketFormFields);
|
||||
}
|
||||
});
|
||||
|
||||
usePersistedOrderStoreSubscription(market.id, (storedOrder) => {
|
||||
@@ -369,7 +375,7 @@ const SummaryMessage = memo(
|
||||
if (balanceError) {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<MarginWarning balance={balance} margin={margin} asset={asset} />;
|
||||
<MarginWarning balance={balance} margin={margin} asset={asset} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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} />,
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
@@ -22,7 +22,11 @@ import {
|
||||
normalizeOrderAmendment,
|
||||
useVegaTransactionStore,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import type { VegaTxState, TransactionResult } from '@vegaprotocol/wallet';
|
||||
import type {
|
||||
VegaTxState,
|
||||
TransactionResult,
|
||||
OrderTxUpdateFieldsFragment,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import { OrderEditDialog } from '../order-list/order-edit-dialog';
|
||||
import type { OrderSubFieldsFragment } from '../../order-hooks';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
@@ -94,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 (
|
||||
<>
|
||||
@@ -139,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}
|
||||
@@ -203,7 +221,19 @@ export const OrderListManager = ({
|
||||
fields.limitPrice,
|
||||
fields.size
|
||||
);
|
||||
create({ orderAmendment });
|
||||
const originalOrder: OrderTxUpdateFieldsFragment = {
|
||||
type: editOrder.type,
|
||||
id: editOrder.id,
|
||||
status: editOrder.status,
|
||||
createdAt: editOrder.createdAt,
|
||||
size: editOrder.size,
|
||||
price: editOrder.price,
|
||||
timeInForce: editOrder.timeInForce,
|
||||
expiresAt: editOrder.expiresAt,
|
||||
side: editOrder.side,
|
||||
marketId: editOrder.market.id,
|
||||
};
|
||||
create({ orderAmendment }, originalOrder);
|
||||
setEditOrder(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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,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
@@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,6 +15,15 @@ import {
|
||||
signedNumberCssClass,
|
||||
signedNumberCssClassRules,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import {
|
||||
ButtonLink,
|
||||
Tooltip,
|
||||
TooltipCellComponent,
|
||||
Link,
|
||||
ExternalLink,
|
||||
Icon,
|
||||
ProgressBarCell,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
volumePrefix,
|
||||
toBigNum,
|
||||
@@ -27,13 +36,9 @@ 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 +172,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
)
|
||||
);
|
||||
}}
|
||||
cellRenderer={OpenVolumeCell}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Mark price')}
|
||||
@@ -311,7 +317,6 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
}}
|
||||
valueFormatter={({
|
||||
data,
|
||||
node,
|
||||
}: VegaValueFormatterParams<Position, 'marginAccountBalance'>):
|
||||
| string
|
||||
| undefined => {
|
||||
@@ -329,7 +334,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 +353,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 +379,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 +416,106 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
|
||||
);
|
||||
|
||||
export default PositionsTable;
|
||||
|
||||
export const PNLCell = ({
|
||||
valueFormatted,
|
||||
data,
|
||||
}: VegaICellRendererParams<Position, 'realisedPNL'>) => {
|
||||
const { VEGA_DOCS_URL } = useEnvironment();
|
||||
|
||||
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={VEGA_DOCS_URL}>
|
||||
{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}</>;
|
||||
}
|
||||
|
||||
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={VEGA_DOCS_URL}>
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
global.ResizeObserver = ResizeObserver;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { captureException } from '@sentry/react';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { localLoggerFactory } from '@vegaprotocol/utils';
|
||||
|
||||
type ResizeObserverConfiguration = {
|
||||
debounceTime: number;
|
||||
@@ -19,20 +19,26 @@ export function useResizeObserver(
|
||||
callback: ResizeObserverCallback,
|
||||
options: ResizeObserverConfiguration = DEFAULT_OPTIONS
|
||||
) {
|
||||
const wrappedCb = useCallback(
|
||||
(entries: ResizeObserverEntry[], observer: ResizeObserver) =>
|
||||
window.requestAnimationFrame(() => {
|
||||
options.debounceTime > 0
|
||||
? debounce(callback, options.debounceTime)(entries, observer)
|
||||
: callback(entries, observer);
|
||||
}),
|
||||
[callback, options.debounceTime]
|
||||
);
|
||||
|
||||
const observer = useMemo(() => {
|
||||
return new ResizeObserver(
|
||||
options.debounceTime > 0
|
||||
? debounce(callback, options.debounceTime)
|
||||
: callback
|
||||
);
|
||||
}, [callback, options.debounceTime]);
|
||||
return new ResizeObserver(wrappedCb);
|
||||
}, [wrappedCb]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!observer || !target) return;
|
||||
try {
|
||||
observer.observe(target, options.config);
|
||||
} catch (err) {
|
||||
captureException(err);
|
||||
localLoggerFactory({ application: 'react-helpers' }).debug(err as Error);
|
||||
}
|
||||
return () => observer?.disconnect();
|
||||
}, [observer, options.config, target]);
|
||||
|
||||
@@ -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,2 +1 @@
|
||||
export * from './ascii-to-hex';
|
||||
export * from './prepend-0x';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -82,7 +82,7 @@ export const Notification = ({
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm">{message}</div>
|
||||
<div className="text-sm [word-break:break-word]">{message}</div>
|
||||
{buttonProps && (
|
||||
<Button
|
||||
size={buttonProps.size || 'sm'}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { ApolloError } from '@apollo/client';
|
||||
import type { GraphQLErrors } from '@apollo/client/errors';
|
||||
|
||||
const NOT_FOUND = 'NotFound';
|
||||
const INTERNAL = 'Internal';
|
||||
|
||||
const isApolloGraphQLError = (
|
||||
error: ApolloError | Error | undefined
|
||||
@@ -29,12 +28,5 @@ export const isNotFoundGraphQLError = (
|
||||
);
|
||||
};
|
||||
|
||||
export const marketDataErrorPolicyGuard = (errors: GraphQLErrors) => {
|
||||
const path = ['market', 'data'];
|
||||
return errors.every(
|
||||
(e) =>
|
||||
e.extensions &&
|
||||
e.extensions['type'] === INTERNAL &&
|
||||
(!path || path.every((item, i) => item === e?.path?.[i]))
|
||||
);
|
||||
};
|
||||
export const marketDataErrorPolicyGuard = (errors: GraphQLErrors) =>
|
||||
errors.every((e) => e.message.match(/no market data for market:/i));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user