From 1bfbce4d6e444dc081830c79fd4e111fee2dcad1 Mon Sep 17 00:00:00 2001 From: Dominik Guzei Date: Tue, 12 Nov 2024 14:31:08 +0100 Subject: [PATCH 01/30] feat: Add navigation with governance to voting page [LW-11519] --- source/renderer/app/Routes.tsx | 22 ++++++-- .../VotingPowerDelegation.scss | 16 ++++++ .../VotingPowerDelegation.tsx | 25 ++++++++++ .../app/config/votingNavigationConfig.ts | 4 ++ .../renderer/app/containers/voting/Voting.tsx | 50 +++++++++++++++++++ .../voting/VotingGovernancePage.tsx | 21 ++++++++ .../voting/VotingRegistrationPage.tsx | 5 +- source/renderer/app/routes-config.ts | 2 + 8 files changed, 137 insertions(+), 8 deletions(-) create mode 100644 source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.scss create mode 100644 source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.tsx create mode 100644 source/renderer/app/config/votingNavigationConfig.ts create mode 100644 source/renderer/app/containers/voting/Voting.tsx create mode 100644 source/renderer/app/containers/voting/VotingGovernancePage.tsx diff --git a/source/renderer/app/Routes.tsx b/source/renderer/app/Routes.tsx index 7f6129b635..30a8231e0e 100644 --- a/source/renderer/app/Routes.tsx +++ b/source/renderer/app/Routes.tsx @@ -36,6 +36,9 @@ import VotingRegistrationPage from './containers/voting/VotingRegistrationPage'; import { IS_STAKING_INFO_PAGE_AVAILABLE } from './config/stakingConfig'; import AnalyticsConsentPage from './containers/profile/AnalyticsConsentPage'; import TrackedRoute from './analytics/TrackedRoute'; +import { Voting } from './containers/voting/Voting'; +import VotingPowerDelegation from './components/voting/voting-governance/VotingPowerDelegation'; +import VotingGovernancePage from './containers/voting/VotingGovernancePage'; export const Routes = withRouter(() => ( @@ -205,11 +208,20 @@ export const Routes = withRouter(() => ( component={RedeemItnRewardsContainer} /> - + + + + + + diff --git a/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.scss b/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.scss new file mode 100644 index 0000000000..e69f6ee789 --- /dev/null +++ b/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.scss @@ -0,0 +1,16 @@ +@import '../votingConfig'; + +.component { + flex: 1 0 0; + padding: 20px; +} + +.heading { + @extend %accentText; + font-family: var(--font-semibold); + font-size: 18px; + letter-spacing: 2px; + margin-bottom: 14px; + text-align: center; + text-transform: uppercase; +} diff --git a/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.tsx b/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.tsx new file mode 100644 index 0000000000..eff5c8725d --- /dev/null +++ b/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { observer } from 'mobx-react'; +import { injectIntl } from 'react-intl'; +import BorderedBox from '../../widgets/BorderedBox'; +import { messages } from '../voting-info/Headline.messages'; +import styles from './VotingPowerDelegation.scss'; +import type { Intl } from '../../../types/i18nTypes'; + +type Props = { + intl: Intl; +}; + +function VotingPowerDelegation({ intl }: Props) { + return ( +
+ +

+ {intl.formatMessage(messages.heading)} +

+
+
+ ); +} + +export default observer(injectIntl(VotingPowerDelegation)); diff --git a/source/renderer/app/config/votingNavigationConfig.ts b/source/renderer/app/config/votingNavigationConfig.ts new file mode 100644 index 0000000000..b2e0b26f6e --- /dev/null +++ b/source/renderer/app/config/votingNavigationConfig.ts @@ -0,0 +1,4 @@ +export const VOTING_NAV_IDS = { + REGISTRATION: 'registration', + GOVERNANCE: 'governance', +}; diff --git a/source/renderer/app/containers/voting/Voting.tsx b/source/renderer/app/containers/voting/Voting.tsx new file mode 100644 index 0000000000..2a4c51473e --- /dev/null +++ b/source/renderer/app/containers/voting/Voting.tsx @@ -0,0 +1,50 @@ +import React, { Component } from 'react'; +import { inject, observer } from 'mobx-react'; +import Navigation, { type NavButtonProps } from '../../components/navigation/Navigation'; +import type { InjectedContainerProps } from '../../types/injectedPropsType'; +import MainLayout from '../MainLayout'; +import { ROUTES } from '../../routes-config'; + +type Props = InjectedContainerProps; + +@inject('stores', 'actions') +@observer +export class Voting extends Component { + + static defaultProps = { + actions: null, + stores: null, + }; + + render() { + const { app } = this.props.stores; + const navItems: Array = [ + { + id: ROUTES.VOTING.REGISTRATION, + label: 'Registration', + }, + { + id: ROUTES.VOTING.GOVERNANCE, + label: 'Governance', + }, + ]; + const activeItem = navItems.find((item) => app.currentRoute === item.id); + return ( + +
+ navItemId === activeItem.id} + onNavItemClick={(navItemId: string) => { + this.props.actions.router.goToRoute.trigger({ + route: navItemId + }); + }} + /> +
+ {this.props.children} +
+ ); + } +} diff --git a/source/renderer/app/containers/voting/VotingGovernancePage.tsx b/source/renderer/app/containers/voting/VotingGovernancePage.tsx new file mode 100644 index 0000000000..20ba3a6f38 --- /dev/null +++ b/source/renderer/app/containers/voting/VotingGovernancePage.tsx @@ -0,0 +1,21 @@ +import React, { Component } from 'react'; +import { inject, observer } from 'mobx-react'; +import type { InjectedProps } from '../../types/injectedPropsType'; +import VotingPowerDelegation from '../../components/voting/voting-governance/VotingPowerDelegation'; + +type Props = InjectedProps; + +@inject('stores', 'actions') +@observer +class VotingGovernancePage extends Component { + static defaultProps = { + actions: null, + stores: null, + }; + + render() { + return ; + } +} + +export default VotingGovernancePage; diff --git a/source/renderer/app/containers/voting/VotingRegistrationPage.tsx b/source/renderer/app/containers/voting/VotingRegistrationPage.tsx index 365dc7b390..61a16cd183 100644 --- a/source/renderer/app/containers/voting/VotingRegistrationPage.tsx +++ b/source/renderer/app/containers/voting/VotingRegistrationPage.tsx @@ -1,6 +1,5 @@ import React, { Component } from 'react'; import { observer, inject } from 'mobx-react'; -import Layout from '../MainLayout'; import { VOTING_REGISTRATION_MIN_WALLET_FUNDS } from '../../config/votingConfig'; import VerticalFlexContainer from '../../components/layout/VerticalFlexContainer'; import VotingInfo from '../../components/voting/voting-info/VotingInfo'; @@ -77,7 +76,7 @@ class VotingRegistrationPage extends Component { ); const innerContent = this.getInnerContent(isVotingRegistrationDialogOpen); return ( - + <> {innerContent} @@ -86,7 +85,7 @@ class VotingRegistrationPage extends Component { {isVotingRegistrationDialogOpen && ( )} - + ); } } diff --git a/source/renderer/app/routes-config.ts b/source/renderer/app/routes-config.ts index a25b0aa24f..26c7ea94d0 100644 --- a/source/renderer/app/routes-config.ts +++ b/source/renderer/app/routes-config.ts @@ -32,7 +32,9 @@ export const ROUTES = { UTXO: '/wallets/:id/utxo', }, VOTING: { + ROOT: '/voting', REGISTRATION: '/voting/registration', + GOVERNANCE: '/voting/governance', }, SETTINGS: { ROOT: '/settings', From 027b7a7b2d21765c875948aff88ce1926a29dd6e Mon Sep 17 00:00:00 2001 From: Dominik Guzei Date: Wed, 13 Nov 2024 13:13:03 +0100 Subject: [PATCH 02/30] fixup! feat: Add navigation with governance to voting page [LW-11519] --- source/renderer/app/containers/voting/Voting.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/renderer/app/containers/voting/Voting.tsx b/source/renderer/app/containers/voting/Voting.tsx index 2a4c51473e..485f679c3e 100644 --- a/source/renderer/app/containers/voting/Voting.tsx +++ b/source/renderer/app/containers/voting/Voting.tsx @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import { inject, observer } from 'mobx-react'; -import Navigation, { type NavButtonProps } from '../../components/navigation/Navigation'; +import Navigation from '../../components/navigation/Navigation'; +import type { NavButtonProps } from '../../components/navigation/Navigation'; import type { InjectedContainerProps } from '../../types/injectedPropsType'; import MainLayout from '../MainLayout'; import { ROUTES } from '../../routes-config'; @@ -10,7 +11,6 @@ type Props = InjectedContainerProps; @inject('stores', 'actions') @observer export class Voting extends Component { - static defaultProps = { actions: null, stores: null, @@ -38,7 +38,7 @@ export class Voting extends Component { isActiveNavItem={(navItemId: string) => navItemId === activeItem.id} onNavItemClick={(navItemId: string) => { this.props.actions.router.goToRoute.trigger({ - route: navItemId + route: navItemId, }); }} /> From 3a6dd29c072a50778329af3c895b525783571e87 Mon Sep 17 00:00:00 2001 From: Dominik Guzei Date: Wed, 13 Nov 2024 20:15:00 +0100 Subject: [PATCH 03/30] fixup! feat: Add navigation with governance to voting page [LW-11519] This adds the introduction text with help links to the new governance page. --- .../VotingPowerDelegation.messages.ts | 39 ++++++++++++++++++ .../VotingPowerDelegation.scss | 8 ++++ .../VotingPowerDelegation.tsx | 37 +++++++++++++++-- .../widgets/FormattedHTMLMessageWithLink.tsx | 8 ++-- .../voting/VotingGovernancePage.tsx | 3 +- .../app/i18n/locales/defaultMessages.json | 40 +++++++++++++++++++ source/renderer/app/i18n/locales/en-US.json | 9 ++++- source/renderer/app/i18n/locales/ja-JP.json | 7 ++++ translations/messages.json | 40 +++++++++++++++++++ 9 files changed, 181 insertions(+), 10 deletions(-) create mode 100644 source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.messages.ts diff --git a/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.messages.ts b/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.messages.ts new file mode 100644 index 0000000000..92a9c97950 --- /dev/null +++ b/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.messages.ts @@ -0,0 +1,39 @@ +import { defineMessages } from 'react-intl'; + +export const messages = defineMessages({ + heading: { + id: 'voting.governance.heading', + defaultMessage: '!!!CARDANO VOTING POWER DELEGATION', + description: 'Headline for Governance', + }, + learnMoreLinkLabel: { + id: 'voting.governance.learnMoreLinkLabel', + defaultMessage: '!!!Governance link label', + description: 'Link labels for governance page', + }, + paragraph1: { + id: 'voting.governance.paragraph1', + defaultMessage: '!!!Governance first paragraph', + description: 'First paragraph for governance page', + }, + paragraph1LinkUrl: { + id: 'voting.governance.paragraph1LinkUrl', + defaultMessage: '!!!Governance first paragraph link url', + description: 'First paragraph link for governance page', + }, + paragraph2: { + id: 'voting.governance.paragraph2', + defaultMessage: '!!!Governance second paragraph', + description: 'Second paragraph for governance page', + }, + paragraph3: { + id: 'voting.governance.paragraph3', + defaultMessage: '!!!Governance third paragraph', + description: 'Third paragraph for governance page', + }, + paragraph3LinkUrl: { + id: 'voting.governance.paragraph3LinkUrl', + defaultMessage: '!!!Governance third paragraph link url', + description: 'Link for third governance page paragraph', + }, +}); diff --git a/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.scss b/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.scss index e69f6ee789..ac864a7cbb 100644 --- a/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.scss +++ b/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.scss @@ -14,3 +14,11 @@ text-align: center; text-transform: uppercase; } + +.info { + @extend %regularText; + p { + display: block; + margin-bottom: 1em; + } +} diff --git a/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.tsx b/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.tsx index eff5c8725d..30ffb0e737 100644 --- a/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.tsx +++ b/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.tsx @@ -1,22 +1,53 @@ import React from 'react'; import { observer } from 'mobx-react'; -import { injectIntl } from 'react-intl'; +import { injectIntl, FormattedHTMLMessage } from 'react-intl'; import BorderedBox from '../../widgets/BorderedBox'; -import { messages } from '../voting-info/Headline.messages'; +import { messages } from './VotingPowerDelegation.messages'; import styles from './VotingPowerDelegation.scss'; import type { Intl } from '../../../types/i18nTypes'; +import { FormattedHTMLMessageWithLink } from '../../widgets/FormattedHTMLMessageWithLink'; type Props = { intl: Intl; + onExternalLinkClick: (...args: Array) => any; }; -function VotingPowerDelegation({ intl }: Props) { +function VotingPowerDelegation({ intl, onExternalLinkClick }: Props) { return (

{intl.formatMessage(messages.heading)}

+
+

+ +

+

+ +

+

+ +

+
); diff --git a/source/renderer/app/components/widgets/FormattedHTMLMessageWithLink.tsx b/source/renderer/app/components/widgets/FormattedHTMLMessageWithLink.tsx index 3a55745fba..8018f4fec4 100644 --- a/source/renderer/app/components/widgets/FormattedHTMLMessageWithLink.tsx +++ b/source/renderer/app/components/widgets/FormattedHTMLMessageWithLink.tsx @@ -23,9 +23,7 @@ export class FormattedHTMLMessageWithLink extends Component { const { message, onExternalLinkClick } = this.props; const { linkPosition, linkLabel, linkURL } = message.values; const MainMessage = ( - -  {intl.formatMessage(message)}  - + {intl.formatMessage(message)} ); const url = intl.formatMessage(linkURL); const Link = ( @@ -36,7 +34,7 @@ export class FormattedHTMLMessageWithLink extends Component { ); return linkPosition === 'before' - ? [Link, MainMessage] - : [MainMessage, Link]; + ? [Link, <> , MainMessage] + : [MainMessage, <> , Link]; } } diff --git a/source/renderer/app/containers/voting/VotingGovernancePage.tsx b/source/renderer/app/containers/voting/VotingGovernancePage.tsx index 20ba3a6f38..d23f482959 100644 --- a/source/renderer/app/containers/voting/VotingGovernancePage.tsx +++ b/source/renderer/app/containers/voting/VotingGovernancePage.tsx @@ -14,7 +14,8 @@ class VotingGovernancePage extends Component { }; render() { - return ; + const { openExternalLink } = this.props.stores.app; + return ; } } diff --git a/source/renderer/app/i18n/locales/defaultMessages.json b/source/renderer/app/i18n/locales/defaultMessages.json index 0307a1fb28..4b54f7c554 100644 --- a/source/renderer/app/i18n/locales/defaultMessages.json +++ b/source/renderer/app/i18n/locales/defaultMessages.json @@ -4384,6 +4384,46 @@ ], "path": "source/renderer/app/components/staking/widgets/TooltipPool.tsx" }, + { + "descriptors": [ + { + "defaultMessage": "!!!CARDANO VOTING POWER DELEGATION", + "description": "Headline for Governance", + "id": "voting.governance.heading" + }, + { + "defaultMessage": "!!!Governance link label", + "description": "Link labels for governance page", + "id": "voting.governance.learnMoreLinkLabel" + }, + { + "defaultMessage": "!!!Governance first paragraph", + "description": "First paragraph for governance page", + "id": "voting.governance.paragraph1" + }, + { + "defaultMessage": "!!!Governance first paragraph link url", + "description": "First paragraph link for governance page", + "id": "voting.governance.paragraph1LinkUrl" + }, + { + "defaultMessage": "!!!Governance second paragraph", + "description": "Second paragraph for governance page", + "id": "voting.governance.paragraph2" + }, + { + "defaultMessage": "!!!Governance third paragraph", + "description": "Third paragraph for governance page", + "id": "voting.governance.paragraph3" + }, + { + "defaultMessage": "!!!Governance third paragraph link url", + "description": "Link for third governance page paragraph", + "id": "voting.governance.paragraph3LinkUrl" + } + ], + "path": "source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.messages.ts" + }, { "descriptors": [ { diff --git a/source/renderer/app/i18n/locales/en-US.json b/source/renderer/app/i18n/locales/en-US.json index 0086307637..47186fb4b4 100755 --- a/source/renderer/app/i18n/locales/en-US.json +++ b/source/renderer/app/i18n/locales/en-US.json @@ -718,6 +718,13 @@ "voting.catalystFooterLinks.newsletter": "Newsletter", "voting.catalystFooterLinks.projects": "Projects", "voting.fundName": "Fund{votingFundNumber}", + "voting.governance.heading": "CARDANO VOTING POWER DELEGATION", + "voting.governance.learnMoreLinkLabel": "Learn more", + "voting.governance.paragraph1": "Cardano governance introduces an on-chain mechanism that empowers community-driven decisions, ensuring every ada holder has a voice.", + "voting.governance.paragraph1LinkUrl": "https://docs.intersectmbo.org/cardano/cardano-governance/overview", + "voting.governance.paragraph2": "After the governance bootstrapping period ends with the second Chang upgrade, each wallet will be required to delegate voting power to withdraw staking rewards.", + "voting.governance.paragraph3": "Select your wallet to delegate its voting power to a DRep or to one of the 'Automatic' voting options: 'Abstain' or ‘No Confidence’.", + "voting.governance.paragraph3LinkUrl": "https://docs.gov.tools/about/what-is-cardano-govtool/govtool-functions/delegating", "voting.info.androidAppButtonUrl": "https://play.google.com/store/apps/details?id=io.iohk.vitvoting", "voting.info.appleAppButtonUrl": "https://apps.apple.com/in/app/catalyst-voting/id1517473397", "voting.info.learnMoreLinkLabel": "Learn more", @@ -1346,4 +1353,4 @@ "wallet.transferFunds.dialog2.total.label": "Total", "widgets.itemsDropdown.syncingLabel": "Syncing", "widgets.itemsDropdown.syncingLabelProgress": "Syncing {syncingProgress}%" -} \ No newline at end of file +} diff --git a/source/renderer/app/i18n/locales/ja-JP.json b/source/renderer/app/i18n/locales/ja-JP.json index 49612f7a3f..21f659988f 100755 --- a/source/renderer/app/i18n/locales/ja-JP.json +++ b/source/renderer/app/i18n/locales/ja-JP.json @@ -718,6 +718,13 @@ "voting.catalystFooterLinks.newsletter": "ニュースレター", "voting.catalystFooterLinks.projects": "プロジェクト", "voting.fundName": "Fund{votingFundNumber}", + "voting.governance.heading": "!!!CARDANO VOTING POWER DELEGATION", + "voting.governance.learnMoreLinkLabel": "!!!Governance link label", + "voting.governance.paragraph1": "!!!Governance first paragraph", + "voting.governance.paragraph1LinkUrl": "!!!Governance first paragraph link url", + "voting.governance.paragraph2": "!!!Governance second paragraph", + "voting.governance.paragraph3": "!!!Governance third paragraph", + "voting.governance.paragraph3LinkUrl": "!!!Governance third paragraph link url", "voting.info.androidAppButtonUrl": "https://play.google.com/store/apps/details?id=io.iohk.vitvoting", "voting.info.appleAppButtonUrl": "https://apps.apple.com/in/app/catalyst-voting/id1517473397", "voting.info.learnMoreLinkLabel": "もっと知る", diff --git a/translations/messages.json b/translations/messages.json index 148e633f75..d20f98b1ae 100644 --- a/translations/messages.json +++ b/translations/messages.json @@ -4384,6 +4384,46 @@ ], "path": "source/renderer/app/components/staking/widgets/TooltipPool.tsx" }, + { + "descriptors": [ + { + "defaultMessage": "!!!CARDANO VOTING POWER DELEGATION", + "description": "Headline for Governance", + "id": "voting.governance.heading" + }, + { + "defaultMessage": "!!!Governance link label", + "description": "Link labels for governance page", + "id": "voting.governance.learnMoreLinkLabel" + }, + { + "defaultMessage": "!!!Governance first paragraph", + "description": "First paragraph for governance page", + "id": "voting.governance.paragraph1" + }, + { + "defaultMessage": "!!!Governance first paragraph link url", + "description": "First paragraph link for governance page", + "id": "voting.governance.paragraph1LinkUrl" + }, + { + "defaultMessage": "!!!Governance second paragraph", + "description": "Second paragraph for governance page", + "id": "voting.governance.paragraph2" + }, + { + "defaultMessage": "!!!Governance third paragraph", + "description": "Third paragraph for governance page", + "id": "voting.governance.paragraph3" + }, + { + "defaultMessage": "!!!Governance third paragraph link url", + "description": "Link for third governance page paragraph", + "id": "voting.governance.paragraph3LinkUrl" + } + ], + "path": "source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.messages.ts" + }, { "descriptors": [ { From 25592eab1ac3fa31e4c85a89025a841e5c1e76cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Mas=C5=82owski?= Date: Fri, 22 Nov 2024 12:24:00 +0100 Subject: [PATCH 04/30] feat: add delegate votes API call and integrate it with mobx voting store --- source/renderer/app/actions/voting-actions.ts | 2 ++ source/renderer/app/api/api.ts | 24 +++++++++++++++++++ .../app/api/voting/requests/delegateVotes.ts | 18 ++++++++++++++ source/renderer/app/api/voting/types.ts | 6 +++++ source/renderer/app/stores/VotingStore.ts | 10 +++++++- 5 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 source/renderer/app/api/voting/requests/delegateVotes.ts diff --git a/source/renderer/app/actions/voting-actions.ts b/source/renderer/app/actions/voting-actions.ts index 78aa694820..d92b3140a9 100644 --- a/source/renderer/app/actions/voting-actions.ts +++ b/source/renderer/app/actions/voting-actions.ts @@ -1,4 +1,5 @@ import Action from './lib/Action'; +import { DelegateVotesParams } from '../api/voting/types'; export default class VotingActions { selectWallet: Action = new Action(); @@ -6,6 +7,7 @@ export default class VotingActions { amount: number; passphrase: string | null | undefined; }> = new Action(); + delegateVotes: Action = new Action(); generateQrCode: Action = new Action(); saveAsPDF: Action = new Action(); saveAsPDFSuccess: Action = new Action(); diff --git a/source/renderer/app/api/api.ts b/source/renderer/app/api/api.ts index d88c4a64e5..299e9a5dee 100644 --- a/source/renderer/app/api/api.ts +++ b/source/renderer/app/api/api.ts @@ -40,6 +40,7 @@ import { getPublicKey } from './transactions/requests/getPublicKey'; import { getICOPublicKey } from './transactions/requests/getICOPublicKey'; // Voting requests import { createWalletSignature } from './voting/requests/createWalletSignature'; +import { delegateVotes } from './voting/requests/delegateVotes'; import { getCatalystFund } from './voting/requests/getCatalystFund'; // Wallets requests import { updateSpendingPassword } from './wallets/requests/updateSpendingPassword'; @@ -207,6 +208,7 @@ import type { CreateWalletSignatureRequest, GetCatalystFundResponse, CatalystFund, + DelegateVotesParams, } from './voting/types'; import type { StakePoolProps } from '../domains/StakePool'; import type { FaultInjectionIpcRequest } from '../../../common/types/cardano-node.types'; @@ -2747,6 +2749,28 @@ export default class AdaApi { throw new ApiError(error); } }; + + delegateVotes = async (params: DelegateVotesParams) => { + logger.debug('AdaApi::delegateVotes called', { + parameters: filterLogData(params), + }); + + try { + const response = await delegateVotes(this.config, params); + logger.debug('AdaApi::delegateVotes success', { + response, + }); + + return response; + } catch (error) { + logger.debug('AdaApi::delegateVotes error', { + error, + }); + + throw new ApiError(error); + } + }; + createVotingRegistrationTransaction = async ( request: CreateVotingRegistrationRequest ): Promise => { diff --git a/source/renderer/app/api/voting/requests/delegateVotes.ts b/source/renderer/app/api/voting/requests/delegateVotes.ts new file mode 100644 index 0000000000..cc5f6c63a0 --- /dev/null +++ b/source/renderer/app/api/voting/requests/delegateVotes.ts @@ -0,0 +1,18 @@ +import { request } from '../../utils/request'; +import { RequestConfig } from '../../common/types'; +import { Transaction } from '../../transactions/types'; +import { DelegateVotesParams } from '../types'; + +export const delegateVotes = ( + config: RequestConfig, + { dRepId, passphrase, walletId }: DelegateVotesParams +): Promise => + request( + { + ...config, + method: 'PUT', + path: `/v2/dreps/${dRepId}/wallets/${walletId}`, + }, + {}, + { passphrase } + ); diff --git a/source/renderer/app/api/voting/types.ts b/source/renderer/app/api/voting/types.ts index ab15db9d65..7a4dadf570 100644 --- a/source/renderer/app/api/voting/types.ts +++ b/source/renderer/app/api/voting/types.ts @@ -53,3 +53,9 @@ export type CatalystFund = { registrationSnapshotTime: Date; }; }; + +export type DelegateVotesParams = { + dRepId: string; + passphrase: string; + walletId: string; +}; diff --git a/source/renderer/app/stores/VotingStore.ts b/source/renderer/app/stores/VotingStore.ts index 02c6128eac..28b21b869f 100644 --- a/source/renderer/app/stores/VotingStore.ts +++ b/source/renderer/app/stores/VotingStore.ts @@ -24,7 +24,7 @@ import type { GetTransactionRequest, VotingMetadataType, } from '../api/transactions/types'; -import type { CatalystFund } from '../api/voting/types'; +import type { CatalystFund, DelegateVotesParams } from '../api/voting/types'; import { EventCategories } from '../analytics'; export type VotingRegistrationKeyType = { @@ -79,6 +79,7 @@ export default class VotingStore extends Store { const { voting: votingActions } = this.actions; votingActions.selectWallet.listen(this._setSelectedWalletId); votingActions.sendTransaction.listen(this._sendTransaction); + votingActions.delegateVotes.listen(this._delegateVotes); votingActions.generateQrCode.listen(this._generateQrCode); votingActions.saveAsPDF.listen(this._saveAsPDF); votingActions.nextRegistrationStep.listen(this._nextRegistrationStep); @@ -117,6 +118,8 @@ export default class VotingStore extends Store { this.api.ada.createWalletSignature ); @observable + delegateVotes: Request = new Request(this.api.ada.delegateVotes); + @observable getTransactionRequest: Request = new Request( this.api.ada.getTransaction ); @@ -297,6 +300,11 @@ export default class VotingStore extends Store { throw e; } }; + _delegateVotes = async (params: DelegateVotesParams) => { + this.delegateVotes.reset(); + // @ts-ignore ts-migrate(1320) FIXME: Type of 'await' operand must either be a valid pro... Remove this comment to see the full error message + const transaction = await this.delegateVotes.execute(params); + }; _sendTransaction = async ({ amount, passphrase, From a4e3f3b7ec0a3bd8adbbf526e11f8b7f3eeb0d00 Mon Sep 17 00:00:00 2001 From: Dominik Guzei Date: Thu, 14 Nov 2024 14:05:08 +0100 Subject: [PATCH 05/30] feat: Add governance voting form [LW-11519] --- .../VotingPowerDelegation.scss | 32 ++++++++ .../VotingPowerDelegation.tsx | 79 ++++++++++++++++++- .../widgets/FormattedHTMLMessageWithLink.tsx | 8 +- .../widgets/forms/ItemsDropdown.tsx | 4 + .../voting/VotingGovernancePage.tsx | 12 ++- source/renderer/app/i18n/locales/en-US.json | 2 +- 6 files changed, 129 insertions(+), 8 deletions(-) diff --git a/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.scss b/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.scss index ac864a7cbb..0b95930de8 100644 --- a/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.scss +++ b/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.scss @@ -22,3 +22,35 @@ margin-bottom: 1em; } } + +.walletSelect { + margin-top: 20px; + + &.error { + input { + border-color: var(--theme-color-error); + } + + :global { + .SimpleSelect_selectInput { + &:after { + background-color: var(--theme-color-error); + } + } + } + } + + :global { + .SimpleOptions_option { + align-items: center; + display: flex; + height: 50px; + padding-bottom: 0; + padding-top: 0; + } + } +} + +.voteTypeSelect, .drepInput { + margin-top: 20px; +} diff --git a/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.tsx b/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.tsx index 30ffb0e737..bb5e6d7c67 100644 --- a/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.tsx +++ b/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.tsx @@ -1,18 +1,50 @@ -import React from 'react'; +import React, { useState } from 'react'; import { observer } from 'mobx-react'; import { injectIntl, FormattedHTMLMessage } from 'react-intl'; +import { Input } from 'react-polymorph/lib/components/Input'; + import BorderedBox from '../../widgets/BorderedBox'; import { messages } from './VotingPowerDelegation.messages'; import styles from './VotingPowerDelegation.scss'; import type { Intl } from '../../../types/i18nTypes'; import { FormattedHTMLMessageWithLink } from '../../widgets/FormattedHTMLMessageWithLink'; +import WalletsDropdown from '../../widgets/forms/WalletsDropdown'; +import Wallet from '../../../domains/Wallet'; +import StakePool from '../../../domains/StakePool'; +import ItemsDropdown from '../../widgets/forms/ItemsDropdown'; type Props = { + getStakePoolById: (...args: Array) => any; intl: Intl; onExternalLinkClick: (...args: Array) => any; + stakePools: Array; + wallets: Array; }; -function VotingPowerDelegation({ intl, onExternalLinkClick }: Props) { +function VotingPowerDelegation({ + getStakePoolById, + intl, + onExternalLinkClick, + wallets, + stakePools, +}: Props) { + const [selectedWalletId, setSelectedWalletId] = useState(null); + const [selectedVoteType, setSelectedVoteType] = useState(null); + const [drepInputValue, setDrepInputValue] = useState(''); + const voteTypes = [ + { + value: 'abstain', + label: 'Abstain', + }, + { + value: 'noConfidence', + label: 'No confidence', + }, + { + value: 'drep', + label: 'Delegate to dRep', + }, + ]; return (
@@ -48,9 +80,50 @@ function VotingPowerDelegation({ intl, onExternalLinkClick }: Props) { />

+ + setSelectedWalletId(walletId)} + placeholder={'Select a wallet …'} + value={selectedWalletId} + getStakePoolById={getStakePoolById} + /> + + {selectedWalletId && ( + setSelectedVoteType(option.value)} + value={selectedVoteType} + /> + )} + + {selectedVoteType && ( + + Please type or paste a valid DRep ID here. Look up{' '} + onExternalLinkClick('https://google.com')}> + DRep directory + + + } + placeholder={'Paste DRep ID here.'} + /> + )} ); } -export default observer(injectIntl(VotingPowerDelegation)); +export default injectIntl(observer(VotingPowerDelegation)); diff --git a/source/renderer/app/components/widgets/FormattedHTMLMessageWithLink.tsx b/source/renderer/app/components/widgets/FormattedHTMLMessageWithLink.tsx index 8018f4fec4..688b12c6b7 100644 --- a/source/renderer/app/components/widgets/FormattedHTMLMessageWithLink.tsx +++ b/source/renderer/app/components/widgets/FormattedHTMLMessageWithLink.tsx @@ -34,7 +34,11 @@ export class FormattedHTMLMessageWithLink extends Component { ); return linkPosition === 'before' - ? [Link, <> , MainMessage] - : [MainMessage, <> , Link]; + ? [Link,  , MainMessage] + : [ + MainMessage, +  , + Link, + ]; } } diff --git a/source/renderer/app/components/widgets/forms/ItemsDropdown.tsx b/source/renderer/app/components/widgets/forms/ItemsDropdown.tsx index 7599f0d4a0..aacfe297af 100644 --- a/source/renderer/app/components/widgets/forms/ItemsDropdown.tsx +++ b/source/renderer/app/components/widgets/forms/ItemsDropdown.tsx @@ -29,8 +29,12 @@ import globalMessages from '../../../i18n/global-messages'; */ export type ItemDropdownProps = { options: Array; + label?: string; className?: string; disabled?: boolean; + handleChange?: (...args: Array) => any; + value?: string; + placeholder?: string; }; export const onSearchItemsDropdown = ( searchValue: string, diff --git a/source/renderer/app/containers/voting/VotingGovernancePage.tsx b/source/renderer/app/containers/voting/VotingGovernancePage.tsx index d23f482959..75dad6edb3 100644 --- a/source/renderer/app/containers/voting/VotingGovernancePage.tsx +++ b/source/renderer/app/containers/voting/VotingGovernancePage.tsx @@ -14,8 +14,16 @@ class VotingGovernancePage extends Component { }; render() { - const { openExternalLink } = this.props.stores.app; - return ; + const { wallets, staking, app } = this.props.stores; + const { openExternalLink } = app; + return ( + + ); } } diff --git a/source/renderer/app/i18n/locales/en-US.json b/source/renderer/app/i18n/locales/en-US.json index 47186fb4b4..681ddbb469 100755 --- a/source/renderer/app/i18n/locales/en-US.json +++ b/source/renderer/app/i18n/locales/en-US.json @@ -1353,4 +1353,4 @@ "wallet.transferFunds.dialog2.total.label": "Total", "widgets.itemsDropdown.syncingLabel": "Syncing", "widgets.itemsDropdown.syncingLabelProgress": "Syncing {syncingProgress}%" -} +} \ No newline at end of file From 79557aa3e92a55998b5d49bd10af66edef8eba72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20W=C5=82odek?= Date: Thu, 14 Nov 2024 18:50:01 +0100 Subject: [PATCH 06/30] fixup! feat: Add governance voting form [LW-11519] add governance voting form validation --- .../common/utils/assertIsBech32WithPrefix.ts | 34 +++++++ .../VotingPowerDelegation.messages.ts | 15 ---- .../VotingPowerDelegation.scss | 10 ++- .../VotingPowerDelegation.tsx | 89 ++++++++++++++----- .../app/i18n/locales/defaultMessages.json | 15 ---- source/renderer/app/i18n/locales/en-US.json | 9 +- source/renderer/app/i18n/locales/ja-JP.json | 3 - translations/messages.json | 15 ---- 8 files changed, 110 insertions(+), 80 deletions(-) create mode 100644 source/common/utils/assertIsBech32WithPrefix.ts diff --git a/source/common/utils/assertIsBech32WithPrefix.ts b/source/common/utils/assertIsBech32WithPrefix.ts new file mode 100644 index 0000000000..bafa43fd64 --- /dev/null +++ b/source/common/utils/assertIsBech32WithPrefix.ts @@ -0,0 +1,34 @@ +import { bech32, Decoded } from 'bech32'; + +const MAX_BECH32_LENGTH_LIMIT = 1023; + +const isOneOf = (target: T, options: T | T[]) => + (Array.isArray(options) && options.includes(target)) || target === options; + +export const assertIsBech32WithPrefix = ( + target: string, + prefix: string | string[], + expectedDecodedLength?: number | number[] +): void => { + let decoded: Decoded; + try { + decoded = bech32.decode(target, MAX_BECH32_LENGTH_LIMIT); + } catch (error) { + throw new Error( + `expected bech32-encoded string with '${prefix}' prefix; ${error}` + ); + } + if (!isOneOf(decoded.prefix, prefix)) { + throw new Error( + `expected bech32 prefix '${prefix}', got '${decoded.prefix}''` + ); + } + if ( + expectedDecodedLength && + !isOneOf(decoded.words.length, expectedDecodedLength) + ) { + throw new Error( + `expected decoded length of '${expectedDecodedLength}', got '${decoded.words.length}'` + ); + } +}; diff --git a/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.messages.ts b/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.messages.ts index 92a9c97950..aeb3a68b8a 100644 --- a/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.messages.ts +++ b/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.messages.ts @@ -21,19 +21,4 @@ export const messages = defineMessages({ defaultMessage: '!!!Governance first paragraph link url', description: 'First paragraph link for governance page', }, - paragraph2: { - id: 'voting.governance.paragraph2', - defaultMessage: '!!!Governance second paragraph', - description: 'Second paragraph for governance page', - }, - paragraph3: { - id: 'voting.governance.paragraph3', - defaultMessage: '!!!Governance third paragraph', - description: 'Third paragraph for governance page', - }, - paragraph3LinkUrl: { - id: 'voting.governance.paragraph3LinkUrl', - defaultMessage: '!!!Governance third paragraph link url', - description: 'Link for third governance page paragraph', - }, }); diff --git a/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.scss b/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.scss index 0b95930de8..2c4c05d331 100644 --- a/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.scss +++ b/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.scss @@ -51,6 +51,12 @@ } } -.voteTypeSelect, .drepInput { - margin-top: 20px; +.voteTypeSelect, +.drepInput, +.voteSubmit { + margin-top: 40px; +} + +.voteSubmit { + width: 100%; } diff --git a/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.tsx b/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.tsx index bb5e6d7c67..9033ac4d3d 100644 --- a/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.tsx +++ b/source/renderer/app/components/voting/voting-governance/VotingPowerDelegation.tsx @@ -1,7 +1,8 @@ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { observer } from 'mobx-react'; -import { injectIntl, FormattedHTMLMessage } from 'react-intl'; +import { injectIntl } from 'react-intl'; import { Input } from 'react-polymorph/lib/components/Input'; +import { Button } from 'react-polymorph/lib/components/Button'; import BorderedBox from '../../widgets/BorderedBox'; import { messages } from './VotingPowerDelegation.messages'; @@ -12,6 +13,7 @@ import WalletsDropdown from '../../widgets/forms/WalletsDropdown'; import Wallet from '../../../domains/Wallet'; import StakePool from '../../../domains/StakePool'; import ItemsDropdown from '../../widgets/forms/ItemsDropdown'; +import { assertIsBech32WithPrefix } from '../../../../../common/utils/assertIsBech32WithPrefix'; type Props = { getStakePoolById: (...args: Array) => any; @@ -21,6 +23,19 @@ type Props = { wallets: Array; }; +type VoteType = 'abstain' | 'noConfidence' | 'drep'; + +// TODO discuss if we need to restrict the length +const isDrepIdValid = (drepId: string) => { + try { + assertIsBech32WithPrefix(drepId, ['drep', 'drep_script']); + } catch (e) { + return false; + } + + return true; +}; + function VotingPowerDelegation({ getStakePoolById, intl, @@ -29,9 +44,21 @@ function VotingPowerDelegation({ stakePools, }: Props) { const [selectedWalletId, setSelectedWalletId] = useState(null); - const [selectedVoteType, setSelectedVoteType] = useState(null); - const [drepInputValue, setDrepInputValue] = useState(''); - const voteTypes = [ + const [selectedVoteType, setSelectedVoteType] = useState('drep'); + const [drepInputState, setDrepInputState] = useState({ + blurred: false, + value: '', + }); + const drepInputIsValid = useMemo( + () => (drepInputState.blurred ? isDrepIdValid(drepInputState.value) : true), + [drepInputState.blurred, drepInputState.value] + ); + const formIsValid = + !!selectedWalletId && + (selectedVoteType === 'drep' + ? drepInputState.blurred && drepInputState.value && drepInputIsValid + : true); + const voteTypes: { value: VoteType; label: string }[] = [ { value: 'abstain', label: 'Abstain', @@ -64,21 +91,6 @@ function VotingPowerDelegation({ onExternalLinkClick={onExternalLinkClick} />

-

- -

-

- -

)} - {selectedVoteType && ( + {selectedWalletId && selectedVoteType === 'drep' && ( { + setDrepInputState({ + blurred: false, + value, + }); + }} + onBlur={() => { + setDrepInputState((prevState) => ({ + ...prevState, + blurred: true, + })); + }} spellCheck={false} - value={drepInputValue} + value={drepInputState.value} label={ } placeholder={'Paste DRep ID here.'} + error={drepInputIsValid ? undefined : 'Invalid DRep ID'} /> )} +