diff --git a/apps/explorer/src/app/components/txs/details/proposal/summary.tsx b/apps/explorer/src/app/components/txs/details/proposal/summary.tsx index 0972e3d07..d694e827f 100644 --- a/apps/explorer/src/app/components/txs/details/proposal/summary.tsx +++ b/apps/explorer/src/app/components/txs/details/proposal/summary.tsx @@ -64,7 +64,9 @@ export const ProposalSummary = ({ return (
{id && } - {rationale?.title &&

{rationale.title}

} + {rationale?.title && ( +

{rationale.title}

+ )} {rationale?.description && (
; + + +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) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(ExplorerTransferVoteDocument, options); + } +export function useExplorerTransferVoteLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(ExplorerTransferVoteDocument, options); + } +export type ExplorerTransferVoteQueryHookResult = ReturnType; +export type ExplorerTransferVoteLazyQueryHookResult = ReturnType; +export type ExplorerTransferVoteQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/apps/explorer/src/app/components/txs/details/transfer/blocks/transfer-participants.tsx b/apps/explorer/src/app/components/txs/details/transfer/blocks/transfer-participants.tsx index e84e79f57..843e46a05 100644 --- a/apps/explorer/src/app/components/txs/details/transfer/blocks/transfer-participants.tsx +++ b/apps/explorer/src/app/components/txs/details/transfer/blocks/transfer-participants.tsx @@ -111,7 +111,7 @@ export function TransferParticipants({ @@ -120,7 +120,7 @@ export function TransferParticipants({ diff --git a/apps/explorer/src/app/components/txs/details/transfer/blocks/transfer-rewards.tsx b/apps/explorer/src/app/components/txs/details/transfer/blocks/transfer-rewards.tsx index a9fffc30a..39116a94c 100644 --- a/apps/explorer/src/app/components/txs/details/transfer/blocks/transfer-rewards.tsx +++ b/apps/explorer/src/app/components/txs/details/transfer/blocks/transfer-rewards.tsx @@ -4,6 +4,7 @@ import { headerClasses, wrapperClasses } from '../transfer-details'; import type { components } from '../../../../../../types/explorer'; import type { Recurring } from '../transfer-details'; import { DispatchMetricLabels } from '@vegaprotocol/types'; +import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit'; export type Metric = components['schemas']['vegaDispatchMetric']; export type Strategy = components['schemas']['vegaDispatchStrategy']; @@ -13,6 +14,15 @@ const metricLabels: Record = { ...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 { recurring: Recurring; } @@ -34,7 +44,7 @@ export function TransferRewards({ recurring }: TransferRewardsProps) { return (

{t('Reward metrics')}

-
    +
      {recurring.dispatchStrategy.assetForMetric ? (
    • {t('Asset')}:{' '} @@ -44,6 +54,27 @@ export function TransferRewards({ recurring }: TransferRewardsProps) {
    • {t('Metric')}: {metricLabels[metric]}
    • + {recurring.dispatchStrategy.entityScope && + entityScopeIcons[recurring.dispatchStrategy.entityScope] ? ( +
    • + {t('Scope')}:{' '} + +
    • + ) : null} + + {recurring.dispatchStrategy.individualScope} + {recurring.dispatchStrategy.teamScope} + + {recurring.dispatchStrategy.lockPeriod && + recurring.dispatchStrategy.lockPeriod !== '0' ? ( +
    • + {t('Lock')}:{' '} + {recurring.dispatchStrategy.lockPeriod} +
    • + ) : null} + {recurring.dispatchStrategy.markets && recurring.dispatchStrategy.markets.length > 0 ? (
    • @@ -57,9 +88,30 @@ export function TransferRewards({ recurring }: TransferRewardsProps) {
    ) : null} -
  • - {t('Factor')}: {recurring.factor} -
  • + + {recurring.dispatchStrategy.stakingRequirement && + recurring.dispatchStrategy.stakingRequirement !== '0' ? ( +
  • + {t('Staking requirement')}:{' '} + {recurring.dispatchStrategy.stakingRequirement} +
  • + ) : null} + + {recurring.dispatchStrategy.windowLength && + recurring.dispatchStrategy.windowLength !== '0' ? ( +
  • + {t('Window length')}:{' '} + {recurring.dispatchStrategy.windowLength} +
  • + ) : null} + + {recurring.dispatchStrategy.rankTable && + recurring.dispatchStrategy.rankTable.length > 0 ? ( +
  • + {t('Ranks')}:{' '} + {recurring.dispatchStrategy.rankTable.toString()} +
  • + ) : null}
); diff --git a/apps/explorer/src/app/components/txs/details/transfer/blocks/transfer-status.tsx b/apps/explorer/src/app/components/txs/details/transfer/blocks/transfer-status.tsx new file mode 100644 index 000000000..b0052aa95 --- /dev/null +++ b/apps/explorer/src/app/components/txs/details/transfer/blocks/transfer-status.tsx @@ -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 ( +
+

{t('Status')}

+
+ {loading ? ( + + ) : ( + <> +

+ +

+

{status}

+ + )} +
+
+ ); +} diff --git a/apps/explorer/src/app/components/txs/details/transfer/transfer-details.tsx b/apps/explorer/src/app/components/txs/details/transfer/transfer-details.tsx index de807be5b..ff303e335 100644 --- a/apps/explorer/src/app/components/txs/details/transfer/transfer-details.tsx +++ b/apps/explorer/src/app/components/txs/details/transfer/transfer-details.tsx @@ -2,6 +2,9 @@ import type { components } from '../../../../../types/explorer'; import { TransferRepeat } from './blocks/transfer-repeat'; import { TransferRewards } from './blocks/transfer-rewards'; import { TransferParticipants } from './blocks/transfer-participants'; +import { useExplorerTransferVoteQuery } from './__generated__/Transfer'; +import { TransferStatusView } from './blocks/transfer-status'; +import { TransferStatus } from '@vegaprotocol/types'; export type Recurring = components['schemas']['commandsv1RecurringTransfer']; export type Metric = components['schemas']['vegaDispatchMetric']; @@ -16,6 +19,9 @@ export type Transfer = components['schemas']['commandsv1Transfer']; interface TransferDetailsProps { transfer: Transfer; 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 */ -export function TransferDetails({ transfer, from }: TransferDetailsProps) { +export function TransferDetails({ + transfer, + from, + id, + statusOnly = false, +}: TransferDetailsProps) { const recurring = transfer.recurring; + // Currently all this is passed in to TransferStatus, but the extra details + // may be useful in the future. + const { data, error, loading } = useExplorerTransferVoteQuery({ + variables: { id }, + }); + + const status = error + ? TransferStatus.STATUS_REJECTED + : data?.transfer?.status; + return (
- - {recurring ? : null} - {recurring && recurring.dispatchStrategy ? ( - + {statusOnly ? null : ( + <> + + {recurring ? : null} + {recurring && recurring.dispatchStrategy ? ( + + ) : null} + + )} + {status ? ( + ) : null}
); diff --git a/apps/explorer/src/app/components/txs/details/tx-proposal.tsx b/apps/explorer/src/app/components/txs/details/tx-proposal.tsx index 12a89ab24..cfeb2c665 100644 --- a/apps/explorer/src/app/components/txs/details/tx-proposal.tsx +++ b/apps/explorer/src/app/components/txs/details/tx-proposal.tsx @@ -12,6 +12,8 @@ import { ProposalSignatureBundleNewAsset } from './proposal/signature-bundle-new import { ProposalSignatureBundleUpdateAsset } from './proposal/signature-bundle-update'; import { MarketLink } from '../../links'; import { formatNumber } from '@vegaprotocol/utils'; +import { TransferDetails } from './transfer/transfer-details'; +import { proposalToTransfer } from '../lib/proposal-to-transfer'; export type Proposal = components['schemas']['v1ProposalSubmission']; export type ProposalTerms = components['schemas']['vegaProposalTerms']; @@ -104,6 +106,12 @@ export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => { ? ProposalSignatureBundleNewAsset : ProposalSignatureBundleUpdateAsset; + let transfer, from; + if (proposal.terms?.newTransfer?.changes) { + transfer = proposalToTransfer(proposal.terms?.newTransfer.changes); + from = proposal.terms.newTransfer.changes.source; + } + return ( <> @@ -149,14 +157,27 @@ export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => { ) : null} + + {proposalRequiresSignatureBundle(proposal) && ( )} + + {transfer && ( +
+ +
+ )} ); }; diff --git a/apps/explorer/src/app/components/txs/details/tx-transfer.tsx b/apps/explorer/src/app/components/txs/details/tx-transfer.tsx index feb3ecf0a..3b9fed1e1 100644 --- a/apps/explorer/src/app/components/txs/details/tx-transfer.tsx +++ b/apps/explorer/src/app/components/txs/details/tx-transfer.tsx @@ -60,7 +60,7 @@ export const TxDetailsTransfer = ({ } const from = txData.submitter; - + const id = txSignatureToDeterministicId(txData.signature.value); return ( <> @@ -70,9 +70,7 @@ export const TxDetailsTransfer = ({ {t('Transfer ID')} - - {txSignatureToDeterministicId(txData.signature.value)} - + {id} ) : null} - + ); }; diff --git a/apps/explorer/src/app/components/txs/lib/proposal-to-transfer.ts b/apps/explorer/src/app/components/txs/lib/proposal-to-transfer.ts new file mode 100644 index 000000000..d24f4bb08 --- /dev/null +++ b/apps/explorer/src/app/components/txs/lib/proposal-to-transfer.ts @@ -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, + }; +}