From 86c1bbea66aa5b18d571edb04c3a86edc371000e Mon Sep 17 00:00:00 2001 From: Gabe Rodriguez Date: Tue, 23 Jul 2024 23:15:34 +0200 Subject: [PATCH] Delegator voting action view (#1539) * Support delegator voting action view * Action view support --- .changeset/late-dancers-check.md | 7 + packages/getters/src/delegator-vote-view.ts | 6 + .../src/translators/action-view.ts | 9 ++ .../translators/delegator-vote-view.test.ts | 60 +++++++++ .../src/translators/delegator-vote-view.ts | 20 +++ packages/ui/components/ui/tx/action-view.tsx | 3 +- .../ui/tx/actions-views/delegator-vote.tsx | 127 ++++++++++++++++++ 7 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 .changeset/late-dancers-check.md create mode 100644 packages/getters/src/delegator-vote-view.ts create mode 100644 packages/perspective/src/translators/delegator-vote-view.test.ts create mode 100644 packages/perspective/src/translators/delegator-vote-view.ts create mode 100644 packages/ui/components/ui/tx/actions-views/delegator-vote.tsx diff --git a/.changeset/late-dancers-check.md b/.changeset/late-dancers-check.md new file mode 100644 index 0000000000..e32daa8952 --- /dev/null +++ b/.changeset/late-dancers-check.md @@ -0,0 +1,7 @@ +--- +'@penumbra-zone/perspective': minor +'@penumbra-zone/getters': minor +'@repo/ui': minor +--- + +Add support for delegate vote action views diff --git a/packages/getters/src/delegator-vote-view.ts b/packages/getters/src/delegator-vote-view.ts new file mode 100644 index 0000000000..865751e50b --- /dev/null +++ b/packages/getters/src/delegator-vote-view.ts @@ -0,0 +1,6 @@ +import { createGetter } from './utils/create-getter.js'; +import { DelegatorVoteView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/governance/v1/governance_pb.js'; + +export const getDelegatorVoteBody = createGetter( + (view?: DelegatorVoteView) => view?.delegatorVote.value?.delegatorVote?.body, +); diff --git a/packages/perspective/src/translators/action-view.ts b/packages/perspective/src/translators/action-view.ts index a5c14fee2c..e259b804ac 100644 --- a/packages/perspective/src/translators/action-view.ts +++ b/packages/perspective/src/translators/action-view.ts @@ -5,6 +5,7 @@ import { asOpaqueOutputView, asReceiverOutputView } from './output-view.js'; import { Address } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb.js'; import { asOpaqueSwapView } from './swap-view.js'; import { asOpaqueSwapClaimView } from './swap-claim-view.js'; +import { asOpaqueDelegatorVoteView } from './delegator-vote-view.js'; export const asPublicActionView: Translator = actionView => { switch (actionView?.actionView.case) { @@ -40,6 +41,14 @@ export const asPublicActionView: Translator = actionView => { }, }); + case 'delegatorVote': + return new ActionView({ + actionView: { + case: 'delegatorVote', + value: asOpaqueDelegatorVoteView(actionView.actionView.value), + }, + }); + // Currently defaulting to displaying that all data is public as it's better // to err on communicating private data as public than the other way around // TODO: Do proper audit of what data for each action is public diff --git a/packages/perspective/src/translators/delegator-vote-view.test.ts b/packages/perspective/src/translators/delegator-vote-view.test.ts new file mode 100644 index 0000000000..13db661612 --- /dev/null +++ b/packages/perspective/src/translators/delegator-vote-view.test.ts @@ -0,0 +1,60 @@ +import { asOpaqueDelegatorVoteView } from './delegator-vote-view.js'; +import { describe, expect, test } from 'vitest'; +import { + DelegatorVote, + DelegatorVoteView, + DelegatorVoteView_Opaque, + DelegatorVoteView_Visible, +} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/governance/v1/governance_pb.js'; +import { NoteView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/shielded_pool/v1/shielded_pool_pb.js'; +import { ValueView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb.js'; + +describe('asOpaqueDelegatorVoteView', () => { + test('when passed `undefined` returns an empty, opaque delegator vote view', () => { + const expected = new DelegatorVoteView({ + delegatorVote: { + case: 'opaque', + value: new DelegatorVoteView_Opaque({ + delegatorVote: undefined, + }), + }, + }); + + expect(asOpaqueDelegatorVoteView(undefined)).toEqual(expected); + }); + + test('when passed an already-opaque delegator vote view returns the delegator vote view as-is', () => { + const opaqueDelegatorVoteView = new DelegatorVoteView({ + delegatorVote: { + case: 'opaque', + value: new DelegatorVoteView_Opaque({ + delegatorVote: new DelegatorVote({ body: { proposal: 123n } }), + }), + }, + }); + expect( + asOpaqueDelegatorVoteView(opaqueDelegatorVoteView).equals(opaqueDelegatorVoteView), + ).toBeTruthy(); + }); + + test('returns an opaque version of the delegator vote view', () => { + const visibleDelegatorVoteView = new DelegatorVoteView({ + delegatorVote: { + case: 'visible', + value: new DelegatorVoteView_Visible({ + delegatorVote: new DelegatorVote({ body: { proposal: 123n } }), + note: new NoteView({ value: new ValueView() }), + }), + }, + }); + + const result = asOpaqueDelegatorVoteView(visibleDelegatorVoteView); + + expect(result.delegatorVote.case).toBe('opaque'); + expect( + result.delegatorVote.value?.delegatorVote?.equals( + visibleDelegatorVoteView.delegatorVote.value?.delegatorVote, + ), + ).toBeTruthy(); + }); +}); diff --git a/packages/perspective/src/translators/delegator-vote-view.ts b/packages/perspective/src/translators/delegator-vote-view.ts new file mode 100644 index 0000000000..d676f62b38 --- /dev/null +++ b/packages/perspective/src/translators/delegator-vote-view.ts @@ -0,0 +1,20 @@ +import { Translator } from './types.js'; +import { + DelegatorVoteView, + DelegatorVoteView_Opaque, +} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/governance/v1/governance_pb.js'; + +export const asOpaqueDelegatorVoteView: Translator = delegatorVoteView => { + if (delegatorVoteView?.delegatorVote.case === 'opaque') { + return delegatorVoteView; + } + + return new DelegatorVoteView({ + delegatorVote: { + case: 'opaque', + value: new DelegatorVoteView_Opaque({ + delegatorVote: delegatorVoteView?.delegatorVote.value?.delegatorVote, + }), + }, + }); +}; diff --git a/packages/ui/components/ui/tx/action-view.tsx b/packages/ui/components/ui/tx/action-view.tsx index f36f9c69e6..daaad2d22d 100644 --- a/packages/ui/components/ui/tx/action-view.tsx +++ b/packages/ui/components/ui/tx/action-view.tsx @@ -12,6 +12,7 @@ import { ActionDutchAuctionScheduleViewComponent } from './actions-views/action- import { ActionDutchAuctionEndComponent } from './actions-views/action-dutch-auction-end'; import { ActionDutchAuctionWithdrawViewComponent } from './actions-views/action-dutch-auction-withdraw-view'; import { ValueView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb.js'; +import { DelegatorVoteComponent } from './actions-views/delegator-vote.tsx'; const CASE_TO_LABEL: Record = { daoDeposit: 'DAO Deposit', @@ -106,7 +107,7 @@ export const ActionViewComponent = ({ return ; case 'delegatorVote': - return ; + return ; case 'proposalDepositClaim': return ; diff --git a/packages/ui/components/ui/tx/actions-views/delegator-vote.tsx b/packages/ui/components/ui/tx/actions-views/delegator-vote.tsx new file mode 100644 index 0000000000..60d02e80a2 --- /dev/null +++ b/packages/ui/components/ui/tx/actions-views/delegator-vote.tsx @@ -0,0 +1,127 @@ +import { ViewBox } from '../viewbox'; +import { ValueViewComponent } from '../../value'; +import { getAddress } from '@penumbra-zone/getters/note-view'; +import { ActionDetails } from './action-details'; +import { + DelegatorVoteBody, + DelegatorVoteView, + Vote, + Vote_Vote, +} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/governance/v1/governance_pb'; +import { getDelegatorVoteBody } from '@penumbra-zone/getters/delegator-vote-view'; +import { Amount } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/num/v1/num_pb.js'; +import { + Metadata, + ValueView, +} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb.js'; +import { AddressViewComponent } from '../../address-view'; +import { base64ToUint8Array } from '@penumbra-zone/types/base64'; + +// TODO: This is sad, but at the moment, we aren't provided the metadata to have a rich display. +// Given the high-priority of getting action view support, this is added. +// We should properly implement ValueViews into the protos for DelegatorVote action view and delete this code. +const umMetadata = new Metadata({ + denomUnits: [ + { + denom: 'penumbra', + exponent: 6, + }, + { + denom: 'upenumbra', + exponent: 0, + }, + ], + base: 'upenumbra', + display: 'penumbra', + symbol: 'UM', + penumbraAssetId: { + inner: base64ToUint8Array('KeqcLzNx9qSH5+lcJHBB9KNW+YPrBk5dKzvPMiypahA='), + }, + images: [ + { + svg: 'https://raw.githubusercontent.com/prax-wallet/registry/main/images/um.svg', + }, + ], +}); + +const umValueView = (amount?: Amount) => { + return new ValueView({ + valueView: { + case: 'knownAssetId', + value: { + amount, + metadata: umMetadata, + }, + }, + }); +}; + +export const DelegatorVoteComponent = ({ value }: { value: DelegatorVoteView }) => { + const body = getDelegatorVoteBody.optional()(value); + + if (value.delegatorVote.case === 'visible') { + const note = value.delegatorVote.value.note; + const address = getAddress.optional()(note); + + return ( + + + + + + + } + /> + ); + } + + if (value.delegatorVote.case === 'opaque') { + return ( + + + + } + /> + ); + } + + return
Invalid DelegatorVoteView
; +}; + +const VoteToString = (vote: Vote): string => { + switch (vote.vote) { + case Vote_Vote.UNSPECIFIED: + return 'UNSPECIFIED'; + case Vote_Vote.ABSTAIN: + return 'ABSTAIN'; + case Vote_Vote.YES: + return 'YES'; + case Vote_Vote.NO: + return 'NO'; + } +}; + +const VoteBodyDetails = ({ body }: { body?: DelegatorVoteBody }) => { + return ( + <> + {!!body?.proposal && ( + {Number(body.proposal)} + )} + + {!!body?.vote && ( + {VoteToString(body.vote)} + )} + {!!body?.unbondedAmount && ( + + + + )} + + ); +};