feat(explorer): add basic votesubmission tx view (#2694)
This commit is contained in:
parent
3188f23439
commit
24c017bc2b
@ -6,4 +6,5 @@ NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https:
|
|||||||
NX_VEGA_ENV=DEVNET
|
NX_VEGA_ENV=DEVNET
|
||||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||||
NX_BLOCK_EXPLORER=https://be.devnet1.vega.xyz/rest
|
NX_BLOCK_EXPLORER=https://be.devnet1.vega.xyz/rest
|
||||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||||
|
NX_VEGA_GOVERNANCE_URL=https://dev.token.vega.xyz
|
@ -7,3 +7,4 @@ NX_VEGA_ENV=MAINNET
|
|||||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||||
NX_BLOCK_EXPLORER=https://be.explorer.vega.xyz/rest/
|
NX_BLOCK_EXPLORER=https://be.explorer.vega.xyz/rest/
|
||||||
NX_ETHERSCAN_URL=https://etherscan.io
|
NX_ETHERSCAN_URL=https://etherscan.io
|
||||||
|
NX_VEGA_GOVERNANCE_URL=https://token.vega.xyz
|
||||||
|
@ -7,6 +7,7 @@ NX_VEGA_ENV=MIRROR
|
|||||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||||
NX_BLOCK_EXPLORER=https://be.mainnet-mirror.vega.xyz/rest/
|
NX_BLOCK_EXPLORER=https://be.mainnet-mirror.vega.xyz/rest/
|
||||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||||
|
NX_VEGA_GOVERNANCE_URL=https://mainnet-mirror.token.vega.xyz
|
||||||
|
|
||||||
# App flags
|
# App flags
|
||||||
NX_EXPLORER_ASSETS=1
|
NX_EXPLORER_ASSETS=1
|
||||||
|
@ -9,3 +9,4 @@ NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https:
|
|||||||
NX_TENDERMINT_URL=https://tm.n01.sandbox.vega.xyz
|
NX_TENDERMINT_URL=https://tm.n01.sandbox.vega.xyz
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.sandbox.vega.xyz/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.sandbox.vega.xyz/websocket
|
||||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||||
|
NX_VEGA_GOVERNANCE_URL=https://sandbox.token.vega.xyz
|
||||||
|
@ -11,3 +11,4 @@ NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.xyz
|
|||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
|
||||||
NX_BLOCK_EXPLORER=https://be.stagnet1.vega.xyz/rest
|
NX_BLOCK_EXPLORER=https://be.stagnet1.vega.xyz/rest
|
||||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||||
|
NX_VEGA_GOVERNANCE_URL=https://stagnet1.token.vega.xyz
|
||||||
|
@ -4,4 +4,5 @@ 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={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||||
NX_VEGA_ENV=STAGNET3
|
NX_VEGA_ENV=STAGNET3
|
||||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||||
NX_BLOCK_EXPLORER=https://be.stagnet3.vega.xyz/rest
|
NX_BLOCK_EXPLORER=https://be.stagnet3.vega.xyz/rest
|
||||||
|
NX_VEGA_GOVERNANCE_URL=https://stagnet3.token.vega.xyz
|
||||||
|
@ -10,3 +10,4 @@ NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql
|
|||||||
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
||||||
NX_VEGA_NETWORKS={}
|
NX_VEGA_NETWORKS={}
|
||||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||||
|
NX_VEGA_GOVERNANCE_URL=https://token.fairground.wtf
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
query ExplorerProposal($id: ID!) {
|
||||||
|
proposal(id: $id) {
|
||||||
|
id
|
||||||
|
rationale {
|
||||||
|
title
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
52
apps/explorer/src/app/components/links/proposal-link/__generated__/Proposal.ts
generated
Normal file
52
apps/explorer/src/app/components/links/proposal-link/__generated__/Proposal.ts
generated
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import * as Types from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import * as Apollo from '@apollo/client';
|
||||||
|
const defaultOptions = {} as const;
|
||||||
|
export type ExplorerProposalQueryVariables = Types.Exact<{
|
||||||
|
id: Types.Scalars['ID'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type ExplorerProposalQuery = { __typename?: 'Query', proposal?: { __typename?: 'Proposal', id?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string } } | null };
|
||||||
|
|
||||||
|
|
||||||
|
export const ExplorerProposalDocument = gql`
|
||||||
|
query ExplorerProposal($id: ID!) {
|
||||||
|
proposal(id: $id) {
|
||||||
|
id
|
||||||
|
rationale {
|
||||||
|
title
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useExplorerProposalQuery__
|
||||||
|
*
|
||||||
|
* To run a query within a React component, call `useExplorerProposalQuery` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useExplorerProposalQuery` 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 } = useExplorerProposalQuery({
|
||||||
|
* variables: {
|
||||||
|
* id: // value for 'id'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useExplorerProposalQuery(baseOptions: Apollo.QueryHookOptions<ExplorerProposalQuery, ExplorerProposalQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useQuery<ExplorerProposalQuery, ExplorerProposalQueryVariables>(ExplorerProposalDocument, options);
|
||||||
|
}
|
||||||
|
export function useExplorerProposalLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerProposalQuery, ExplorerProposalQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useLazyQuery<ExplorerProposalQuery, ExplorerProposalQueryVariables>(ExplorerProposalDocument, options);
|
||||||
|
}
|
||||||
|
export type ExplorerProposalQueryHookResult = ReturnType<typeof useExplorerProposalQuery>;
|
||||||
|
export type ExplorerProposalLazyQueryHookResult = ReturnType<typeof useExplorerProposalLazyQuery>;
|
||||||
|
export type ExplorerProposalQueryResult = Apollo.QueryResult<ExplorerProposalQuery, ExplorerProposalQueryVariables>;
|
@ -0,0 +1,85 @@
|
|||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import type { MockedResponse } from '@apollo/client/testing';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import ProposalLink from './proposal-link';
|
||||||
|
import { ExplorerProposalDocument } from './__generated__/Proposal';
|
||||||
|
import { GraphQLError } from 'graphql';
|
||||||
|
|
||||||
|
function renderComponent(id: string, mocks: MockedResponse[]) {
|
||||||
|
return (
|
||||||
|
<MockedProvider mocks={mocks} addTypename={false}>
|
||||||
|
<MemoryRouter>
|
||||||
|
<ProposalLink id={id} />
|
||||||
|
</MemoryRouter>
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Proposal link component', () => {
|
||||||
|
it('Renders the ID at first', () => {
|
||||||
|
const res = render(renderComponent('123', []));
|
||||||
|
expect(res.getByText('123')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders the ID with an emoji on error', async () => {
|
||||||
|
const mock = {
|
||||||
|
request: {
|
||||||
|
query: ExplorerProposalDocument,
|
||||||
|
variables: {
|
||||||
|
id: '456',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
errors: [new GraphQLError('No such proposal')],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const res = render(renderComponent('456', [mock]));
|
||||||
|
// The ID
|
||||||
|
expect(res.getByText('456')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// The emoji
|
||||||
|
expect(await res.findByRole('img')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders the proposal title when the query returns a result', async () => {
|
||||||
|
const mock = {
|
||||||
|
request: {
|
||||||
|
query: ExplorerProposalDocument,
|
||||||
|
variables: {
|
||||||
|
id: '123',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
data: {
|
||||||
|
proposal: {
|
||||||
|
id: '123',
|
||||||
|
rationale: {
|
||||||
|
title: 'test-title',
|
||||||
|
description: 'test description',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = render(renderComponent('123', [mock]));
|
||||||
|
expect(res.getByText('123')).toBeInTheDocument();
|
||||||
|
expect(await res.findByText('test-title')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Leaves the proposal id when the market is not found', async () => {
|
||||||
|
const mock = {
|
||||||
|
request: {
|
||||||
|
query: ExplorerProposalDocument,
|
||||||
|
variables: {
|
||||||
|
id: '123',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
error: new Error('No such asset'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = render(renderComponent('123', [mock]));
|
||||||
|
expect(await res.findByText('123')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,23 @@
|
|||||||
|
import { useExplorerProposalQuery } from './__generated__/Proposal';
|
||||||
|
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { ENV } from '../../../config/env';
|
||||||
|
export type ProposalLinkProps = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a proposal ID, generates an external link over to
|
||||||
|
* the Governance page for more information
|
||||||
|
*/
|
||||||
|
const ProposalLink = ({ id }: ProposalLinkProps) => {
|
||||||
|
const { data } = useExplorerProposalQuery({
|
||||||
|
variables: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
const base = ENV.dataSources.governanceUrl;
|
||||||
|
const label = data?.proposal?.rationale.title || id;
|
||||||
|
|
||||||
|
return <ExternalLink href={`${base}/proposals/${id}`}>{label}</ExternalLink>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProposalLink;
|
@ -20,6 +20,7 @@ import { TxDetailsLiquiditySubmission } from './tx-liquidity-submission';
|
|||||||
import { TxDetailsLiquidityAmendment } from './tx-liquidity-amend';
|
import { TxDetailsLiquidityAmendment } from './tx-liquidity-amend';
|
||||||
import { TxDetailsLiquidityCancellation } from './tx-liquidity-cancel';
|
import { TxDetailsLiquidityCancellation } from './tx-liquidity-cancel';
|
||||||
import { TxDetailsDataSubmission } from './tx-data-submission';
|
import { TxDetailsDataSubmission } from './tx-data-submission';
|
||||||
|
import { TxProposalVote } from './tx-proposal-vote';
|
||||||
|
|
||||||
interface TxDetailsWrapperProps {
|
interface TxDetailsWrapperProps {
|
||||||
txData: BlockExplorerTransactionResult | undefined;
|
txData: BlockExplorerTransactionResult | undefined;
|
||||||
@ -91,6 +92,8 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
|
|||||||
return TxDetailsOrderAmend;
|
return TxDetailsOrderAmend;
|
||||||
case 'Validator Heartbeat':
|
case 'Validator Heartbeat':
|
||||||
return TxDetailsHeartbeat;
|
return TxDetailsHeartbeat;
|
||||||
|
case 'Vote on Proposal':
|
||||||
|
return TxProposalVote;
|
||||||
case 'Batch Market Instructions':
|
case 'Batch Market Instructions':
|
||||||
return TxDetailsBatch;
|
return TxDetailsBatch;
|
||||||
case 'Chain Event':
|
case 'Chain Event':
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
||||||
|
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||||
|
import { TxDetailsShared } from './shared/tx-details-shared';
|
||||||
|
import { TableCell, TableRow, TableWithTbody } from '../../table';
|
||||||
|
import ProposalLink from '../../links/proposal-link/proposal-link';
|
||||||
|
|
||||||
|
interface TxProposalVoteProps {
|
||||||
|
txData: BlockExplorerTransactionResult | undefined;
|
||||||
|
pubKey: string | undefined;
|
||||||
|
blockData: TendermintBlocksResponse | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A vote on a proposal.
|
||||||
|
*
|
||||||
|
* One inconsistency here that feels right but should be standardised is that there are two rows
|
||||||
|
* for the proposal ID, one that creates a link with the text of the proposal title that takes
|
||||||
|
* a user out to the governance site, and the other that just shows the ID. Both are useful, but
|
||||||
|
* doesn't feel quite right. This could be fixed with a separate component to display a preview
|
||||||
|
* of the proposal and link off to the governance site, removing the title from the header. Or
|
||||||
|
* something else. For now, this is more useful than the default view
|
||||||
|
*/
|
||||||
|
export const TxProposalVote = ({
|
||||||
|
txData,
|
||||||
|
pubKey,
|
||||||
|
blockData,
|
||||||
|
}: TxProposalVoteProps) => {
|
||||||
|
if (!txData || !txData.command.voteSubmission) {
|
||||||
|
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vote = txData.command.voteSubmission.value ? '👍' : '👎';
|
||||||
|
return (
|
||||||
|
<TableWithTbody className="mb-8">
|
||||||
|
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('Proposal ID')}</TableCell>
|
||||||
|
<TableCell>{txData.command.voteSubmission.proposalId}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('Proposal details')}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<ProposalLink id={txData.command.voteSubmission.proposalId} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('Proposal')}</TableCell>
|
||||||
|
<TableCell>{txData.command.voteSubmission.proposalId}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('Vote')}</TableCell>
|
||||||
|
<TableCell>{vote}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableWithTbody>
|
||||||
|
);
|
||||||
|
};
|
@ -16,6 +16,7 @@ export const ENV = {
|
|||||||
tendermintUrl: windowOrDefault('NX_TENDERMINT_URL'),
|
tendermintUrl: windowOrDefault('NX_TENDERMINT_URL'),
|
||||||
tendermintWebsocketUrl: windowOrDefault('NX_TENDERMINT_WEBSOCKET_URL'),
|
tendermintWebsocketUrl: windowOrDefault('NX_TENDERMINT_WEBSOCKET_URL'),
|
||||||
ethExplorerUrl: windowOrDefault('NX_ETHERSCAN_URL'),
|
ethExplorerUrl: windowOrDefault('NX_ETHERSCAN_URL'),
|
||||||
|
governanceUrl: windowOrDefault('NX_VEGA_GOVERNANCE_URL'),
|
||||||
},
|
},
|
||||||
flags: {
|
flags: {
|
||||||
assets: truthy.includes(windowOrDefault('NX_EXPLORER_ASSETS')),
|
assets: truthy.includes(windowOrDefault('NX_EXPLORER_ASSETS')),
|
||||||
|
Loading…
Reference in New Issue
Block a user