feat(explorer): extend transfers view

This commit is contained in:
Edd 2024-01-18 17:34:45 +00:00
parent 532ad3a4b9
commit 496b0b5a90
No known key found for this signature in database
10 changed files with 262 additions and 17 deletions

View File

@ -64,7 +64,9 @@ export const ProposalSummary = ({
return ( return (
<div className="w-auto max-w-lg border-2 border-solid border-vega-light-100 dark:border-vega-dark-200 p-5"> <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} />} {id && <ProposalStatusIcon id={id} />}
{rationale?.title && <h1 className="text-xl pb-1">{rationale.title}</h1>} {rationale?.title && (
<h1 className="text-xl pb-1 break-all">{rationale.title}</h1>
)}
{rationale?.description && ( {rationale?.description && (
<div className="pt-2 text-sm leading-tight"> <div className="pt-2 text-sm leading-tight">
<ReactMarkdown <ReactMarkdown

View File

@ -0,0 +1,16 @@
query ExplorerTransferVote($id: ID!) {
transfer(id: $id) {
reference
timestamp
status
reason
fromAccountType
from
to
toAccountType
asset {
id
}
amount
}
}

View File

@ -0,0 +1,59 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerTransferVoteQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
export type ExplorerTransferVoteQuery = { __typename?: 'Query', transfer?: { __typename?: 'Transfer', reference?: string | null, timestamp: any, status: Types.TransferStatus, reason?: string | null, fromAccountType: Types.AccountType, from: string, to: string, toAccountType: Types.AccountType, amount: string, asset?: { __typename?: 'Asset', id: string } | null } | null };
export const ExplorerTransferVoteDocument = gql`
query ExplorerTransferVote($id: ID!) {
transfer(id: $id) {
reference
timestamp
status
reason
fromAccountType
from
to
toAccountType
asset {
id
}
amount
}
}
`;
/**
* __useExplorerTransferVoteQuery__
*
* To run a query within a React component, call `useExplorerTransferVoteQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerTransferVoteQuery` 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 } = useExplorerTransferVoteQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useExplorerTransferVoteQuery(baseOptions: Apollo.QueryHookOptions<ExplorerTransferVoteQuery, ExplorerTransferVoteQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerTransferVoteQuery, ExplorerTransferVoteQueryVariables>(ExplorerTransferVoteDocument, options);
}
export function useExplorerTransferVoteLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerTransferVoteQuery, ExplorerTransferVoteQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerTransferVoteQuery, ExplorerTransferVoteQueryVariables>(ExplorerTransferVoteDocument, options);
}
export type ExplorerTransferVoteQueryHookResult = ReturnType<typeof useExplorerTransferVoteQuery>;
export type ExplorerTransferVoteLazyQueryHookResult = ReturnType<typeof useExplorerTransferVoteLazyQuery>;
export type ExplorerTransferVoteQueryResult = Apollo.QueryResult<ExplorerTransferVoteQuery, ExplorerTransferVoteQueryVariables>;

View File

@ -111,7 +111,7 @@ export function TransferParticipants({
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 9" viewBox="0 0 16 9"
className="fill-vega-light-100 dark:fill-black" className="fill-white dark:fill-black"
> >
<path d="M0,0L8,9l8,-9Z" /> <path d="M0,0L8,9l8,-9Z" />
</svg> </svg>
@ -120,7 +120,7 @@ export function TransferParticipants({
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 9" viewBox="0 0 16 9"
className="fill-vega-light-100 dark:fill-vega-dark-200" className="fill-vega-light-200 dark:fill-vega-dark-200"
> >
<path d="M0,0L8,9l8,-9Z" /> <path d="M0,0L8,9l8,-9Z" />
</svg> </svg>

View File

@ -4,6 +4,7 @@ import { headerClasses, wrapperClasses } from '../transfer-details';
import type { components } from '../../../../../../types/explorer'; import type { components } from '../../../../../../types/explorer';
import type { Recurring } from '../transfer-details'; import type { Recurring } from '../transfer-details';
import { DispatchMetricLabels } from '@vegaprotocol/types'; import { DispatchMetricLabels } from '@vegaprotocol/types';
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
export type Metric = components['schemas']['vegaDispatchMetric']; export type Metric = components['schemas']['vegaDispatchMetric'];
export type Strategy = components['schemas']['vegaDispatchStrategy']; export type Strategy = components['schemas']['vegaDispatchStrategy'];
@ -13,6 +14,15 @@ const metricLabels: Record<Metric, string> = {
...DispatchMetricLabels, ...DispatchMetricLabels,
}; };
// Maps the two (non-null) values of entityScope to the icon that represents it
const entityScopeIcons: Record<
string,
typeof VegaIconNames[keyof typeof VegaIconNames]
> = {
ENTITY_SCOPE_INDIVIDUALS: VegaIconNames.MAN,
ENTITY_SCOPE_TEAMS: VegaIconNames.TEAM,
};
interface TransferRewardsProps { interface TransferRewardsProps {
recurring: Recurring; recurring: Recurring;
} }
@ -34,7 +44,7 @@ export function TransferRewards({ recurring }: TransferRewardsProps) {
return ( return (
<div className={wrapperClasses}> <div className={wrapperClasses}>
<h2 className={headerClasses}>{t('Reward metrics')}</h2> <h2 className={headerClasses}>{t('Reward metrics')}</h2>
<ul className="relative block rounded-lg py-6 text-center p-6"> <ul className="relative block rounded-lg py-6 text-left p-6">
{recurring.dispatchStrategy.assetForMetric ? ( {recurring.dispatchStrategy.assetForMetric ? (
<li> <li>
<strong>{t('Asset')}</strong>:{' '} <strong>{t('Asset')}</strong>:{' '}
@ -44,6 +54,27 @@ export function TransferRewards({ recurring }: TransferRewardsProps) {
<li> <li>
<strong>{t('Metric')}</strong>: {metricLabels[metric]} <strong>{t('Metric')}</strong>: {metricLabels[metric]}
</li> </li>
{recurring.dispatchStrategy.entityScope &&
entityScopeIcons[recurring.dispatchStrategy.entityScope] ? (
<li>
<strong>{t('Scope')}</strong>:{' '}
<VegaIcon
name={entityScopeIcons[recurring.dispatchStrategy.entityScope]}
/>
</li>
) : null}
{recurring.dispatchStrategy.individualScope}
{recurring.dispatchStrategy.teamScope}
{recurring.dispatchStrategy.lockPeriod &&
recurring.dispatchStrategy.lockPeriod !== '0' ? (
<li>
<strong>{t('Lock')}</strong>:{' '}
{recurring.dispatchStrategy.lockPeriod}
</li>
) : null}
{recurring.dispatchStrategy.markets && {recurring.dispatchStrategy.markets &&
recurring.dispatchStrategy.markets.length > 0 ? ( recurring.dispatchStrategy.markets.length > 0 ? (
<li> <li>
@ -57,9 +88,30 @@ export function TransferRewards({ recurring }: TransferRewardsProps) {
</ul> </ul>
</li> </li>
) : null} ) : null}
<li>
<strong>{t('Factor')}</strong>: {recurring.factor} {recurring.dispatchStrategy.stakingRequirement &&
</li> recurring.dispatchStrategy.stakingRequirement !== '0' ? (
<li>
<strong>{t('Staking requirement')}</strong>:{' '}
{recurring.dispatchStrategy.stakingRequirement}
</li>
) : null}
{recurring.dispatchStrategy.windowLength &&
recurring.dispatchStrategy.windowLength !== '0' ? (
<li>
<strong>{t('Window length')}</strong>:{' '}
{recurring.dispatchStrategy.windowLength}
</li>
) : null}
{recurring.dispatchStrategy.rankTable &&
recurring.dispatchStrategy.rankTable.length > 0 ? (
<li>
<strong>{t('Ranks')}</strong>:{' '}
{recurring.dispatchStrategy.rankTable.toString()}
</li>
) : null}
</ul> </ul>
</div> </div>
); );

View File

@ -0,0 +1,40 @@
import { t } from '@vegaprotocol/i18n';
import { headerClasses, wrapperClasses } from '../transfer-details';
import { Icon, Loader } from '@vegaprotocol/ui-toolkit';
import type { ApolloError } from '@apollo/client';
interface TransferStatusProps {
status: string;
error: ApolloError | undefined;
loading: boolean;
}
/**
* Renderer for a transfer. These can vary quite
* widely, essentially every field can be null.
*
* @param transfer A recurring transfer object
*/
export function TransferStatusView({ status, loading }: TransferStatusProps) {
if (!status) {
return null;
}
return (
<div className={wrapperClasses}>
<h2 className={headerClasses}>{t('Status')}</h2>
<div className="relative block rounded-lg py-6 text-center p-6">
{loading ? (
<Loader />
) : (
<>
<p className="leading-10 my-2">
<Icon name="tick" className="text-green-500" />
</p>
<p className="leading-10 my-2">{status}</p>
</>
)}
</div>
</div>
);
}

View File

@ -2,6 +2,9 @@ import type { components } from '../../../../../types/explorer';
import { TransferRepeat } from './blocks/transfer-repeat'; import { TransferRepeat } from './blocks/transfer-repeat';
import { TransferRewards } from './blocks/transfer-rewards'; import { TransferRewards } from './blocks/transfer-rewards';
import { TransferParticipants } from './blocks/transfer-participants'; import { TransferParticipants } from './blocks/transfer-participants';
import { useExplorerTransferVoteQuery } from './__generated__/Transfer';
import { TransferStatusView } from './blocks/transfer-status';
import { TransferStatus } from '@vegaprotocol/types';
export type Recurring = components['schemas']['commandsv1RecurringTransfer']; export type Recurring = components['schemas']['commandsv1RecurringTransfer'];
export type Metric = components['schemas']['vegaDispatchMetric']; export type Metric = components['schemas']['vegaDispatchMetric'];
@ -16,6 +19,9 @@ export type Transfer = components['schemas']['commandsv1Transfer'];
interface TransferDetailsProps { interface TransferDetailsProps {
transfer: Transfer; transfer: Transfer;
from: string; from: string;
id: string;
// If set, all blocks except the status one are hidden
statusOnly?: boolean;
} }
/** /**
@ -24,15 +30,37 @@ interface TransferDetailsProps {
* *
* @param transfer A recurring transfer object * @param transfer A recurring transfer object
*/ */
export function TransferDetails({ transfer, from }: TransferDetailsProps) { export function TransferDetails({
transfer,
from,
id,
statusOnly = false,
}: TransferDetailsProps) {
const recurring = transfer.recurring; const recurring = transfer.recurring;
// Currently all this is passed in to TransferStatus, but the extra details
// may be useful in the future.
const { data, error, loading } = useExplorerTransferVoteQuery({
variables: { id },
});
const status = error
? TransferStatus.STATUS_REJECTED
: data?.transfer?.status;
return ( return (
<div className="flex gap-5 flex-wrap"> <div className="flex gap-5 flex-wrap">
<TransferParticipants from={from} transfer={transfer} /> {statusOnly ? null : (
{recurring ? <TransferRepeat recurring={transfer.recurring} /> : null} <>
{recurring && recurring.dispatchStrategy ? ( <TransferParticipants from={from} transfer={transfer} />
<TransferRewards recurring={transfer.recurring} /> {recurring ? <TransferRepeat recurring={transfer.recurring} /> : null}
{recurring && recurring.dispatchStrategy ? (
<TransferRewards recurring={transfer.recurring} />
) : null}
</>
)}
{status ? (
<TransferStatusView status={status} error={error} loading={loading} />
) : null} ) : null}
</div> </div>
); );

View File

@ -12,6 +12,8 @@ import { ProposalSignatureBundleNewAsset } from './proposal/signature-bundle-new
import { ProposalSignatureBundleUpdateAsset } from './proposal/signature-bundle-update'; import { ProposalSignatureBundleUpdateAsset } from './proposal/signature-bundle-update';
import { MarketLink } from '../../links'; import { MarketLink } from '../../links';
import { formatNumber } from '@vegaprotocol/utils'; import { formatNumber } from '@vegaprotocol/utils';
import { TransferDetails } from './transfer/transfer-details';
import { proposalToTransfer } from '../lib/proposal-to-transfer';
export type Proposal = components['schemas']['v1ProposalSubmission']; export type Proposal = components['schemas']['v1ProposalSubmission'];
export type ProposalTerms = components['schemas']['vegaProposalTerms']; export type ProposalTerms = components['schemas']['vegaProposalTerms'];
@ -104,6 +106,12 @@ export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => {
? ProposalSignatureBundleNewAsset ? ProposalSignatureBundleNewAsset
: ProposalSignatureBundleUpdateAsset; : ProposalSignatureBundleUpdateAsset;
let transfer, from;
if (proposal.terms?.newTransfer?.changes) {
transfer = proposalToTransfer(proposal.terms?.newTransfer.changes);
from = proposal.terms.newTransfer.changes.source;
}
return ( return (
<> <>
<TableWithTbody className="mb-8" allowWrap={true}> <TableWithTbody className="mb-8" allowWrap={true}>
@ -149,14 +157,27 @@ export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => {
</> </>
) : null} ) : null}
</TableWithTbody> </TableWithTbody>
<ProposalSummary <ProposalSummary
id={deterministicId} id={deterministicId}
rationale={proposal.rationale} rationale={proposal.rationale}
terms={proposal?.terms} terms={proposal?.terms}
/> />
{proposalRequiresSignatureBundle(proposal) && ( {proposalRequiresSignatureBundle(proposal) && (
<SignatureBundleComponent id={deterministicId} tx={tx} /> <SignatureBundleComponent id={deterministicId} tx={tx} />
)} )}
{transfer && (
<div className="mt-8">
<TransferDetails
statusOnly={false}
transfer={transfer}
from={from || ''}
id={deterministicId}
/>
</div>
)}
</> </>
); );
}; };

View File

@ -60,7 +60,7 @@ export const TxDetailsTransfer = ({
} }
const from = txData.submitter; const from = txData.submitter;
const id = txSignatureToDeterministicId(txData.signature.value);
return ( return (
<> <>
<TableWithTbody className="mb-8" allowWrap={true}> <TableWithTbody className="mb-8" allowWrap={true}>
@ -70,9 +70,7 @@ export const TxDetailsTransfer = ({
</TableRow> </TableRow>
<TableRow modifier="bordered" data-testid="id"> <TableRow modifier="bordered" data-testid="id">
<TableCell {...sharedHeaderProps}>{t('Transfer ID')}</TableCell> <TableCell {...sharedHeaderProps}>{t('Transfer ID')}</TableCell>
<TableCell> <TableCell>{id}</TableCell>
{txSignatureToDeterministicId(txData.signature.value)}
</TableCell>
</TableRow> </TableRow>
<TxDetailsShared <TxDetailsShared
txData={txData} txData={txData}
@ -105,7 +103,7 @@ export const TxDetailsTransfer = ({
</TableRow> </TableRow>
) : null} ) : null}
</TableWithTbody> </TableWithTbody>
<TransferDetails from={from} transfer={transfer} /> <TransferDetails from={from} transfer={transfer} id={id} />
</> </>
); );
}; };

View File

@ -0,0 +1,29 @@
import type { components } from '../../../../types/explorer';
type TransferProposal = components['schemas']['vegaNewTransferConfiguration'];
type ActualTransfer = components['schemas']['commandsv1Transfer'];
/**
* Converts a governance proposal for a transfer in to a transfer command that the
* TransferDetails component can then render. The types are very similar, but do not
* map precisely to each other due to some missing fields and some different field
* names.
*
* @param proposal Governance proposal for a transfer
* @returns transfer a Transfer object as if it had been submitted
*/
export function proposalToTransfer(proposal: TransferProposal): ActualTransfer {
return {
amount: proposal.amount,
asset: proposal.asset,
// On a transfer, 'from' is determined by the submitter, so there is no 'from' field
// fromAccountType does exist and is just named differently on the proposal
fromAccountType: proposal.sourceType,
oneOff: proposal.oneOff,
recurring: proposal.recurring,
// There is no reference applied on governance initiated transfers
reference: '',
to: proposal.destination,
toAccountType: proposal.destinationType,
};
}