diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.tsx index 333ea17d3e..a49bcd69a4 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.tsx +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.tsx @@ -20,6 +20,7 @@ import { UserPromptService } from '@lib/scripts/background/services'; import { DAPP_CHANNELS } from '@src/utils/constants'; import { of, take } from 'rxjs'; import { runtime } from 'webextension-polyfill'; +import { Skeleton } from 'antd'; export const ConfirmTransaction = (): React.ReactElement => { const { t } = useTranslation(); @@ -92,7 +93,11 @@ export const ConfirmTransaction = (): React.ReactElement => { pageClassname={styles.spaceBetween} title={!confirmTransactionError && txType && t(`core.${txType}.title`)} > - {req && txType && setConfirmTransactionError(true)} />} + {req && txType ? ( + setConfirmTransactionError(true)} /> + ) : ( + + )} {!confirmTransactionError && (
+ +
+ + + ); +}; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/VotingProceduresContainer.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/VotingProceduresContainer.tsx index d85526fb16..771506372b 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/VotingProceduresContainer.tsx +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/VotingProceduresContainer.tsx @@ -1,11 +1,13 @@ import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { VotingProcedures } from '@lace/core'; -import { drepIDasBech32FromHash, votingProceduresInspector } from './utils'; -import { useCexplorerBaseUrl } from './hooks'; -import { VoterTypeEnum, getVote, getVoterType } from '@src/utils/tx-inspection'; +import { getDRepId, hasValidDrepRegistration, votingProceduresInspector } from './utils'; +import { useCexplorerBaseUrl, useDisallowSignTx } from './hooks'; +import { getVote, getVoterType } from '@src/utils/tx-inspection'; import { Wallet } from '@lace/cardano'; +import { NonRegisteredUserModal } from './NonRegisteredUserModal/NonRegisteredUserModal'; import { useViewsFlowContext } from '@providers'; +import { useWalletStore } from '@src/stores'; export const VotingProceduresContainer = (): React.ReactElement => { const { t } = useTranslation(); @@ -13,7 +15,11 @@ export const VotingProceduresContainer = (): React.ReactElement => { signTxRequest: { request }, dappInfo } = useViewsFlowContext(); + const { walletState } = useWalletStore(); const [votingProcedures, setVotingProcedures] = useState([]); + const [isNonRegisteredUserModalVisible, setIsNonRegisteredUserModalVisible] = useState(false); + const [userAckNonRegisteredState, setUserAckNonRegisteredState] = useState(false); + const disallowSignTx = useDisallowSignTx(request); useEffect(() => { const getVotingProcedures = async () => { @@ -24,54 +30,65 @@ export const VotingProceduresContainer = (): React.ReactElement => { getVotingProcedures(); }, [request]); + useEffect(() => { + if (!walletState?.transactions.history || userAckNonRegisteredState) return; + setIsNonRegisteredUserModalVisible(!hasValidDrepRegistration(walletState.transactions.history)); + }, [walletState?.transactions.history, userAckNonRegisteredState]); + const explorerBaseUrl = useCexplorerBaseUrl(); return ( - { - const voterType = getVoterType(votingProcedure.voter.__typename); + <> + { + setUserAckNonRegisteredState(true); + setIsNonRegisteredUserModalVisible(false); + }} + onClose={() => disallowSignTx(true)} + /> + { + const voterType = getVoterType(votingProcedure.voter.__typename); - const drepId = - voterType === VoterTypeEnum.DREP - ? drepIDasBech32FromHash(votingProcedure.voter.credential.hash) - : votingProcedure.voter.credential.hash.toString(); - return { - voter: { - type: t(`core.VotingProcedures.voterTypes.${voterType}`), - dRepId: drepId - }, - votes: votingProcedure.votes.map((vote) => ({ - actionId: { - index: vote.actionId.actionIndex, - txHash: vote.actionId.id.toString(), - txHashUrl: `${explorerBaseUrl}/${vote.actionId.id}` + return { + voter: { + type: t(`core.VotingProcedures.voterTypes.${voterType}`), + dRepId: getDRepId(votingProcedure.voter) }, - votingProcedure: { - vote: t(`core.VotingProcedures.votes.${getVote(vote.votingProcedure.vote)}`), - anchor: !!vote.votingProcedure.anchor?.url && { - url: vote.votingProcedure.anchor?.url, - hash: vote.votingProcedure.anchor?.dataHash.toString() + votes: votingProcedure.votes.map((vote) => ({ + actionId: { + index: vote.actionId.actionIndex, + txHash: vote.actionId.id.toString(), + txHashUrl: `${explorerBaseUrl}/${vote.actionId.id}` + }, + votingProcedure: { + vote: t(`core.VotingProcedures.votes.${getVote(vote.votingProcedure.vote)}`), + anchor: !!vote.votingProcedure.anchor && { + url: vote.votingProcedure.anchor.url, + hash: vote.votingProcedure.anchor.dataHash.toString() + } } - } - })) - }; - })} - translations={{ - voterType: t('core.VotingProcedures.voterType'), - procedureTitle: t('core.VotingProcedures.procedureTitle'), - actionIdTitle: t('core.VotingProcedures.actionIdTitle'), - vote: t('core.VotingProcedures.vote'), - actionId: { - index: t('core.VotingProcedures.actionId.index'), - txHash: t('core.VotingProcedures.actionId.txHash') - }, - anchor: { - hash: t('core.VotingProcedures.anchor.hash'), - url: t('core.VotingProcedures.anchor.url') - }, - dRepId: t('core.VotingProcedures.dRepId') - }} - /> + })) + }; + })} + translations={{ + voterType: t('core.VotingProcedures.voterType'), + procedureTitle: t('core.VotingProcedures.procedureTitle'), + actionIdTitle: t('core.VotingProcedures.actionIdTitle'), + vote: t('core.VotingProcedures.vote'), + actionId: { + index: t('core.VotingProcedures.actionId.index'), + txHash: t('core.VotingProcedures.actionId.txHash') + }, + anchor: { + hash: t('core.VotingProcedures.anchor.hash'), + url: t('core.VotingProcedures.anchor.url') + }, + dRepId: t('core.VotingProcedures.dRepId') + }} + /> + ); }; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/VotingProceduresContainer.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/VotingProceduresContainer.test.tsx index e8f382ee1c..bfb5f96462 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/VotingProceduresContainer.test.tsx +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/VotingProceduresContainer.test.tsx @@ -7,10 +7,13 @@ const mockUseWalletStore = jest.fn(); const t = jest.fn().mockImplementation((res) => res); const mockUseTranslation = jest.fn(() => ({ t })); const mockVotingProcedures = jest.fn(); +const mockNonRegisteredUserModal = jest.fn(); +const mockUseDisallowSignTx = jest.fn(); +const mockHasValidDrepRegistration = jest.fn(); const mockPreprodCexplorerBaseUrl = 'PREPROD_CEXPLORER_BASE_URL'; const mockCexplorerUrlPathsTx = 'CEXPLORER_URL_PATHS.TX'; import * as React from 'react'; -import { cleanup, render } from '@testing-library/react'; +import { cleanup, render, waitFor } from '@testing-library/react'; import { VotingProceduresContainer } from '../VotingProceduresContainer'; import '@testing-library/jest-dom'; import { act } from 'react-dom/test-utils'; @@ -47,6 +50,15 @@ jest.mock('@lace/core', () => { }; }); +jest.mock('../NonRegisteredUserModal/NonRegisteredUserModal', () => { + const original = jest.requireActual('../NonRegisteredUserModal/NonRegisteredUserModal'); + return { + __esModule: true, + ...original, + NonRegisteredUserModal: mockNonRegisteredUserModal + }; +}); + jest.mock('react-i18next', () => { const original = jest.requireActual('react-i18next'); return { @@ -56,6 +68,20 @@ jest.mock('react-i18next', () => { }; }); +jest.mock('../utils', () => ({ + ...jest.requireActual('../utils'), + hasValidDrepRegistration: mockHasValidDrepRegistration +})); + +jest.mock('../hooks', () => { + const original = jest.requireActual('../hooks'); + return { + __esModule: true, + ...original, + useDisallowSignTx: mockUseDisallowSignTx + }; +}); + const dappInfo = { name: 'dappName', logo: 'dappLogo', @@ -153,19 +179,21 @@ jest.mock('@providers', () => ({ describe('Testing VotingProceduresContainer component', () => { beforeEach(() => { + mockHasValidDrepRegistration.mockReset(); + mockHasValidDrepRegistration.mockReturnValue(true); mockUseWalletStore.mockReset(); mockUseWalletStore.mockImplementation(() => ({ environmentName: 'Preprod' })); mockVotingProcedures.mockReset(); mockVotingProcedures.mockReturnValue(); + mockNonRegisteredUserModal.mockReset(); + mockNonRegisteredUserModal.mockReturnValue(); mockUseTranslation.mockReset(); mockUseTranslation.mockImplementation(() => ({ t })); }); afterEach(() => { - jest.resetModules(); - jest.resetAllMocks(); cleanup(); }); @@ -225,6 +253,71 @@ describe('Testing VotingProceduresContainer component', () => { ); }); + test('should handle NonRegisteredUserModal onConfirm', async () => { + mockHasValidDrepRegistration.mockReset(); + mockHasValidDrepRegistration.mockReturnValue(false); + mockUseWalletStore.mockReset(); + mockUseWalletStore.mockImplementation(() => ({ + environmentName: 'Preprod', + walletState: { + transactions: { + history: [] + } + } + })); + + await act(async () => { + render(, { + wrapper: getWrapper() + }); + }); + + expect(mockNonRegisteredUserModal.mock.calls[mockNonRegisteredUserModal.mock.calls.length - 1][0].visible).toEqual( + true + ); + + await act(async () => { + mockNonRegisteredUserModal.mock.calls[mockNonRegisteredUserModal.mock.calls.length - 1][0].onConfirm(); + }); + + await waitFor(async () => { + expect( + mockNonRegisteredUserModal.mock.calls[mockNonRegisteredUserModal.mock.calls.length - 1][0].visible + ).toEqual(false); + }); + }); + + test('should handle NonRegisteredUserModal onClose', async () => { + const disallowSignTxMock = jest.fn(); + mockUseDisallowSignTx.mockReset(); + mockUseDisallowSignTx.mockReturnValue(disallowSignTxMock); + mockHasValidDrepRegistration.mockReset(); + mockHasValidDrepRegistration.mockReturnValue(false); + mockUseWalletStore.mockReset(); + mockUseWalletStore.mockImplementation(() => ({ + environmentName: 'Preprod', + walletState: { + transactions: { + history: [] + } + } + })); + + await act(async () => { + render(, { + wrapper: getWrapper() + }); + }); + + expect(disallowSignTxMock).not.toHaveBeenCalled(); + + await act(async () => { + mockNonRegisteredUserModal.mock.calls[mockNonRegisteredUserModal.mock.calls.length - 1][0].onClose(); + }); + + expect(disallowSignTxMock).toHaveBeenCalledWith(true); + }); + test('testing getVoterType', () => { expect(getVoterType(constitutionalCommitteeKeyHashVoter.__typename)).toEqual( VoterTypeEnum.CONSTITUTIONAL_COMMITTEE diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/utils.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/utils.test.tsx index 919dad9d21..ca920d6f46 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/utils.test.tsx +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/utils.test.tsx @@ -17,7 +17,8 @@ import { getTxType, drepIDasBech32FromHash, pubDRepKeyToHash, - depositPaidWithSymbol + depositPaidWithSymbol, + hasValidDrepRegistration } from '../utils'; jest.mock('@cardano-sdk/core', () => ({ @@ -170,4 +171,59 @@ describe('Testing utils', () => { 'coinId Unknown not supported' ); }); + + describe('hasValidDrepRegistration', () => { + test('should return false if there transactions', () => { + const transactions = [] as unknown as Wallet.Cardano.HydratedTx[]; + expect(hasValidDrepRegistration(transactions)).toBe(false); + }); + + test('should return false if there is no certificates', () => { + const transactions = [{ body: {} }, { body: { certificates: [] } }] as unknown as Wallet.Cardano.HydratedTx[]; + expect(hasValidDrepRegistration(transactions)).toBe(false); + }); + + test('should return true if first certificate has RegisterDelegateRepresentative __typename', () => { + const transactions = [ + { + body: { + certificates: [ + { __typename: Wallet.Cardano.CertificateType.RegisterDelegateRepresentative }, + { __typename: Wallet.Cardano.CertificateType.UnregisterDelegateRepresentative } + ] + } + }, + { + body: { + certificates: [ + { __typename: Wallet.Cardano.CertificateType.UnregisterDelegateRepresentative }, + { __typename: Wallet.Cardano.CertificateType.RegisterDelegateRepresentative } + ] + } + } + ] as unknown as Wallet.Cardano.HydratedTx[]; + expect(hasValidDrepRegistration(transactions)).toBe(true); + }); + test('should return false if first certificate has UnregisterDelegateRepresentative __typename', () => { + const transactions = [ + { + body: { + certificates: [ + { __typename: Wallet.Cardano.CertificateType.UnregisterDelegateRepresentative }, + { __typename: Wallet.Cardano.CertificateType.RegisterDelegateRepresentative } + ] + } + }, + { + body: { + certificates: [ + { __typename: Wallet.Cardano.CertificateType.RegisterDelegateRepresentative }, + { __typename: Wallet.Cardano.CertificateType.UnregisterDelegateRepresentative } + ] + } + } + ] as unknown as Wallet.Cardano.HydratedTx[]; + expect(hasValidDrepRegistration(transactions)).toBe(false); + }); + }); }); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/hooks.ts b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/hooks.ts index daa89a645a..78cca9fbd4 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/hooks.ts +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/hooks.ts @@ -161,7 +161,7 @@ export const useCreateMintedAssetList = ({ export const useDisallowSignTx = ( req: TransactionWitnessRequest -): ((close?: boolean) => void) => useCallback(() => disallowSignTx(req), [req]); +): ((close?: boolean) => void) => useCallback((close) => disallowSignTx(req, close), [req]); export const useAllowSignTx = ( req: TransactionWitnessRequest diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/utils.ts b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/utils.ts index 5345ff4549..aa50a42c26 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/utils.ts +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/utils.ts @@ -7,6 +7,7 @@ import type { UserPromptService } from '@lib/scripts/background/services'; import { DAPP_CHANNELS, cardanoCoin } from '@src/utils/constants'; import { runtime } from 'webextension-polyfill'; import { of } from 'rxjs'; +import { VoterTypeEnum, getVoterType } from '@src/utils/tx-inspection'; const { CertificateType } = Wallet.Cardano; @@ -166,3 +167,23 @@ export const depositPaidWithSymbol = (deposit: bigint, coinId: Wallet.CoinId): s throw new Error(`coinId ${coinId.name} not supported`); } }; + +export const hasValidDrepRegistration = (history: Wallet.Cardano.HydratedTx[]): boolean => { + for (const transaction of history) { + const drepRegistrationOrRetirementCerticicate = transaction.body.certificates?.find((cert) => + [CertificateType.UnregisterDelegateRepresentative, CertificateType.RegisterDelegateRepresentative].includes( + cert.__typename + ) + ); + + if (drepRegistrationOrRetirementCerticicate) { + return drepRegistrationOrRetirementCerticicate.__typename === CertificateType.RegisterDelegateRepresentative; + } + } + return false; +}; + +export const getDRepId = (voter: Wallet.Cardano.Voter): Wallet.Cardano.DRepID | string => + getVoterType(voter.__typename) === VoterTypeEnum.DREP + ? drepIDasBech32FromHash(voter.credential.hash) + : voter.credential.hash.toString(); diff --git a/apps/browser-extension-wallet/src/lib/translations/en.json b/apps/browser-extension-wallet/src/lib/translations/en.json index 714c43c15f..0bc03f5880 100644 --- a/apps/browser-extension-wallet/src/lib/translations/en.json +++ b/apps/browser-extension-wallet/src/lib/translations/en.json @@ -1430,6 +1430,14 @@ "yes": "Yes", "no": "No", "abstain": "Abstain" + }, + "NonRegisteredUserModal": { + "title": "You're not a registered DRep", + "description": "Proceeding with this vote won't have any impact. Do you still want to proceed?", + "cta": { + "ok": "Proceed anyway", + "cancel": "Cancel" + } } }, "DRepRegistration": {