diff --git a/packages/commonwealth/client/scripts/views/components/ProposalCard/ProposalCard.tsx b/packages/commonwealth/client/scripts/views/components/ProposalCard/ProposalCard.tsx index d1754079c63..934a8ed42f1 100644 --- a/packages/commonwealth/client/scripts/views/components/ProposalCard/ProposalCard.tsx +++ b/packages/commonwealth/client/scripts/views/components/ProposalCard/ProposalCard.tsx @@ -2,22 +2,21 @@ import React, { useEffect, useState } from 'react'; import 'components/ProposalCard/ProposalCard.scss'; import AaveProposal from 'controllers/chain/ethereum/aave/proposal'; -import { isNotNil } from 'helpers/typeGuards'; import { getProposalUrlPath } from 'identifiers'; import type { AnyProposal } from '../../../models/types'; +import { useCommonNavigate } from 'navigation/helpers'; import app from 'state'; +import { + useCosmosProposalMetadataQuery, + useCosmosProposalTallyQuery, +} from 'state/api/proposals'; import { slugify } from 'utils'; import { CWCard } from '../component_kit/cw_card'; import { CWDivider } from '../component_kit/cw_divider'; import { CWText } from '../component_kit/cw_text'; -import { getPrimaryTagText, getStatusClass, getStatusText } from './helpers'; import { ProposalTag } from './ProposalTag'; -import { useCommonNavigate } from 'navigation/helpers'; -import { - useCosmosProposalTallyQuery, - useCosmosProposalMetadataQuery, -} from 'state/api/proposals'; +import { getPrimaryTagText, getStatusClass, getStatusText } from './helpers'; type ProposalCardProps = { injectedContent?: React.ReactNode; @@ -30,7 +29,7 @@ export const ProposalCard = ({ }: ProposalCardProps) => { const navigate = useCommonNavigate(); const [title, setTitle] = useState( - proposal.title || `Proposal ${proposal.identifier}` + proposal.title || `Proposal ${proposal.identifier}`, ); const { data: metadata } = useCosmosProposalMetadataQuery(proposal); const { isFetching: isFetchingTally } = useCosmosProposalTallyQuery(proposal); @@ -48,15 +47,6 @@ export const ProposalCard = ({ } }, [proposal]); - useEffect(() => { - if (proposal instanceof AaveProposal) { - proposal.ipfsDataReady.once('ready', () => { - // triggers render of shortDescription too - setTitle(proposal?.ipfsData.title); - }); - } - }, [proposal]); - return ( {getStatusText(proposal, isFetchingTally)} diff --git a/packages/commonwealth/client/scripts/views/components/proposals/voting_actions.tsx b/packages/commonwealth/client/scripts/views/components/proposals/voting_actions.tsx index 57a23bdae3b..98040231e2b 100644 --- a/packages/commonwealth/client/scripts/views/components/proposals/voting_actions.tsx +++ b/packages/commonwealth/client/scripts/views/components/proposals/voting_actions.tsx @@ -1,5 +1,3 @@ -import React, { useEffect, useState } from 'react'; - import 'components/proposals/voting_actions.scss'; import { notifyError } from 'controllers/app/notifications'; import type CosmosAccount from 'controllers/chain/cosmos/account'; @@ -24,8 +22,11 @@ import { NearSputnikVote, NearSputnikVoteString, } from 'controllers/chain/near/sputnik/types'; +import React, { useEffect, useState } from 'react'; import type { AnyProposal } from '../../../models/types'; import { VotingType } from '../../../models/types'; +import { MixpanelGovernanceEvents } from '/analytics/types'; +import { useBrowserAnalyticsTrack } from '/hooks/useBrowserAnalyticsTrack'; import app from 'state'; @@ -51,6 +52,8 @@ export const VotingActions = (props: VotingActionsProps) => { const [isLoggedIn, setIsLoggedIn] = useState(app.isLoggedIn()); const [, setConviction] = useState(); + const { trackAnalytics } = useBrowserAnalyticsTrack({ onAction: true }); + useEffect(() => { app.loginStateEmitter.once('redraw', () => { setIsLoggedIn(app.isLoggedIn()); @@ -103,7 +106,7 @@ export const VotingActions = (props: VotingActionsProps) => { const chain = app.chain as Cosmos; const depositAmountInMinimalDenom = parseInt( naturalDenomToMinimal(amount, chain.meta?.decimals), - 10 + 10, ); proposal @@ -111,35 +114,57 @@ export const VotingActions = (props: VotingActionsProps) => { .then(emitRedraw) .catch((err) => notifyError(err.toString())); } else { - proposal - .voteTx(new CosmosVote(user, 'Yes')) - .then(emitRedraw) - .catch((err) => notifyError(err.toString())); + try { + await proposal.voteTx(new CosmosVote(user, 'Yes')); + emitRedraw(); + trackAnalytics({ + event: MixpanelGovernanceEvents.COSMOS_VOTE_OCCURRED, + }); + } catch (error) { + notifyError(error.toString()); + } } } else if (proposal instanceof CompoundProposal) { - proposal - .submitVoteWebTx(new CompoundProposalVote(user, BravoVote.YES)) - .then(emitRedraw) - .catch((err) => notifyError(err.toString())); + try { + await proposal.submitVoteWebTx( + new CompoundProposalVote(user, BravoVote.YES), + ); + emitRedraw(); + trackAnalytics({ + event: MixpanelGovernanceEvents.COMPOUND_VOTE_OCCURRED, + }); + } catch (err) { + notifyError(err.toString()); + } } else if (proposal instanceof AaveProposal) { - proposal - .submitVoteWebTx(new AaveProposalVote(user, true)) - .then(emitRedraw) - .catch((err) => notifyError(err.toString())); + try { + await proposal.submitVoteWebTx(new AaveProposalVote(user, true)); + emitRedraw(); + trackAnalytics({ + event: MixpanelGovernanceEvents.AAVE_VOTE_OCCURRED, + }); + } catch (err) { + notifyError(err.toString()); + } } else if (proposal instanceof NearSputnikProposal) { - proposal - .submitVoteWebTx( - new NearSputnikVote(user, NearSputnikVoteString.Approve) - ) - .then(emitRedraw) - .catch((err) => notifyError(err.toString())); + try { + await proposal.submitVoteWebTx( + new NearSputnikVote(user, NearSputnikVoteString.Approve), + ); + emitRedraw(); + trackAnalytics({ + event: MixpanelGovernanceEvents.SPUTNIK_VOTE_OCCURRED, + }); + } catch (err) { + notifyError(err.toString()); + } } else { toggleVotingModal(false); return notifyError('Invalid proposal type'); } }; - const voteNo = (e) => { + const voteNo = async (e) => { e.preventDefault(); toggleVotingModal(true); @@ -147,27 +172,49 @@ export const VotingActions = (props: VotingActionsProps) => { proposal instanceof CosmosProposal || proposal instanceof CosmosProposalV1 ) { - proposal - .voteTx(new CosmosVote(user, 'No')) - .then(emitRedraw) - .catch((err) => notifyError(err.toString())); + try { + await proposal.voteTx(new CosmosVote(user, 'No')); + emitRedraw(); + trackAnalytics({ + event: MixpanelGovernanceEvents.COSMOS_VOTE_OCCURRED, + }); + } catch (err) { + notifyError(err.toString()); + } } else if (proposal instanceof CompoundProposal) { - proposal - .submitVoteWebTx(new CompoundProposalVote(user, BravoVote.NO)) - .then(emitRedraw) - .catch((err) => notifyError(err.toString())); + try { + await proposal.submitVoteWebTx( + new CompoundProposalVote(user, BravoVote.NO), + ); + emitRedraw(); + trackAnalytics({ + event: MixpanelGovernanceEvents.COMPOUND_VOTE_OCCURRED, + }); + } catch (err) { + notifyError(err.toString()); + } } else if (proposal instanceof AaveProposal) { - proposal - .submitVoteWebTx(new AaveProposalVote(user, false)) - .then(emitRedraw) - .catch((err) => notifyError(err.toString())); + try { + await proposal.submitVoteWebTx(new AaveProposalVote(user, false)); + emitRedraw(); + trackAnalytics({ + event: MixpanelGovernanceEvents.AAVE_VOTE_OCCURRED, + }); + } catch (err) { + notifyError(err.toString()); + } } else if (proposal instanceof NearSputnikProposal) { - proposal - .submitVoteWebTx( - new NearSputnikVote(user, NearSputnikVoteString.Reject) - ) - .then(emitRedraw) - .catch((err) => notifyError(err.toString())); + try { + await proposal.submitVoteWebTx( + new NearSputnikVote(user, NearSputnikVoteString.Reject), + ); + emitRedraw(); + trackAnalytics({ + event: MixpanelGovernanceEvents.SPUTNIK_VOTE_OCCURRED, + }); + } catch (err) { + notifyError(err.toString()); + } } else { toggleVotingModal(false); return notifyError('Invalid proposal type'); @@ -225,7 +272,7 @@ export const VotingActions = (props: VotingActionsProps) => { if (proposal instanceof NearSputnikProposal) { proposal .submitVoteWebTx( - new NearSputnikVote(user, NearSputnikVoteString.Remove) + new NearSputnikVote(user, NearSputnikVoteString.Remove), ) .then(() => { onModalClose(); diff --git a/packages/commonwealth/client/scripts/views/modals/confirm_snapshot_vote_modal.tsx b/packages/commonwealth/client/scripts/views/modals/confirm_snapshot_vote_modal.tsx index 0a419711814..3e41c561f7a 100644 --- a/packages/commonwealth/client/scripts/views/modals/confirm_snapshot_vote_modal.tsx +++ b/packages/commonwealth/client/scripts/views/modals/confirm_snapshot_vote_modal.tsx @@ -1,19 +1,20 @@ import React from 'react'; import type { SnapshotProposal, SnapshotSpace } from 'helpers/snapshot_utils'; -import app from '../../state'; +import { useBrowserAnalyticsTrack } from 'hooks/useBrowserAnalyticsTrack'; +import '../../../styles/modals/confirm_snapshot_vote_modal.scss'; import { notifyError } from '../../controllers/app/notifications'; import { castVote } from '../../helpers/snapshot_utils'; -import { formatNumberShort } from '../../../../shared/adapters/currency'; -import { CWButton } from '../components/component_kit/new_designs/cw_button'; +import app from '../../state'; import { CWText } from '../components/component_kit/cw_text'; import { CWModalBody, CWModalFooter, CWModalHeader, } from '../components/component_kit/new_designs/CWModal'; - -import '../../../styles/modals/confirm_snapshot_vote_modal.scss'; +import { CWButton } from '../components/component_kit/new_designs/cw_button'; +import { formatNumberShort } from '/adapters/currency'; +import { MixpanelSnapshotEvents } from '/analytics/types'; type ConfirmSnapshotVoteModalProps = { id: string; @@ -28,7 +29,7 @@ type ConfirmSnapshotVoteModalProps = { }; export const ConfirmSnapshotVoteModal = ( - props: ConfirmSnapshotVoteModalProps + props: ConfirmSnapshotVoteModalProps, ) => { const { id, @@ -44,6 +45,35 @@ export const ConfirmSnapshotVoteModal = ( const [isSaving, setIsSaving] = React.useState(false); + const { trackAnalytics } = useBrowserAnalyticsTrack({ onAction: true }); + + const handleVote = async (e) => { + e.preventDefault(); + setIsSaving(true); + const votePayload = { + space: space.id, + proposal: id, + type: 'single-choice', + choice: parseInt(selectedChoice) + 1, + metadata: JSON.stringify({}), + }; + try { + castVote(author.address, votePayload).then(async () => { + await app.snapshot.refreshProposals(); + onModalClose(); + successCallback(); + }); + trackAnalytics({ + event: MixpanelSnapshotEvents.SNAPSHOT_VOTE_OCCURRED, + }); + } catch (err) { + console.log(err); + const errorMessage = err.message; + notifyError(errorMessage); + } + setIsSaving(false); + }; + return (
@@ -83,29 +113,7 @@ export const ConfirmSnapshotVoteModal = ( buttonType="primary" buttonHeight="sm" disabled={isSaving} - onClick={async (e) => { - e.preventDefault(); - setIsSaving(true); - const votePayload = { - space: space.id, - proposal: id, - type: 'single-choice', - choice: parseInt(selectedChoice) + 1, - metadata: JSON.stringify({}), - }; - try { - castVote(author.address, votePayload).then(async () => { - await app.snapshot.refreshProposals(); - onModalClose(); - successCallback(); - }); - } catch (err) { - console.log(err); - const errorMessage = err.message; - notifyError(errorMessage); - } - setIsSaving(false); - }} + onClick={handleVote} />
diff --git a/packages/commonwealth/client/scripts/views/pages/new_proposal/aave_proposal_form.tsx b/packages/commonwealth/client/scripts/views/pages/new_proposal/aave_proposal_form.tsx index 27e766401e0..d0a28903b92 100644 --- a/packages/commonwealth/client/scripts/views/pages/new_proposal/aave_proposal_form.tsx +++ b/packages/commonwealth/client/scripts/views/pages/new_proposal/aave_proposal_form.tsx @@ -1,15 +1,16 @@ import { utils } from 'ethers'; import React, { useEffect, useState } from 'react'; -import 'pages/new_proposal/aave_proposal_form.scss'; - +import { useBrowserAnalyticsTrack } from 'client/scripts/hooks/useBrowserAnalyticsTrack'; import { notifyError } from 'controllers/app/notifications'; import type Aave from 'controllers/chain/ethereum/aave/adapter'; import { AaveExecutor } from 'controllers/chain/ethereum/aave/api'; import type { AaveProposalArgs } from 'controllers/chain/ethereum/aave/governance'; +import 'pages/new_proposal/aave_proposal_form.scss'; import app from 'state'; import { PopoverMenu } from 'views/components/component_kit/CWPopoverMenu'; import { User } from 'views/components/user/user'; +import { MixpanelGovernanceEvents } from '../../../../../shared/analytics/types'; import { CWButton } from '../../components/component_kit/cw_button'; import { CWCheckbox } from '../../components/component_kit/cw_checkbox'; import { CWIconButton } from '../../components/component_kit/cw_icon_button'; @@ -36,6 +37,7 @@ export const AaveProposalForm = () => { const author = app.user.activeAccount; const aave = app.chain as Aave; + const { trackAnalytics } = useBrowserAnalyticsTrack({ onAction: true }); useEffect(() => { const getExecutors = async () => { @@ -56,6 +58,67 @@ export const AaveProposalForm = () => { setAaveProposalState(newAaveProposalState); }; + const handleSendTransaction = async (e) => { + e.preventDefault(); + + setProposer(app.user?.activeAccount?.address); + + if (!proposer) { + throw new Error('Invalid address / not signed in'); + } + + if (!executor) { + throw new Error('Invalid executor'); + } + + if (!ipfsHash) { + throw new Error('No ipfs hash'); + } + + const targets = []; + const values = []; + const calldatas = []; + const signatures = []; + const withDelegateCalls = []; + + for (let i = 0; i < tabCount; i++) { + const aaveProposal = aaveProposalState[i]; + + if (aaveProposal.target) { + targets.push(aaveProposal.target); + } else { + throw new Error(`No target for Call ${i + 1}`); + } + + values.push(aaveProposal.value || '0'); + calldatas.push(aaveProposal.calldata || ''); + withDelegateCalls.push(aaveProposal.withDelegateCall || false); + signatures.push(aaveProposal.signature || ''); + } + + // TODO: preload this ipfs value to ensure it's correct + const _ipfsHash = utils.formatBytes32String(ipfsHash); + + const details: AaveProposalArgs = { + executor, + targets, + values, + calldatas, + signatures, + withDelegateCalls, + ipfsHash: _ipfsHash, + }; + + try { + await aave.governance.propose(details); + trackAnalytics({ + event: MixpanelGovernanceEvents.AAVE_PROPOSAL_CREATED, + }); + } catch (err) { + notifyError(err.data?.message || err.message); + } + }; + return (
@@ -198,64 +261,7 @@ export const AaveProposalForm = () => { label="Delegate Call" value="" /> - { - e.preventDefault(); - - setProposer(app.user?.activeAccount?.address); - - if (!proposer) { - throw new Error('Invalid address / not signed in'); - } - - if (!executor) { - throw new Error('Invalid executor'); - } - - if (!ipfsHash) { - throw new Error('No ipfs hash'); - } - - const targets = []; - const values = []; - const calldatas = []; - const signatures = []; - const withDelegateCalls = []; - - for (let i = 0; i < tabCount; i++) { - const aaveProposal = aaveProposalState[i]; - - if (aaveProposal.target) { - targets.push(aaveProposal.target); - } else { - throw new Error(`No target for Call ${i + 1}`); - } - - values.push(aaveProposal.value || '0'); - calldatas.push(aaveProposal.calldata || ''); - withDelegateCalls.push(aaveProposal.withDelegateCall || false); - signatures.push(aaveProposal.signature || ''); - } - - // TODO: preload this ipfs value to ensure it's correct - const _ipfsHash = utils.formatBytes32String(ipfsHash); - - const details: AaveProposalArgs = { - executor, - targets, - values, - calldatas, - signatures, - withDelegateCalls, - ipfsHash: _ipfsHash, - }; - - aave.governance - .propose(details) - .catch((err) => notifyError(err.data?.message || err.message)); - }} - /> +
); }; diff --git a/packages/commonwealth/client/scripts/views/pages/new_proposal/compound_proposal_form.tsx b/packages/commonwealth/client/scripts/views/pages/new_proposal/compound_proposal_form.tsx index 4b2d29fea2a..21434505727 100644 --- a/packages/commonwealth/client/scripts/views/pages/new_proposal/compound_proposal_form.tsx +++ b/packages/commonwealth/client/scripts/views/pages/new_proposal/compound_proposal_form.tsx @@ -1,14 +1,13 @@ -import React, { useState } from 'react'; - +import { useBrowserAnalyticsTrack } from 'client/scripts/hooks/useBrowserAnalyticsTrack'; import { notifyError, notifySuccess } from 'controllers/app/notifications'; import type Compound from 'controllers/chain/ethereum/compound/adapter'; import type { CompoundProposalArgs } from 'controllers/chain/ethereum/compound/governance'; - import 'pages/new_proposal/compound_proposal_form.scss'; - +import React, { useState } from 'react'; import app from 'state'; import { PopoverMenu } from 'views/components/component_kit/CWPopoverMenu'; import { User } from 'views/components/user/user'; +import { MixpanelGovernanceEvents } from '../../../../../shared/analytics/types'; import { CWButton } from '../../components/component_kit/cw_button'; import { CWIconButton } from '../../components/component_kit/cw_icon_button'; import { CWLabel } from '../../components/component_kit/cw_label'; @@ -33,6 +32,70 @@ export const CompoundProposalForm = () => { const author = app.user.activeAccount; + const { trackAnalytics } = useBrowserAnalyticsTrack({ onAction: true }); + + const handleSendTransaction = async (e) => { + e.preventDefault(); + + setProposer(app.user?.activeAccount?.address); + + if (!proposer) { + throw new Error('Invalid address / not signed in'); + } + + if (!description) { + throw new Error('Invalid description'); + } + + const targets = []; + const values = []; + const calldatas = []; + const signatures = []; + + for (let i = 0; i < tabCount; i++) { + const aaveProposal = aaveProposalState[i]; + if (aaveProposal.target) { + targets.push(aaveProposal.target); + } else { + throw new Error(`No target for Call ${i + 1}`); + } + + values.push(aaveProposal.value || '0'); + calldatas.push(aaveProposal.calldata || ''); + signatures.push(aaveProposal.signature || ''); + } + + // if they passed a title, use the JSON format for description. + // otherwise, keep description raw + + if (title) { + setDescription( + JSON.stringify({ + description, + title, + }), + ); + } + + const details: CompoundProposalArgs = { + description, + targets, + values, + calldatas, + signatures, + }; + + try { + const result = await (app.chain as Compound).governance.propose(details); + notifySuccess(`Proposal ${result} created successfully!`); + trackAnalytics({ + event: MixpanelGovernanceEvents.COMPOUND_PROPOSAL_CREATED, + }); + } catch (err) { + notifyError(err.data?.message || err.message); + } + }; + return (
@@ -141,67 +204,7 @@ export const CompoundProposalForm = () => { aaveProposalState[activeTabIndex].signature = e.target.value; }} /> - { - e.preventDefault(); - - setProposer(app.user?.activeAccount?.address); - - if (!proposer) { - throw new Error('Invalid address / not signed in'); - } - - if (!description) { - throw new Error('Invalid description'); - } - - const targets = []; - const values = []; - const calldatas = []; - const signatures = []; - - for (let i = 0; i < tabCount; i++) { - const aaveProposal = aaveProposalState[i]; - if (aaveProposal.target) { - targets.push(aaveProposal.target); - } else { - throw new Error(`No target for Call ${i + 1}`); - } - - values.push(aaveProposal.value || '0'); - calldatas.push(aaveProposal.calldata || ''); - signatures.push(aaveProposal.signature || ''); - } - - // if they passed a title, use the JSON format for description. - // otherwise, keep description raw - - if (title) { - setDescription( - JSON.stringify({ - description, - title, - }), - ); - } - - const details: CompoundProposalArgs = { - description, - targets, - values, - calldatas, - signatures, - }; - - (app.chain as Compound).governance - .propose(details) - .then((result: string) => { - notifySuccess(`Proposal ${result} created successfully!`); - }) - .catch((err) => notifyError(err.data?.message || err.message)); - }} - /> +
); }; diff --git a/packages/commonwealth/client/scripts/views/pages/new_proposal/cosmos_proposal_form.tsx b/packages/commonwealth/client/scripts/views/pages/new_proposal/cosmos_proposal_form.tsx index 5bcdba80645..204aa474ffc 100644 --- a/packages/commonwealth/client/scripts/views/pages/new_proposal/cosmos_proposal_form.tsx +++ b/packages/commonwealth/client/scripts/views/pages/new_proposal/cosmos_proposal_form.tsx @@ -1,6 +1,4 @@ -import type { Any as ProtobufAny } from 'cosmjs-types/google/protobuf/any'; -import React, { useState } from 'react'; - +import { useBrowserAnalyticsTrack } from 'client/scripts/hooks/useBrowserAnalyticsTrack'; import { notifyError } from 'controllers/app/notifications'; import type CosmosAccount from 'controllers/chain/cosmos/account'; import type Cosmos from 'controllers/chain/cosmos/adapter'; @@ -9,13 +7,15 @@ import { encodeTextProposal, } from 'controllers/chain/cosmos/gov/v1beta1/utils-v1beta1'; import { CosmosToken } from 'controllers/chain/cosmos/types'; +import type { Any as ProtobufAny } from 'cosmjs-types/google/protobuf/any'; +import { useCommonNavigate } from 'navigation/helpers'; +import React, { useState } from 'react'; +import app from 'state'; import { useDepositParamsQuery, useStakingParamsQuery, } from 'state/api/chainParams'; - -import { useCommonNavigate } from 'navigation/helpers'; -import app from 'state'; +import { MixpanelGovernanceEvents } from '../../../../../shared/analytics/types'; import { minimalToNaturalDenom, naturalDenomToMinimal, @@ -39,6 +39,8 @@ export const CosmosProposalForm = () => { const navigate = useCommonNavigate(); + const { trackAnalytics } = useBrowserAnalyticsTrack({ onAction: true }); + const author = app.user.activeAccount as CosmosAccount; const cosmos = app.chain as Cosmos; const meta = cosmos.meta; @@ -48,9 +50,60 @@ export const CosmosProposalForm = () => { useDepositParamsQuery(stakingDenom); const minDeposit = parseFloat( - minimalToNaturalDenom(+depositParams?.minDeposit, meta?.decimals) + minimalToNaturalDenom(+depositParams?.minDeposit, meta?.decimals), ); + const handleSendTransaction = async (e) => { + e.preventDefault(); + + let prop: ProtobufAny; + + const depositInMinimalDenom = naturalDenomToMinimal( + deposit, + meta?.decimals, + ); + + const _deposit = deposit + ? new CosmosToken( + depositParams?.minDeposit?.denom, + depositInMinimalDenom, + false, + ) + : depositParams?.minDeposit; + + if (cosmosProposalType === 'textProposal') { + prop = encodeTextProposal(title, description); + } else if (cosmosProposalType === 'communitySpend') { + const spendAmountInMinimalDenom = naturalDenomToMinimal( + payoutAmount, + meta?.decimals, + ); + prop = encodeCommunitySpend( + title, + description, + recipient, + spendAmountInMinimalDenom, + depositParams?.minDeposit?.denom, + ); + } else { + throw new Error('Unknown Cosmos proposal type.'); + } + + try { + const result = await cosmos.governance.submitProposalTx( + author, + _deposit, + prop, + ); + trackAnalytics({ + event: MixpanelGovernanceEvents.COSMOS_PROPOSAL_CREATED, + }); + navigate(`/proposal/${result}`); + } catch (err) { + notifyError(err.message); + } + }; + return ( <> @@ -114,53 +167,7 @@ export const CosmosProposalForm = () => { }} /> )} - { - e.preventDefault(); - - let prop: ProtobufAny; - - const depositInMinimalDenom = naturalDenomToMinimal( - deposit, - meta?.decimals - ); - - const _deposit = deposit - ? new CosmosToken( - depositParams?.minDeposit?.denom, - depositInMinimalDenom, - false - ) - : depositParams?.minDeposit; - - if (cosmosProposalType === 'textProposal') { - prop = encodeTextProposal(title, description); - } else if (cosmosProposalType === 'communitySpend') { - const spendAmountInMinimalDenom = naturalDenomToMinimal( - payoutAmount, - meta?.decimals - ); - prop = encodeCommunitySpend( - title, - description, - recipient, - spendAmountInMinimalDenom, - depositParams?.minDeposit?.denom - ); - } else { - throw new Error('Unknown Cosmos proposal type.'); - } - - // TODO: add disabled / loading - cosmos.governance - .submitProposalTx(author, _deposit, prop) - .then((result) => { - navigate(`/proposal/${result}`); - }) - .catch((err) => notifyError(err.message)); - }} - /> + ); }; diff --git a/packages/commonwealth/client/scripts/views/pages/new_proposal/sputnik_proposal_form.tsx b/packages/commonwealth/client/scripts/views/pages/new_proposal/sputnik_proposal_form.tsx index 1f0557d9402..421212661d4 100644 --- a/packages/commonwealth/client/scripts/views/pages/new_proposal/sputnik_proposal_form.tsx +++ b/packages/commonwealth/client/scripts/views/pages/new_proposal/sputnik_proposal_form.tsx @@ -1,10 +1,11 @@ import React, { useState } from 'react'; +import { useBrowserAnalyticsTrack } from 'client/scripts/hooks/useBrowserAnalyticsTrack'; import { notifyError } from 'controllers/app/notifications'; import type NearSputnik from 'controllers/chain/near/sputnik/adapter'; import type { NearSputnikProposalKind } from 'controllers/chain/near/sputnik/types'; - import app from 'state'; +import { MixpanelGovernanceEvents } from '../../../../../shared/analytics/types'; import { CWButton } from '../../components/component_kit/cw_button'; import { CWDropdown } from '../../components/component_kit/cw_dropdown'; import { CWTextInput } from '../../components/component_kit/cw_text_input'; @@ -33,9 +34,51 @@ export const SputnikProposalForm = () => { const [member, setMember] = useState(''); const [payoutAmount, setPayoutAmount] = useState(0); const [sputnikProposalType, setSputnikProposalType] = useState( - sputnikProposalOptions[0].value + sputnikProposalOptions[0].value, ); const [tokenId, setTokenId] = useState(''); + const { trackAnalytics } = useBrowserAnalyticsTrack({ onAction: true }); + + const handleSendTransaction = async (e) => { + e.preventDefault(); + + let propArgs: NearSputnikProposalKind; + + if (sputnikProposalType === 'addMember') { + propArgs = { + AddMemberToRole: { role: 'council', member_id: member }, + }; + } else if (sputnikProposalType === 'removeMember') { + propArgs = { + RemoveMemberFromRole: { role: 'council', member_id: member }, + }; + } else if (sputnikProposalType === 'payout') { + let amount: string; + // treat NEAR as in dollars but tokens as whole #s + if (!tokenId) { + amount = app.chain.chain.coins(+payoutAmount, true).asBN.toString(); + } else { + amount = `${+payoutAmount}`; + } + + propArgs = { + Transfer: { receiver_id: member, token_id: tokenId, amount }, + }; + } else if (sputnikProposalType === 'vote') { + propArgs = 'Vote'; + } else { + throw new Error('unsupported sputnik proposal type'); + } + + try { + await (app.chain as NearSputnik).dao.proposeTx(description, propArgs); + trackAnalytics({ + event: MixpanelGovernanceEvents.SPUTNIK_PROPOSAL_CREATED, + }); + } catch (err) { + notifyError(err.message); + } + }; return ( <> @@ -81,46 +124,7 @@ export const SputnikProposalForm = () => { }} /> )} - { - e.preventDefault(); - - let propArgs: NearSputnikProposalKind; - - if (sputnikProposalType === 'addMember') { - propArgs = { - AddMemberToRole: { role: 'council', member_id: member }, - }; - } else if (sputnikProposalType === 'removeMember') { - propArgs = { - RemoveMemberFromRole: { role: 'council', member_id: member }, - }; - } else if (sputnikProposalType === 'payout') { - let amount: string; - // treat NEAR as in dollars but tokens as whole #s - if (!tokenId) { - amount = app.chain.chain - .coins(+payoutAmount, true) - .asBN.toString(); - } else { - amount = `${+payoutAmount}`; - } - - propArgs = { - Transfer: { receiver_id: member, token_id: tokenId, amount }, - }; - } else if (sputnikProposalType === 'vote') { - propArgs = 'Vote'; - } else { - throw new Error('unsupported sputnik proposal type'); - } - - (app.chain as NearSputnik).dao - .proposeTx(description, propArgs) - .catch((err) => notifyError(err.message)); - }} - /> + ); }; diff --git a/packages/commonwealth/client/scripts/views/pages/view_snapshot_proposal/snapshot_poll_card.tsx b/packages/commonwealth/client/scripts/views/pages/view_snapshot_proposal/snapshot_poll_card.tsx index 4f9df9f7e15..40aff7cf1ed 100644 --- a/packages/commonwealth/client/scripts/views/pages/view_snapshot_proposal/snapshot_poll_card.tsx +++ b/packages/commonwealth/client/scripts/views/pages/view_snapshot_proposal/snapshot_poll_card.tsx @@ -1,6 +1,7 @@ -import React, { useEffect } from 'react'; - +import { useBrowserAnalyticsTrack } from 'client/scripts/hooks/useBrowserAnalyticsTrack'; import 'components/poll_card.scss'; +import React, { useEffect } from 'react'; +import { MixpanelSnapshotEvents } from '../../../../../shared/analytics/types'; import { CWCard } from '../../components/component_kit/cw_card'; import { CWText } from '../../components/component_kit/cw_text'; @@ -9,11 +10,11 @@ import type { VoteInformation, } from '../../components/poll_card'; import { - buildVoteDirectionString, CastVoteSection, PollOptions, ResultsSection, VoteDisplay, + buildVoteDirectionString, } from '../../components/poll_card'; export type SnapshotPollCardProps = Omit< @@ -44,21 +45,26 @@ export const SnapshotPollCard = (props: SnapshotPollCardProps) => { const [internalHasVoted, setInternalHasVoted] = React.useState(hasVoted); const [selectedOptions, setSelectedOptions] = React.useState>( - [] // is never updated? + [], // is never updated? ); const [internalTotalVoteCount, setInternalTotalVoteCount] = React.useState(totalVoteCount); const [voteDirectionString, setVoteDirectionString] = React.useState( - votedFor ? buildVoteDirectionString(votedFor) : '' + votedFor ? buildVoteDirectionString(votedFor) : '', ); const [internalVoteInformation, setInternalVoteInformation] = React.useState>(voteInformation); const resultString = 'Results'; + const { trackAnalytics } = useBrowserAnalyticsTrack({ onAction: true }); + const castVote = () => { setVoteDirectionString(buildVoteDirectionString(selectedOptions[0])); onSnapshotVoteCast(selectedOptions[0]); + trackAnalytics({ + event: MixpanelSnapshotEvents.SNAPSHOT_VOTE_OCCURRED, + }); }; useEffect(() => { diff --git a/packages/commonwealth/shared/analytics/types.ts b/packages/commonwealth/shared/analytics/types.ts index 2f05469f675..3e1ef12e63f 100644 --- a/packages/commonwealth/shared/analytics/types.ts +++ b/packages/commonwealth/shared/analytics/types.ts @@ -69,6 +69,17 @@ export const enum MixpanelSnapshotEvents { SNAPSHOT_PROPOSAL_CREATED = 'Snapshot Proposal Created', } +export const enum MixpanelGovernanceEvents { + SPUTNIK_PROPOSAL_CREATED = 'Sputnik Proposal Created', + AAVE_PROPOSAL_CREATED = 'Aave Proposal Created', + COMPOUND_PROPOSAL_CREATED = 'Compound Proposal Created', + COSMOS_PROPOSAL_CREATED = 'Cosmos Proposal Created', + SPUTNIK_VOTE_OCCURRED = 'Sputnik Vote Occurred', + AAVE_VOTE_OCCURRED = 'Aave Vote Occurred', + COMPOUND_VOTE_OCCURRED = 'Compund Vote Occurred', + COSMOS_VOTE_OCCURRED = 'Cosmos Vote Occurred', +} + export type MixpanelEvents = | MixpanelLoginEvent | MixpanelUserSignupEvent @@ -77,7 +88,8 @@ export type MixpanelEvents = | MixpanelCommunityInteractionEvent | MixpanelSnapshotEvents | MixpanelErrorCaptureEvent - | MixpanelClickthroughEvent; + | MixpanelClickthroughEvent + | MixpanelGovernanceEvents; export type AnalyticsEvent = MixpanelEvents; // add other providers events here