diff --git a/src/modules/governance/StateBadge.tsx b/src/modules/governance/StateBadge.tsx index d8f3161370..3a596611d2 100644 --- a/src/modules/governance/StateBadge.tsx +++ b/src/modules/governance/StateBadge.tsx @@ -16,6 +16,7 @@ export enum ProposalBadgeState { Executed = 'Executed', Cancelled = 'Cancelled', Expired = 'Expired', + PartiallyExecuted = 'Partially executed', } type BadgeProps = { @@ -52,6 +53,7 @@ const Badge = styled('span')(({ theme, state }) => { [ProposalBadgeState.Executed]: theme.palette.success.main, [ProposalBadgeState.Cancelled]: theme.palette.error.main, [ProposalBadgeState.Expired]: theme.palette.error.main, + [ProposalBadgeState.PartiallyExecuted]: theme.palette.warning.main, [ProposalBadgeState.Failed]: theme.palette.error.main, }; const color = COLOR_MAP[state] || '#000'; @@ -91,6 +93,8 @@ export const stateToString = (stateToString: ProposalBadgeState) => { return 'Cancelled'; case ProposalBadgeState.Expired: return 'Expired'; + case ProposalBadgeState.PartiallyExecuted: + return 'Partially executed'; case ProposalBadgeState.Failed: return 'Failed'; } @@ -110,6 +114,8 @@ export const stringToState = (state: string) => { return ProposalBadgeState.Cancelled; case 'Expired': return ProposalBadgeState.Expired; + case 'Partially executed': + return ProposalBadgeState.PartiallyExecuted; case 'Failed': return ProposalBadgeState.Failed; } diff --git a/src/modules/governance/adapters.ts b/src/modules/governance/adapters.ts index cb3979eb9f..f2033f3c01 100644 --- a/src/modules/governance/adapters.ts +++ b/src/modules/governance/adapters.ts @@ -35,6 +35,10 @@ export function cacheStateToBadge(state: string): ProposalBadgeState { return ProposalBadgeState.Failed; case 'cancelled': return ProposalBadgeState.Cancelled; + case 'expired': + return ProposalBadgeState.Expired; + case 'partially_executed': + return ProposalBadgeState.PartiallyExecuted; default: return ProposalBadgeState.Created; } diff --git a/src/modules/governance/proposal/ProposalLifecycleCache.tsx b/src/modules/governance/proposal/ProposalLifecycleCache.tsx index 48a1f697cf..0758e75f83 100644 --- a/src/modules/governance/proposal/ProposalLifecycleCache.tsx +++ b/src/modules/governance/proposal/ProposalLifecycleCache.tsx @@ -158,7 +158,16 @@ export const ProposalLifecycleCache = ({ } const state = proposal.state; - const stateOrder = ['created', 'active', 'queued', 'executed', 'failed', 'cancelled']; + const stateOrder = [ + 'created', + 'active', + 'queued', + 'executed', + 'partially_executed', + 'expired', + 'failed', + 'cancelled', + ]; const currentStateIndex = stateOrder.indexOf(state); // Build payload creation substeps @@ -205,9 +214,12 @@ export const ProposalLifecycleCache = ({ }); } payloadExecutionSubsteps.push({ - stepName: `Payload ${p.payloadId} executed on ${getNetworkName(p.chainId)}`, + stepName: + p.state === 'expired' + ? `Payload ${p.payloadId} expired on ${getNetworkName(p.chainId)}` + : `Payload ${p.payloadId} executed on ${getNetworkName(p.chainId)}`, timestamp: p.executedAt, - completed: p.state === 'executed', + completed: p.state === 'executed' || p.state === 'expired', active: false, networkLogo: getNetworkLogo(p.chainId), }); @@ -263,13 +275,36 @@ export const ProposalLifecycleCache = ({ // Add final state step const allPayloadsExecuted = !!payloads?.length && payloads.every((p) => p.state === 'executed'); + // proposal.executedAt is the dispatch time; payloads finish later after their timelock, so the + // completed step should reflect the latest payload execution, matching the on-chain finish. + const payloadExecutedTimes = (payloads ?? []) + .map((p) => p.executedAt) + .filter((t): t is string => !!t) + .sort(); + const lastPayloadExecutedAt = payloadExecutedTimes[payloadExecutedTimes.length - 1] ?? null; - if (state === 'queued' || state === 'executed') { + if ( + state === 'queued' || + state === 'executed' || + state === 'partially_executed' || + state === 'expired' + ) { + const isTerminal = state === 'partially_executed' || state === 'expired'; + const finalStepName = + state === 'expired' + ? 'Expired' + : state === 'partially_executed' + ? 'Partially executed' + : allPayloadsExecuted + ? 'Payloads executed' + : 'Payload execution'; steps.push({ - stepName: allPayloadsExecuted ? 'Payloads executed' : 'Payload execution', - timestamp: allPayloadsExecuted ? proposal.executedAt : proposal.queuedAt, - completed: allPayloadsExecuted, - active: !allPayloadsExecuted, + stepName: finalStepName, + timestamp: allPayloadsExecuted + ? lastPayloadExecutedAt ?? proposal.executedAt + : proposal.queuedAt, + completed: isTerminal || allPayloadsExecuted, + active: !isTerminal && !allPayloadsExecuted, lastStep: true, substeps: payloadExecutionSubsteps, });