From fc659cb488ad0fe36724de4195b6109395dedf38 Mon Sep 17 00:00:00 2001 From: Michael Chappell <7581002+mchappell@users.noreply.github.com> Date: Wed, 6 Mar 2024 10:42:12 +0000 Subject: [PATCH] feat(extension): introduce conway-era dapp transaction screens and activity history (#727) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore/Bump cardano-js-sdk (#559) * chore!: bump cardano-js-sdk packages to latest versions * ci: add cardano services urls parameters to build step * Feat/sanchonet network switch support (#612) * feat: update env to support switching to sanchonet * chore: remove old testnet * chore: bump cardano-sdk packages to latest version --------- Co-authored-by: mirceahasegan <105701265+mirceahasegan@users.noreply.github.com> * [LW-7983] [LW-7984] dRep confirmation screens (#554) * refactor: move logic out of component * fix: file path * feat(core): setup storybook * refactor(ui): extract insufficient funds warning component * feat(core): add lace/ui package * feat(core): create ConfirmDRepRegistration component * feat(extension): install @lace/ui * refactor: move functions to utils * fix: type * refactor: types file * feat: check for DRep transaction * feat: integrate confirm drep registration component * refactor(core): use Metadata component * refactor(core): use flexbox * refactor(core): remove insufficient funds message * refactor(core): create ConfirmDRepRetirement * refactor: remove insufficient funds warning; break down main component * feat: add certificate data * feat: add drep retirement container * fix: condition * fix: type * fix: style * fix: check for anchor * fix: check for undefined certificates * fix: merge conflict * fix: type error * feat: temp env changes * feat(extension): use consistent naming of DRep ID * feat(extension): show anchor metadata if exists * feat(extension): convert user facing DRepID to bech32 * fix: add cardano symbol to deposit amount * chore(core): downgrade storybook to v6 * fix: update lock file * fix: styles and text * fix: storybook build * fix: storybook build * feat: check retired drep key * fix: check retired drep key --------- Co-authored-by: Renan Ferreira Co-authored-by: Michael Chappell <7581002+mchappell@users.noreply.github.com> * feat(extension): [LW-7984] voting procedures (#655) * feat(ui): create metadata component * wip * feat(ui): text link component * chore(ui): fix rebase * feat(ui): create metadata link component * feat(core): create voting procedures component * chore(extension): fix rebase * fix(ui): add work break * feat(extension): voting procedures * refactor(core): make actions optional * refactor(core): use action as key prop * refactor(extension): add custom title for voting procedures * [LW-8491] Voting delegation confirmation (#648) * feat(core): setup storybook * feat(core): add lace/ui package * feat: check for DRep transaction * feat: integrate confirm drep registration component * refactor: remove insufficient funds warning; break down main component * feat: add certificate data * feat: add drep retirement container * feat(extension): use consistent naming of DRep ID * fix: add cardano symbol to deposit amount * refactor: certificate inspector factory * feat: voting delegation * fix: type errors * fix: merge conflicts --------- Co-authored-by: Michael Chappell <7581002+mchappell@users.noreply.github.com> * chore: modify sanchonet urls (#667) * feat: drep update certificate signing (#664) * fix(extension): updated padding and missing bech32 addressing * fix: no explorer for sanchonet * chore(extension): add unit tests for dapp/confirm-tx (#743) * fix: update drep id casing * fix: update title translations for cip95 popups * fix(extension): [LW-9163] nullish check on existence of stakeKeyDeposit property not value (#751) * fix: e2e no unresolved imports * chore(extension): resolve pr comments (#748) * fix(extension): translation keys casing and unit tests (#756) * chore(all): exclude stories and tests from the rollup builds (#752) * chore(core): fix extension of stories .ts -> .tsx (impacts: rollup) * chore(all): exclude test files from rollup builds * Chore/update for sdk conway era (#734) * chore: stake pools sort by ros * chore: metrics require lastRos and ros * Revert "Chore/update for sdk conway era (#734)" (#767) This reverts commit 5b81b19b201fea3c4a94948a5bb6199958b8a3c0. * test(extension): add test ids for governance actions (#765) * Merge main into feat/sanchonet (#763) * feat(staking): maintenance 24 Nov 2023 (#754) * feat(staking): maintenance 24 Nov 2023 * [LW-9151] Edit account name component (#746) * feat(ui): input component * feat(extension): edit account drawer * feat(extension): add unit test * feat: rename input component * feat(ui): use pseudo parameters * feat(ui): input width * feat(ui): input disabled style * feat(extension): add ui package * feat: update lock file * feat(ui): remove redundant styles * feat(extension): [LW-5806] update Minting transaction display from dapps (#608) * feat: identify minting txs from dapps and display proper UI to confirm refs: LW-5806 * feat(extension): update dapp transaction styling * refactor(extension,core): fix sonarcloud quality checks * ci(core): add @lace/ui to dependencies * test(extension): add mocks in ConfirmTransaction.test.tsx * test(extension): remove leftover from local testing * refactor(core): add missing newline at end of file --------- Co-authored-by: przemyslaw.wlodek * test(extension): maintenance 27 nov (#757) * test(extension): maintenance nov 27 * test(extension): clear failed request test logs * test(extension): add real time console test logging, remove unnecessary logs * test(extension): update wdio version * test(extension): add node option env variable for e2e github runs * test(extension): move node options to e2e test sections * test(extension): increase max-old-space-size * test(extension): downgrade wdio to stable version, remove node env variable * feat(staking): [LW-6437, LW-8877] past epochs rewards chart (#718) --------- Co-authored-by: refi93 Co-authored-by: przemyslaw.wlodek Co-authored-by: januszjanus * feat(extension): track manual re-sync and hd wallet discovery (#712) * feat(staking): [LW-8929, LW-8777] apply pool search also to selected pools (#706) --------- Co-authored-by: refi93 * feat(staking,common): enable manageDelegation CTA from pool details (#720) Co-authored-by: przemyslaw.wlodek * feat(staking): load multi-delegation for HW wallets (#686) --------- Signed-off-by: Kamil Džurman Co-authored-by: Rafael Korbaš Co-authored-by: januszjanus Co-authored-by: przemyslaw.wlodek * test(extension): test maintenance 29 Nov - fix Dapp e2e (#760) * feat(extension): [LW-9028] Add wallet concepts to menu (#742) * feat(extension): update dropdown menu with new wallet component * feat(extension): add add new wallet option to profile dropdown * refactor(ui): add test id * refactor(extension): add test id for multi wallet * refactor(extension): increase button width to match figma * refactor(extension): remove copy address tooltip from multi-wallet menu it em * refactor(extension): align top navigation buttons with side panel * refactor(extension): update the expand button styling * refactor(extension): update network pill and expand button styling * refactor(extension): update network pill border radius * refactor(extension): limit network pill change to popup only * fix(extension): fix moving left side bar * refactor(extension,core,cardano): post-merge fixes * chore(extension): fix unit tests + rename * chore(extension): fix unit tests and mocks * chore(extension): remove TODO comments * chore(core): review comments --------- Signed-off-by: Kamil Džurman Co-authored-by: Janusz Janus Co-authored-by: Lucas Co-authored-by: Leonel Gobbi <57540576+lgobbi-atix@users.noreply.github.com> Co-authored-by: bslabiak <112852128+bslabiak@users.noreply.github.com> Co-authored-by: Tomek Marciniak <16132011+mrcnk@users.noreply.github.com> Co-authored-by: refi93 Co-authored-by: Kamil Džurman Co-authored-by: Lukasz Jagiela <12641433+ljagiela@users.noreply.github.com> Co-authored-by: Renan Valentin * feat(extension): add drep retirement id mismatch modal (#758) * chore: update cardano-sdk packages * refactor: update lace based on cardano-sdk breaking changes refactor: use internal wallet address discovery * fix: typos in translation * feat: stake and vote confirmation screens (#683) * chore/Bump cardano-js-sdk (#559) * chore!: bump cardano-js-sdk packages to latest versions * ci: add cardano services urls parameters to build step * Feat/sanchonet network switch support (#612) * feat: update env to support switching to sanchonet * chore: remove old testnet * chore: bump cardano-sdk packages to latest version --------- Co-authored-by: mirceahasegan <105701265+mirceahasegan@users.noreply.github.com> * [LW-7983] [LW-7984] dRep confirmation screens (#554) * refactor: move logic out of component * fix: file path * feat(core): setup storybook * refactor(ui): extract insufficient funds warning component * feat(core): add lace/ui package * feat(core): create ConfirmDRepRegistration component * feat(extension): install @lace/ui * refactor: move functions to utils * fix: type * refactor: types file * feat: check for DRep transaction * feat: integrate confirm drep registration component * refactor(core): use Metadata component * refactor(core): use flexbox * refactor(core): remove insufficient funds message * refactor(core): create ConfirmDRepRetirement * refactor: remove insufficient funds warning; break down main component * feat: add certificate data * feat: add drep retirement container * fix: condition * fix: type * fix: style * fix: check for anchor * fix: check for undefined certificates * fix: merge conflict * fix: type error * feat: temp env changes * feat(extension): use consistent naming of DRep ID * feat(extension): show anchor metadata if exists * feat(extension): convert user facing DRepID to bech32 * fix: add cardano symbol to deposit amount * chore(core): downgrade storybook to v6 * fix: update lock file * fix: styles and text * fix: storybook build * fix: storybook build * feat: check retired drep key * fix: check retired drep key --------- Co-authored-by: Renan Ferreira Co-authored-by: Michael Chappell <7581002+mchappell@users.noreply.github.com> * feat(extension): [LW-7984] voting procedures (#655) * feat(ui): create metadata component * wip * feat(ui): text link component * chore(ui): fix rebase * feat(ui): create metadata link component * feat(core): create voting procedures component * chore(extension): fix rebase * fix(ui): add work break * feat(extension): voting procedures * refactor(core): make actions optional * refactor(core): use action as key prop * refactor(extension): add custom title for voting procedures * [LW-8491] Voting delegation confirmation (#648) * feat(core): setup storybook * feat(core): add lace/ui package * feat: check for DRep transaction * feat: integrate confirm drep registration component * refactor: remove insufficient funds warning; break down main component * feat: add certificate data * feat: add drep retirement container * feat(extension): use consistent naming of DRep ID * fix: add cardano symbol to deposit amount * refactor: certificate inspector factory * feat: voting delegation * fix: type errors * fix: merge conflicts --------- Co-authored-by: Michael Chappell <7581002+mchappell@users.noreply.github.com> * chore: modify sanchonet urls (#667) * feat: drep update certificate signing (#664) * feat: stake and vote confirmation screens * fix: correct capitalisation of DRep ID * fix: voting translation titles * feat(extension): update staking credential path --------- Co-authored-by: Leonel Gobbi <57540576+lgobbi-atix@users.noreply.github.com> Co-authored-by: mirceahasegan <105701265+mirceahasegan@users.noreply.github.com> Co-authored-by: Renan Ferreira Co-authored-by: Michael Chappell <7581002+mchappell@users.noreply.github.com> * [LW-8490] Governance Actions (#670) * chore/Bump cardano-js-sdk (#559) * chore!: bump cardano-js-sdk packages to latest versions * ci: add cardano services urls parameters to build step * Feat/sanchonet network switch support (#612) * feat: update env to support switching to sanchonet * chore: remove old testnet * chore: bump cardano-sdk packages to latest version --------- Co-authored-by: mirceahasegan <105701265+mirceahasegan@users.noreply.github.com> * [LW-7983] [LW-7984] dRep confirmation screens (#554) * refactor: move logic out of component * fix: file path * feat(core): setup storybook * refactor(ui): extract insufficient funds warning component * feat(core): add lace/ui package * feat(core): create ConfirmDRepRegistration component * feat(extension): install @lace/ui * refactor: move functions to utils * fix: type * refactor: types file * feat: check for DRep transaction * feat: integrate confirm drep registration component * refactor(core): use Metadata component * refactor(core): use flexbox * refactor(core): remove insufficient funds message * refactor(core): create ConfirmDRepRetirement * refactor: remove insufficient funds warning; break down main component * feat: add certificate data * feat: add drep retirement container * fix: condition * fix: type * fix: style * fix: check for anchor * fix: check for undefined certificates * fix: merge conflict * fix: type error * feat: temp env changes * feat(extension): use consistent naming of DRep ID * feat(extension): show anchor metadata if exists * feat(extension): convert user facing DRepID to bech32 * fix: add cardano symbol to deposit amount * chore(core): downgrade storybook to v6 * fix: update lock file * fix: styles and text * fix: storybook build * fix: storybook build * feat: check retired drep key * fix: check retired drep key --------- Co-authored-by: Renan Ferreira Co-authored-by: Michael Chappell <7581002+mchappell@users.noreply.github.com> * feat(extension): [LW-7984] voting procedures (#655) * feat(ui): create metadata component * wip * feat(ui): text link component * chore(ui): fix rebase * feat(ui): create metadata link component * feat(core): create voting procedures component * chore(extension): fix rebase * fix(ui): add work break * feat(extension): voting procedures * refactor(core): make actions optional * refactor(core): use action as key prop * refactor(extension): add custom title for voting procedures * feat(core): create parameter change action * feat(core): create hard fork initiation action * feat(core): create new constitution action * feat(core): create no confidence action * feat(core): create treasury withdrawals action * feat(core): create info action * feat(core): create update comittee action * refactor(core): check if anchor is provided * refactor(core): update storybook path * feat(extension): add translations * refactor(core): remove ununsed prop * feat(staking): [LW-8684] add tooltip to piechart (#616) * feat: lw-8684-add tooltip to delegation piechart * fix: add custom tooltip dot style * feat(ui): add tooltip to metadata component * chore(core): fix pipeline * chore(core): update proposal procedures components to the latest ui * fix(extension): resolve pr comments --------- Co-authored-by: Leonel Gobbi <57540576+lgobbi-atix@users.noreply.github.com> Co-authored-by: mirceahasegan <105701265+mirceahasegan@users.noreply.github.com> Co-authored-by: Lucas Co-authored-by: Michael Chappell <7581002+mchappell@users.noreply.github.com> Co-authored-by: John Oshalusi Co-authored-by: Vitalii Vozniak * fix(extension): resolve pr comments * fix(extension): remove code duplications for metadata and metadata link components * feat(extension): lw-8600 (#819) * feat(extension): lw-8600 * feat(extension): fix translations * feat(extension): fix tx details mappers * fix(extension): lw-8600 move helpers to common util * fix(extension): resolve pr comment * fix(extension): unify tx details lists * fix(extension): resolve sdet comments * fix(extension): resolve sdet comments * refactor: remove need to target dev endpoints in CI * refactor: remove sanchonet as default network option * refactor: remove sanchonet explorer check * feat(extension): update drep mapper check * fix(extension): conway-era tx activities line items (#860) * fix(extension): conway-era tx activiries line items * fix(extension): adjust tests * fix(extension): resolve pr comments * feat(extension): update drep mapper check * fix(extension): convert drepid and stakekeyhash to bech32, fix translations * fix(extension): lw-9688: unable to see transaction details for conway-era transactions * fix(extension): resolve lw-9621 pe comments * fix(extension): resolve lw-9621 pr comments * fix(extension): lw-9621 add storybook showing all conway era governance activity items * chore(core): fix storybook build * fix: use correct stake address bech32 * fix(core): sanchonet-related storybook errors [LW-9721] (#880) * fix(core): add custom viewports and proper structure to storybook * fix(core): react key prop errors * refactor: rename useCexplorerBaseUrl typo * fix: add storybook back for core package * feat: update stories for conway-era tx activity history * fix: add retry fallback for matching last active tab based on non-standard URL * chore: update dev preview env file * feat(extension): update conway era tx details to the latest figma (#868) * feat(extension): update conway era tx details to the latest figma * feat(extension): handle multiline row items with subitems * feat(extension): handle parameter_change_action * fix(extension): resolve pr comments * fix(extension): update to the latest figma/AC * fix(extension): resolve pr comments * fix(extension): resolve sanchonet pr comments * fix(extension): after merge with sanchonet fixes * fix(extension): after merge with sanchonet fixes * fix(extension): resolve sanchonet pr comments * chore(core): mock @lace/cardano for storybook * fix(extension): fix tests * chore: downgrade uuid package to same used by pouchdb * refactor: remove unintended config changes * fix: no wallet dapp test * fix(extension): resolve lw-9622 sdet comments * feat(extension): update conway era history line items * fix(extension): make anchor url clickable * fix(extension): resolve pr comments * fix: lw-8940 casing in reward account * feat(extension): update sign with hw * chore: reword stakeVoteDelegation title * feat(extension): handle requests from non registered users (#935) * feat(extension): handle requests from non registered users * feat(extension): fix modal visibility condition * feat(extension): fix test * fix(extension): resolve pr comments * fix(extension): fix labels * refactor: rename proprosal procedure call * fix: broken hook test * feat: revert useTxSummary hook * feat(extension): fix test * refactor: revert story change --------- Signed-off-by: Kamil Džurman Co-authored-by: Leonel Gobbi <57540576+lgobbi-atix@users.noreply.github.com> Co-authored-by: mirceahasegan <105701265+mirceahasegan@users.noreply.github.com> Co-authored-by: Lucas Co-authored-by: Renan Ferreira Co-authored-by: vetalcore Co-authored-by: Przemysław Włodek Co-authored-by: Wojtek Kłos <114915819+wklos-iohk@users.noreply.github.com> Co-authored-by: Janusz Janus Co-authored-by: bslabiak <112852128+bslabiak@users.noreply.github.com> Co-authored-by: Tomek Marciniak <16132011+mrcnk@users.noreply.github.com> Co-authored-by: refi93 Co-authored-by: Kamil Džurman Co-authored-by: Lukasz Jagiela <12641433+ljagiela@users.noreply.github.com> Co-authored-by: John Oshalusi Co-authored-by: wklos-iohk --- apps/browser-extension-wallet/.env.defaults | 5 +- .../.env.developerpreview | 3 +- apps/browser-extension-wallet/.env.example | 5 +- apps/browser-extension-wallet/src/config.ts | 8 +- .../src/dapp-connector.tsx | 5 +- .../context/AddressBookProvider.tsx | 3 +- .../features/dapp/components/DappError.tsx | 53 + .../dapp/components/DappTransactionFail.tsx | 4 +- .../components/DappTransactionSuccess.tsx | 4 +- .../dapp/components/Layout.module.scss | 7 +- .../src/features/dapp/components/NoWallet.tsx | 35 - .../collateral/CreateCollateral.tsx | 5 +- .../ConfirmDRepRegistrationContainer.tsx | 62 ++ .../ConfirmDRepRetirementContainer.tsx | 93 ++ .../ConfirmDRepUpdateContainer.tsx | 52 + ...rmStakeRegistrationDelegationContainer.tsx | 59 ++ .../ConfirmStakeVoteDelegationContainer.tsx | 63 ++ ...akeVoteRegistrationDelegationContainer.tsx | 70 ++ .../ConfirmTransaction.module.scss | 21 +- .../ConfirmTransaction.tsx | 125 +++ .../ConfirmTransactionContent.tsx | 46 + .../ConfirmVoteDelegationContainer.tsx | 57 ++ ...irmVoteRegistrationDelegationContainer.tsx | 66 ++ .../DappTransactionContainer.tsx} | 186 +--- .../NonRegisteredUserModal.module.scss | 52 + .../NonRegisteredUserModal.tsx | 50 + .../ProposalProceduresContainer.tsx | 66 ++ .../VotingProceduresContainer.tsx | 94 ++ .../ConfirmDRepRegistrationContainer.test.tsx | 150 +++ .../ConfirmDRepRetirementContainer.test.tsx | 222 +++++ .../ConfirmDRepUpdateContainer.test.tsx | 147 +++ ...keRegistrationDelegationContainer.test.tsx | 166 ++++ ...nfirmStakeVoteDelegationContainer.test.tsx | 186 ++++ ...teRegistrationDelegationContainer.test.tsx | 197 ++++ .../__tests__/ConfirmTransaction.test.tsx | 248 +++++ .../ConfirmTransactionContent.test.tsx | 399 ++++++++ .../ConfirmVoteDelegationContainer.test.tsx | 262 +++++ ...teRegistrationDelegationContainer.test.tsx | 193 ++++ .../DappTransactionContainer.test.tsx | 305 ++++++ .../ProposalProceduresContainer.test.tsx | 191 ++++ .../VotingProceduresContainer.test.tsx | 338 +++++++ .../__tests__/hooks.test.tsx | 324 +++++++ .../__tests__/utils.test.tsx | 229 +++++ .../components/confirm-transaction/hooks.ts | 234 +++++ .../HardForkInitiationActionContainer.tsx | 96 ++ .../InfoActionContainer.tsx | 50 + .../NewConstitutionActionContainer.tsx | 101 ++ .../NoConfidenceActionContainer.tsx | 84 ++ .../ParameterChangeActionContainer.tsx | 344 +++++++ .../TreasuryWithdrawalsActionContainer.tsx | 95 ++ .../UpdateCommitteeActionContainer.tsx | 108 +++ ...HardForkInitiationActionContainer.test.tsx | 177 ++++ .../__tests__/InfoActionContainer.test.tsx | 115 +++ .../NewConstitutionActionContainer.test.tsx | 181 ++++ .../NoConfidenceActionContainer.test.tsx | 162 ++++ .../ParameterChangeActionContainer.test.tsx | 506 ++++++++++ ...reasuryWithdrawalsActionContainer.test.tsx | 170 ++++ .../UpdateCommitteeActionContainer.test.tsx | 210 ++++ .../confirm-transaction/testing.utils.tsx | 43 + .../components/confirm-transaction/types.ts | 2 + .../components/confirm-transaction/utils.ts | 176 ++-- .../src/features/dapp/components/index.ts | 1 - .../src/features/dapp/config/ViewsConfig.tsx | 2 +- .../__tests__/useActionExecution.test.ts | 2 +- .../__tests__/useDelegationDetails.test.ts | 2 +- .../src/lib/scripts/background/wallet.ts | 2 +- .../src/lib/translations/en.json | 340 ++++++- .../client/PostHogClient.ts | 2 +- .../PostHogClientProvider/client/config.ts | 12 +- .../providers/ViewFlowProvider/context.tsx | 12 +- .../src/routes/DappConnectorView.tsx | 27 +- .../__tests__/activity-detail-slice.test.ts | 31 + .../transaction-detail-slice.test.ts | 11 +- .../__tests__/wallet-info-slice.test.ts | 2 +- .../stores/slices/activity-detail-slice.ts | 66 +- .../stores/slices/wallet-activities-slice.ts | 21 +- .../src/stores/types.ts | 15 +- .../src/types/activity-detail.ts | 13 +- .../src/utils/__tests__/chain.test.ts | 6 +- .../src/utils/__tests__/inspectTxType.test.ts | 189 +++- .../src/utils/chain.ts | 3 + .../src/utils/get-chain-name.ts | 2 + .../src/utils/mocks/certificates.ts | 73 ++ .../src/utils/mocks/governance.ts | 62 ++ .../src/utils/mocks/raw-transactions.ts | 81 ++ .../src/utils/mocks/test-helpers.tsx | 3 +- .../src/utils/mocks/tx.ts | 6 + .../src/utils/tx-inspection.ts | 199 +++- .../activity/components/ActivityDetail.tsx | 19 +- .../components/TransactionDetailsProxy.tsx | 22 +- .../activity/helpers/common-tx-transformer.ts | 593 ++++++++++- .../helpers/filter-outputs-by-tx-direction.ts | 4 +- .../helpers/reward-history-transformer.ts | 8 +- .../features/activity/helpers/types.ts | 19 +- .../send-transaction/components/Form/util.ts | 5 +- .../Collateral/send/CollateralStepSend.tsx | 5 +- .../settings/components/NetworkChoice.tsx | 18 +- .../StakePoolConfirmation.tsx | 10 +- .../components/CatalystRegistrationFlow.tsx | 12 +- .../test/__mocks__/set-env-vars.js | 5 +- .../browser-extension-wallet/webpack-utils.js | 2 +- package.json | 1 + packages/cardano/src/wallet/types.ts | 34 +- packages/common/src/analytics/types.ts | 2 + packages/core/.storybook/__mocks__/cardano.ts | 6 + packages/core/.storybook/main.js | 39 +- packages/core/package.json | 5 +- packages/core/src/index.ts | 10 + .../arrow-diagonal-down-outline.component.svg | 3 + .../arrow-diagonal-up-outline.component.svg | 3 + .../icons/badge-check-outline.component.svg | 3 + .../ui/assets/icons/ban-outline.component.svg | 3 + .../icons/briefcase-back-icon.component.svg | 4 + .../icons/briefcase-outline.component.svg | 3 + .../clipboard-check-outline.component.svg | 3 + .../clipboard-copy-outline.component.svg | 3 + .../clipboard-list-outline.component.svg | 3 + .../icons/clipboard-x-outline.component.svg | 4 + .../ui/assets/icons/delegation.component.svg | 4 - .../icons/document-add-outline.component.svg | 3 + .../document-report-outline.component.svg | 3 + .../icons/document-text-outline.component.svg | 3 + .../icons/feather-outline.component.svg | 3 + .../assets/icons/gift-outline.component.svg | 3 + .../identification-outline.component.svg | 3 + .../ui/assets/icons/incoming.component.svg | 4 - .../assets/icons/info-outline.component.svg | 3 + .../ui/assets/icons/outgoing.component.svg | 4 - .../icons/receipt-right-outline.component.svg | 3 + .../icons/refresh-outline.component.svg | 3 + .../icons/self-transaction.component.svg | 4 - .../icons/terminal-outile.component.svg | 3 + .../ticket-arrow-right-outline.component.svg | 3 + .../icons/ticket-forward-icon.component.svg | 3 + .../icons/ticket-outline-icon.component.svg | 6 + .../components/Activity/AssetActivityItem.tsx | 54 +- .../Activity/AssetActivityList.stories.tsx | 58 ++ .../__tests__/AssetActivityItem.test.tsx | 5 +- .../__tests__/AssetActivityList.test.tsx | 3 +- .../GroupedAssetActivityList.test.tsx | 3 +- .../ActivityDetailHeader.module.scss | 2 +- .../ActivityTypeIcon.module.scss | 34 + .../ActivityDetail/ActivityTypeIcon.tsx | 110 ++- .../TransactionDetails.module.scss | 24 +- .../TransactionDetails.stories.tsx | 918 ++++++++++++++++++ .../ActivityDetail/TransactionDetails.tsx | 316 +++++- .../ActivityDetail/TransactionFee.module.scss | 107 ++ .../TransactionInputOutput.module.scss | 36 +- .../ActivityDetail/TxDetailsList.tsx | 83 ++ .../components/DetailRow.module.scss | 45 + .../ActivityDetail/components/DetailRow.tsx | 31 + .../components/DetailRowSubitems.tsx | 16 + .../ActivityDetail/components/DetailRows.tsx | 44 + .../ActivityDetail/components/InfoItem.tsx | 19 + .../ActivityDetail/components/index.ts | 1 + .../src/ui/components/ActivityDetail/index.ts | 1 + .../src/ui/components/ActivityDetail/types.ts | 155 +++ .../ConfirmDRepRegistration.stories.tsx | 64 ++ .../ConfirmDRepRegistration.tsx | 65 ++ .../ConfirmDRepRegistration/index.ts | 1 + .../ConfirmDRepRetirement.stories.tsx | 60 ++ .../ConfirmDRepRetirement.tsx | 52 + .../components/ConfirmDRepRetirement/index.ts | 1 + .../ConfirmDRepUpdate.stories.tsx | 72 ++ .../ConfirmDRepUpdate/ConfirmDRepUpdate.tsx | 57 ++ .../ui/components/ConfirmDRepUpdate/index.ts | 1 + ...irmStakeRegistrationDelegation.stories.tsx | 62 ++ .../ConfirmStakeRegistrationDelegation.tsx | 54 ++ .../index.ts | 1 + .../ConfirmStakeVoteDelegation.stories.tsx | 91 ++ .../ConfirmStakeVoteDelegation.tsx | 66 ++ .../ConfirmStakeVoteDelegation/index.ts | 1 + ...takeVoteRegistrationDelegation.stories.tsx | 93 ++ ...ConfirmStakeVoteRegistrationDelegation.tsx | 76 ++ .../index.ts | 1 + .../ConfirmVoteDelegation.stories.tsx | 83 ++ .../ConfirmVoteDelegation.tsx | 68 ++ .../components/ConfirmVoteDelegation/index.ts | 1 + ...firmVoteRegistrationDelegation.stories.tsx | 91 ++ .../ConfirmVoteRegistrationDelegation.tsx | 71 ++ .../index.ts | 1 + .../DappTransaction.stories.tsx | 54 ++ .../DappTransaction/DappTransaction.tsx | 23 +- .../DappTxAsset/DappTxAsset.tsx | 3 +- .../DappTransaction/DappTxAsset/index.ts | 1 + .../DappTxOutput/DappTxOutput.tsx | 4 +- .../InsufficientFundsWarning.module.scss | 23 + .../InsufficientFundsWarning.tsx | 15 + .../InsufficientFundsWarning/index.ts | 1 + .../HardForkInitiationAction.stories.tsx | 98 ++ .../HardForkInitiationAction.tsx | 62 ++ .../HardForkInitiationActionTypes.ts | 25 + .../HardForkInitiationAction/index.ts | 1 + .../InfoAction/InfoAction.stories.tsx | 75 ++ .../InfoAction/InfoAction.tsx | 40 + .../InfoAction/InfoActionTypes.ts | 11 + .../ProposalProcedures/InfoAction/index.ts | 1 + .../NewConstitutionAction.stories.tsx | 103 ++ .../NewConstitutionAction.tsx | 63 ++ .../NewConstitutionActionTypes.ts | 30 + .../NewConstitutionAction/index.ts | 1 + .../NoConfidenceAction.stories.tsx | 88 ++ .../NoConfidenceAction/NoConfidenceAction.tsx | 51 + .../NoConfidenceActionTypes.ts | 15 + .../NoConfidenceAction/index.ts | 1 + .../ParameterChangeAction/EconomicGroup.tsx | 84 ++ .../ParameterChangeAction/GovernanceGroup.tsx | 144 +++ .../ParameterChangeAction/NetworkGroup.tsx | 59 ++ .../ParameterChangeAction.stories.tsx | 243 +++++ .../ParameterChangeAction.tsx | 91 ++ .../ParameterChangeActionTypes.ts | 185 ++++ .../ParameterChangeAction/TechnicalGroup.tsx | 58 ++ .../ParameterChangeAction/index.ts | 1 + .../TreasuryWithdrawalsAction.stories.tsx | 99 ++ .../TreasuryWithdrawalsAction.tsx | 71 ++ .../TreasuryWithdrawalsActionTypes.ts | 24 + .../TreasuryWithdrawalsAction/index.ts | 1 + .../UpdateCommitteeActionAction.stories.tsx | 130 +++ .../UpdateCommitteeActionAction.tsx | 97 ++ .../UpdateCommitteeActionTypes.ts | 50 + .../UpdateCommitteeAction/index.ts | 1 + .../components/ActionId.tsx | 30 + .../components/ActionIdTypes.ts | 10 + .../components/Card.module.scss | 3 + .../ProposalProcedures/components/Card.tsx | 94 ++ .../components/Procedure.tsx | 32 + .../components/ProcedureTypes.ts | 15 + .../components/TransactionDetails.tsx | 37 + .../components/TransactionDetailsTypes.ts | 12 + .../ui/components/ProposalProcedures/index.ts | 7 + .../VotingProcedures.stories.tsx | 121 +++ .../VotingProcedures/VotingProcedures.tsx | 135 +++ .../ui/components/VotingProcedures/index.ts | 1 + packages/core/src/ui/lib/translations/en.json | 313 +++++- .../src/assert/dAppConnectorAssert.ts | 2 +- .../DelegationCard/DelegationTooltip.css.ts | 15 + .../Drawer/confirmation/AmountInfo.tsx | 8 +- .../src/features/overview/Overview.tsx | 2 +- .../src/features/overview/OverviewPopup.tsx | 2 +- .../hasPendingDelegationTransaction.ts | 10 +- packages/ui/package.json | 12 + packages/ui/src/design-system/index.ts | 2 +- .../ui/src/design-system/metadata/index.ts | 1 + .../metadata/metadata-link.component.tsx | 31 + .../design-system/metadata/metadata.base.tsx | 47 + .../metadata/metadata.component.tsx | 29 +- .../metadata/metadata.stories.tsx | 1 + .../design-system/text-link/text-link.css.ts | 1 + .../tooltip/tooltip-content.css.ts | 1 + .../transaction-summary-address.component.tsx | 14 +- ...transaction-summary-metadata.component.tsx | 13 +- rollup.config.js | 2 +- yarn.lock | 107 +- 253 files changed, 15605 insertions(+), 634 deletions(-) create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/DappError.tsx delete mode 100644 apps/browser-extension-wallet/src/features/dapp/components/NoWallet.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmDRepRegistrationContainer.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmDRepRetirementContainer.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmDRepUpdateContainer.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmStakeRegistrationDelegationContainer.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmStakeVoteDelegationContainer.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmStakeVoteRegistrationDelegationContainer.tsx rename apps/browser-extension-wallet/src/features/dapp/components/{ => confirm-transaction}/ConfirmTransaction.module.scss (56%) create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransactionContent.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmVoteDelegationContainer.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmVoteRegistrationDelegationContainer.tsx rename apps/browser-extension-wallet/src/features/dapp/components/{ConfirmTransaction.tsx => confirm-transaction/DappTransactionContainer.tsx} (58%) create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/NonRegisteredUserModal/NonRegisteredUserModal.module.scss create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/NonRegisteredUserModal/NonRegisteredUserModal.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ProposalProceduresContainer.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/VotingProceduresContainer.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmDRepRegistrationContainer.test.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmDRepRetirementContainer.test.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmDRepUpdateContainer.test.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmStakeRegistrationDelegationContainer.test.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmStakeVoteDelegationContainer.test.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmStakeVoteRegistrationDelegationContainer.test.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmTransaction.test.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmTransactionContent.test.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmVoteDelegationContainer.test.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmVoteRegistrationDelegationContainer.test.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/DappTransactionContainer.test.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ProposalProceduresContainer.test.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/VotingProceduresContainer.test.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/hooks.test.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/utils.test.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/hooks.ts create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/HardForkInitiationActionContainer.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/InfoActionContainer.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/NewConstitutionActionContainer.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/NoConfidenceActionContainer.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/ParameterChangeActionContainer.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/TreasuryWithdrawalsActionContainer.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/UpdateCommitteeActionContainer.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/HardForkInitiationActionContainer.test.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/InfoActionContainer.test.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/NewConstitutionActionContainer.test.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/NoConfidenceActionContainer.test.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/ParameterChangeActionContainer.test.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/TreasuryWithdrawalsActionContainer.test.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/UpdateCommitteeActionContainer.test.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/testing.utils.tsx create mode 100644 apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/types.ts create mode 100644 apps/browser-extension-wallet/src/stores/slices/__tests__/activity-detail-slice.test.ts create mode 100644 apps/browser-extension-wallet/src/utils/mocks/certificates.ts create mode 100644 apps/browser-extension-wallet/src/utils/mocks/governance.ts create mode 100644 apps/browser-extension-wallet/src/utils/mocks/raw-transactions.ts create mode 100644 packages/core/.storybook/__mocks__/cardano.ts create mode 100644 packages/core/src/ui/assets/icons/arrow-diagonal-down-outline.component.svg create mode 100644 packages/core/src/ui/assets/icons/arrow-diagonal-up-outline.component.svg create mode 100644 packages/core/src/ui/assets/icons/badge-check-outline.component.svg create mode 100644 packages/core/src/ui/assets/icons/ban-outline.component.svg create mode 100644 packages/core/src/ui/assets/icons/briefcase-back-icon.component.svg create mode 100644 packages/core/src/ui/assets/icons/briefcase-outline.component.svg create mode 100644 packages/core/src/ui/assets/icons/clipboard-check-outline.component.svg create mode 100644 packages/core/src/ui/assets/icons/clipboard-copy-outline.component.svg create mode 100644 packages/core/src/ui/assets/icons/clipboard-list-outline.component.svg create mode 100644 packages/core/src/ui/assets/icons/clipboard-x-outline.component.svg delete mode 100644 packages/core/src/ui/assets/icons/delegation.component.svg create mode 100644 packages/core/src/ui/assets/icons/document-add-outline.component.svg create mode 100644 packages/core/src/ui/assets/icons/document-report-outline.component.svg create mode 100644 packages/core/src/ui/assets/icons/document-text-outline.component.svg create mode 100644 packages/core/src/ui/assets/icons/feather-outline.component.svg create mode 100644 packages/core/src/ui/assets/icons/gift-outline.component.svg create mode 100644 packages/core/src/ui/assets/icons/identification-outline.component.svg delete mode 100644 packages/core/src/ui/assets/icons/incoming.component.svg create mode 100644 packages/core/src/ui/assets/icons/info-outline.component.svg delete mode 100644 packages/core/src/ui/assets/icons/outgoing.component.svg create mode 100644 packages/core/src/ui/assets/icons/receipt-right-outline.component.svg create mode 100644 packages/core/src/ui/assets/icons/refresh-outline.component.svg delete mode 100644 packages/core/src/ui/assets/icons/self-transaction.component.svg create mode 100644 packages/core/src/ui/assets/icons/terminal-outile.component.svg create mode 100644 packages/core/src/ui/assets/icons/ticket-arrow-right-outline.component.svg create mode 100644 packages/core/src/ui/assets/icons/ticket-forward-icon.component.svg create mode 100644 packages/core/src/ui/assets/icons/ticket-outline-icon.component.svg create mode 100644 packages/core/src/ui/components/Activity/AssetActivityList.stories.tsx create mode 100644 packages/core/src/ui/components/ActivityDetail/ActivityTypeIcon.module.scss create mode 100644 packages/core/src/ui/components/ActivityDetail/TransactionDetails.stories.tsx create mode 100644 packages/core/src/ui/components/ActivityDetail/TransactionFee.module.scss create mode 100644 packages/core/src/ui/components/ActivityDetail/TxDetailsList.tsx create mode 100644 packages/core/src/ui/components/ActivityDetail/components/DetailRow.module.scss create mode 100644 packages/core/src/ui/components/ActivityDetail/components/DetailRow.tsx create mode 100644 packages/core/src/ui/components/ActivityDetail/components/DetailRowSubitems.tsx create mode 100644 packages/core/src/ui/components/ActivityDetail/components/DetailRows.tsx create mode 100644 packages/core/src/ui/components/ActivityDetail/components/InfoItem.tsx create mode 100644 packages/core/src/ui/components/ActivityDetail/components/index.ts create mode 100644 packages/core/src/ui/components/ActivityDetail/types.ts create mode 100644 packages/core/src/ui/components/ConfirmDRepRegistration/ConfirmDRepRegistration.stories.tsx create mode 100644 packages/core/src/ui/components/ConfirmDRepRegistration/ConfirmDRepRegistration.tsx create mode 100644 packages/core/src/ui/components/ConfirmDRepRegistration/index.ts create mode 100644 packages/core/src/ui/components/ConfirmDRepRetirement/ConfirmDRepRetirement.stories.tsx create mode 100644 packages/core/src/ui/components/ConfirmDRepRetirement/ConfirmDRepRetirement.tsx create mode 100644 packages/core/src/ui/components/ConfirmDRepRetirement/index.ts create mode 100644 packages/core/src/ui/components/ConfirmDRepUpdate/ConfirmDRepUpdate.stories.tsx create mode 100644 packages/core/src/ui/components/ConfirmDRepUpdate/ConfirmDRepUpdate.tsx create mode 100644 packages/core/src/ui/components/ConfirmDRepUpdate/index.ts create mode 100644 packages/core/src/ui/components/ConfirmStakeRegistrationDelegation/ConfirmStakeRegistrationDelegation.stories.tsx create mode 100644 packages/core/src/ui/components/ConfirmStakeRegistrationDelegation/ConfirmStakeRegistrationDelegation.tsx create mode 100644 packages/core/src/ui/components/ConfirmStakeRegistrationDelegation/index.ts create mode 100644 packages/core/src/ui/components/ConfirmStakeVoteDelegation/ConfirmStakeVoteDelegation.stories.tsx create mode 100644 packages/core/src/ui/components/ConfirmStakeVoteDelegation/ConfirmStakeVoteDelegation.tsx create mode 100644 packages/core/src/ui/components/ConfirmStakeVoteDelegation/index.ts create mode 100644 packages/core/src/ui/components/ConfirmStakeVoteRegistrationDelegation/ConfirmStakeVoteRegistrationDelegation.stories.tsx create mode 100644 packages/core/src/ui/components/ConfirmStakeVoteRegistrationDelegation/ConfirmStakeVoteRegistrationDelegation.tsx create mode 100644 packages/core/src/ui/components/ConfirmStakeVoteRegistrationDelegation/index.ts create mode 100644 packages/core/src/ui/components/ConfirmVoteDelegation/ConfirmVoteDelegation.stories.tsx create mode 100644 packages/core/src/ui/components/ConfirmVoteDelegation/ConfirmVoteDelegation.tsx create mode 100644 packages/core/src/ui/components/ConfirmVoteDelegation/index.ts create mode 100644 packages/core/src/ui/components/ConfirmVoteRegistrationDelegation/ConfirmVoteRegistrationDelegation.stories.tsx create mode 100644 packages/core/src/ui/components/ConfirmVoteRegistrationDelegation/ConfirmVoteRegistrationDelegation.tsx create mode 100644 packages/core/src/ui/components/ConfirmVoteRegistrationDelegation/index.ts create mode 100644 packages/core/src/ui/components/DappTransaction/DappTransaction.stories.tsx create mode 100644 packages/core/src/ui/components/DappTransaction/DappTxAsset/index.ts create mode 100644 packages/core/src/ui/components/InsufficientFundsWarning/InsufficientFundsWarning.module.scss create mode 100644 packages/core/src/ui/components/InsufficientFundsWarning/InsufficientFundsWarning.tsx create mode 100644 packages/core/src/ui/components/InsufficientFundsWarning/index.ts create mode 100644 packages/core/src/ui/components/ProposalProcedures/HardForkInitiationAction/HardForkInitiationAction.stories.tsx create mode 100644 packages/core/src/ui/components/ProposalProcedures/HardForkInitiationAction/HardForkInitiationAction.tsx create mode 100644 packages/core/src/ui/components/ProposalProcedures/HardForkInitiationAction/HardForkInitiationActionTypes.ts create mode 100644 packages/core/src/ui/components/ProposalProcedures/HardForkInitiationAction/index.ts create mode 100644 packages/core/src/ui/components/ProposalProcedures/InfoAction/InfoAction.stories.tsx create mode 100644 packages/core/src/ui/components/ProposalProcedures/InfoAction/InfoAction.tsx create mode 100644 packages/core/src/ui/components/ProposalProcedures/InfoAction/InfoActionTypes.ts create mode 100644 packages/core/src/ui/components/ProposalProcedures/InfoAction/index.ts create mode 100644 packages/core/src/ui/components/ProposalProcedures/NewConstitutionAction/NewConstitutionAction.stories.tsx create mode 100644 packages/core/src/ui/components/ProposalProcedures/NewConstitutionAction/NewConstitutionAction.tsx create mode 100644 packages/core/src/ui/components/ProposalProcedures/NewConstitutionAction/NewConstitutionActionTypes.ts create mode 100644 packages/core/src/ui/components/ProposalProcedures/NewConstitutionAction/index.ts create mode 100644 packages/core/src/ui/components/ProposalProcedures/NoConfidenceAction/NoConfidenceAction.stories.tsx create mode 100644 packages/core/src/ui/components/ProposalProcedures/NoConfidenceAction/NoConfidenceAction.tsx create mode 100644 packages/core/src/ui/components/ProposalProcedures/NoConfidenceAction/NoConfidenceActionTypes.ts create mode 100644 packages/core/src/ui/components/ProposalProcedures/NoConfidenceAction/index.ts create mode 100644 packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/EconomicGroup.tsx create mode 100644 packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/GovernanceGroup.tsx create mode 100644 packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/NetworkGroup.tsx create mode 100644 packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/ParameterChangeAction.stories.tsx create mode 100644 packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/ParameterChangeAction.tsx create mode 100644 packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/ParameterChangeActionTypes.ts create mode 100644 packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/TechnicalGroup.tsx create mode 100644 packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/index.ts create mode 100644 packages/core/src/ui/components/ProposalProcedures/TreasuryWithdrawalsAction/TreasuryWithdrawalsAction.stories.tsx create mode 100644 packages/core/src/ui/components/ProposalProcedures/TreasuryWithdrawalsAction/TreasuryWithdrawalsAction.tsx create mode 100644 packages/core/src/ui/components/ProposalProcedures/TreasuryWithdrawalsAction/TreasuryWithdrawalsActionTypes.ts create mode 100644 packages/core/src/ui/components/ProposalProcedures/TreasuryWithdrawalsAction/index.ts create mode 100644 packages/core/src/ui/components/ProposalProcedures/UpdateCommitteeAction/UpdateCommitteeActionAction.stories.tsx create mode 100644 packages/core/src/ui/components/ProposalProcedures/UpdateCommitteeAction/UpdateCommitteeActionAction.tsx create mode 100644 packages/core/src/ui/components/ProposalProcedures/UpdateCommitteeAction/UpdateCommitteeActionTypes.ts create mode 100644 packages/core/src/ui/components/ProposalProcedures/UpdateCommitteeAction/index.ts create mode 100644 packages/core/src/ui/components/ProposalProcedures/components/ActionId.tsx create mode 100644 packages/core/src/ui/components/ProposalProcedures/components/ActionIdTypes.ts create mode 100644 packages/core/src/ui/components/ProposalProcedures/components/Card.module.scss create mode 100644 packages/core/src/ui/components/ProposalProcedures/components/Card.tsx create mode 100644 packages/core/src/ui/components/ProposalProcedures/components/Procedure.tsx create mode 100644 packages/core/src/ui/components/ProposalProcedures/components/ProcedureTypes.ts create mode 100644 packages/core/src/ui/components/ProposalProcedures/components/TransactionDetails.tsx create mode 100644 packages/core/src/ui/components/ProposalProcedures/components/TransactionDetailsTypes.ts create mode 100644 packages/core/src/ui/components/ProposalProcedures/index.ts create mode 100644 packages/core/src/ui/components/VotingProcedures/VotingProcedures.stories.tsx create mode 100644 packages/core/src/ui/components/VotingProcedures/VotingProcedures.tsx create mode 100644 packages/core/src/ui/components/VotingProcedures/index.ts create mode 100644 packages/staking/src/features/DelegationCard/DelegationTooltip.css.ts create mode 100644 packages/ui/src/design-system/metadata/metadata-link.component.tsx create mode 100644 packages/ui/src/design-system/metadata/metadata.base.tsx diff --git a/apps/browser-extension-wallet/.env.defaults b/apps/browser-extension-wallet/.env.defaults index f31c60662..515bfb0f8 100644 --- a/apps/browser-extension-wallet/.env.defaults +++ b/apps/browser-extension-wallet/.env.defaults @@ -61,22 +61,25 @@ PRODUCTION_MODE_TRACKING=false POSTHOG_DEV_TOKEN_MAINNET=phc_gH96Lx5lEVXTTWEyytSdTFPDk3Xsxwi4BqG88mKObd1 POSTHOG_DEV_TOKEN_PREPROD=phc_Xlmldm6EYSfQVgB9Uxm3b2xC1noDlgFFXpF9AJ6SMfJ POSTHOG_DEV_TOKEN_PREVIEW=phc_e8SaOOWpXpNE59TnpLumeUjWm4iv024AWjhQqU406jr +POSTHOG_DEV_TOKEN_SANCHONET=phc_OUu6sPucDu5S6skRmYbWN5Jn8TpggWTQu1Y1ETkm3xt # Cardano Services CARDANO_SERVICES_URL_MAINNET=https://dev-mainnet.lw.iog.io CARDANO_SERVICES_URL_PREPROD=https://dev-preprod.lw.iog.io CARDANO_SERVICES_URL_PREVIEW=https://dev-preview.lw.iog.io +CARDANO_SERVICES_URL_SANCHONET=https://backend.dev-sanchonet.eks.lw.iog.io # Explorer URLs CEXPLORER_URL_MAINNET=https://cexplorer.io CEXPLORER_URL_PREVIEW=https://preview.cexplorer.io CEXPLORER_URL_PREPROD=https://preprod.cexplorer.io -CEXPLORER_URL_SANCHONET=https://sanchonet.cexplorer.io +CEXPLORER_URL_SANCHONET=https://sancho.cexplorer.io # ADA Handle URLs ADA_HANDLE_URL_MAINNET=https://api.handle.me ADA_HANDLE_URL_PREVIEW=https://preview.api.handle.me ADA_HANDLE_URL_PREPROD=https://preprod.api.handle.me +ADA_HANDLE_URL_SANCHONET= # Manifest.json LACE_EXTENSION_KEY=gafhhkghbfjjkeiendhlofajokpaflmk diff --git a/apps/browser-extension-wallet/.env.developerpreview b/apps/browser-extension-wallet/.env.developerpreview index 80566178a..6cbc73171 100644 --- a/apps/browser-extension-wallet/.env.developerpreview +++ b/apps/browser-extension-wallet/.env.developerpreview @@ -60,12 +60,13 @@ POSTHOG_DEV_TOKEN_PREVIEW=phc_e8SaOOWpXpNE59TnpLumeUjWm4iv024AWjhQqU406jr CARDANO_SERVICES_URL_MAINNET=https://dev-mainnet.lw.iog.io CARDANO_SERVICES_URL_PREPROD=https://dev-preprod.lw.iog.io CARDANO_SERVICES_URL_PREVIEW=https://dev-preview.lw.iog.io +CARDANO_SERVICES_URL_SANCHONET=https://backend.dev-sanchonet.eks.lw.iog.io # Explorer URLs CEXPLORER_URL_MAINNET=https://cexplorer.io CEXPLORER_URL_PREVIEW=https://preview.cexplorer.io CEXPLORER_URL_PREPROD=https://preprod.cexplorer.io -CEXPLORER_URL_SANCHONET=https://sanchonet.cexplorer.io +CEXPLORER_URL_SANCHONET=https://sancho.cexplorer.io # ADA Handle URLs ADA_HANDLE_URL_MAINNET=https://api.handle.me diff --git a/apps/browser-extension-wallet/.env.example b/apps/browser-extension-wallet/.env.example index 740202ad9..2e963b613 100644 --- a/apps/browser-extension-wallet/.env.example +++ b/apps/browser-extension-wallet/.env.example @@ -59,22 +59,25 @@ POSTHOG_PRODUCTION_TOKEN_PREVIEW=production-preview-token POSTHOG_DEV_TOKEN_MAINNET=dev-mainnet-token POSTHOG_DEV_TOKEN_PREPROD=dev-preprod-token POSTHOG_DEV_TOKEN_PREVIEW=dev-preview-token +POSTHOG_DEV_TOKEN_SANCHONET=dev-sanchonet-token # Cardano Services CARDANO_SERVICES_URL_MAINNET=https://backend.live-mainnet.eks.lw.iog.io CARDANO_SERVICES_URL_PREPROD=https://backend.live-preprod.eks.lw.iog.io CARDANO_SERVICES_URL_PREVIEW=https://backend.live-preview.eks.lw.iog.io +CARDANO_SERVICES_URL_SANCHONET=https://backend.dev-sanchonet.eks.lw.iog.io # Explorer URLs CEXPLORER_URL_MAINNET=https://cexplorer.io CEXPLORER_URL_PREVIEW=https://preview.cexplorer.io CEXPLORER_URL_PREPROD=https://preprod.cexplorer.io -CEXPLORER_URL_TESTNET=https://testnet.cexplorer.io +CEXPLORER_URL_SANCHONET=https://sancho.cexplorer.io # ADA Handle URLs ADA_HANDLE_URL_MAINNET=https://api.handle.me ADA_HANDLE_URL_PREVIEW=https://preview.api.handle.me ADA_HANDLE_URL_PREPROD=https://preprod.api.handle.me +ADA_HANDLE_URL_SANCHONET= # Manifest.json LACE_EXTENSION_KEY=gafhhkghbfjjkeiendhlofajokpaflmk diff --git a/apps/browser-extension-wallet/src/config.ts b/apps/browser-extension-wallet/src/config.ts index fd976f743..3e6ad690f 100644 --- a/apps/browser-extension-wallet/src/config.ts +++ b/apps/browser-extension-wallet/src/config.ts @@ -45,7 +45,7 @@ const envChecks = (chosenChain: Wallet.ChainName): void => { throw new Error('no available chains to connect to'); } - if (!process.env.AVAILABLE_CHAINS.includes('Mainnet')) { + if (!process.env.AVAILABLE_CHAINS.includes('Mainnet') && process.env.DEFAULT_CHAIN !== 'Sanchonet') { throw new Error('mainnet chain not available in env'); } @@ -78,12 +78,14 @@ export const config = (): Config => { CARDANO_SERVICES_URLS: { Mainnet: process.env.CARDANO_SERVICES_URL_MAINNET, Preprod: process.env.CARDANO_SERVICES_URL_PREPROD, - Preview: process.env.CARDANO_SERVICES_URL_PREVIEW + Preview: process.env.CARDANO_SERVICES_URL_PREVIEW, + Sanchonet: process.env.CARDANO_SERVICES_URL_SANCHONET }, CEXPLORER_BASE_URL: { Mainnet: `${process.env.CEXPLORER_URL_MAINNET}`, Preprod: `${process.env.CEXPLORER_URL_PREPROD}`, - Preview: `${process.env.CEXPLORER_URL_PREVIEW}` + Preview: `${process.env.CEXPLORER_URL_PREVIEW}`, + Sanchonet: `${process.env.CEXPLORER_URL_SANCHONET}` }, CEXPLORER_URL_PATHS: { Tx: 'tx', diff --git a/apps/browser-extension-wallet/src/dapp-connector.tsx b/apps/browser-extension-wallet/src/dapp-connector.tsx index 1e40027f4..4c9e2fea3 100644 --- a/apps/browser-extension-wallet/src/dapp-connector.tsx +++ b/apps/browser-extension-wallet/src/dapp-connector.tsx @@ -8,6 +8,7 @@ import { CurrencyStoreProvider } from '@providers/currency'; import { DatabaseProvider, AxiosClientProvider, AppSettingsProvider, AnalyticsProvider } from '@providers'; import { HashRouter } from 'react-router-dom'; import { ThemeProvider } from '@providers/ThemeProvider'; +import { UIThemeProvider } from '@providers/UIThemeProvider'; import { BackgroundServiceAPIProvider } from '@providers/BackgroundServiceAPI'; import { APP_MODE_POPUP } from './utils/constants'; import { PostHogClientProvider } from '@providers/PostHogClientProvider'; @@ -27,7 +28,9 @@ const App = (): React.ReactElement => ( - + + + diff --git a/apps/browser-extension-wallet/src/features/address-book/context/AddressBookProvider.tsx b/apps/browser-extension-wallet/src/features/address-book/context/AddressBookProvider.tsx index 65fea4797..c1604d2c9 100644 --- a/apps/browser-extension-wallet/src/features/address-book/context/AddressBookProvider.tsx +++ b/apps/browser-extension-wallet/src/features/address-book/context/AddressBookProvider.tsx @@ -18,7 +18,8 @@ export type AddressRecordParams = Pick { diff --git a/apps/browser-extension-wallet/src/features/dapp/components/DappError.tsx b/apps/browser-extension-wallet/src/features/dapp/components/DappError.tsx new file mode 100644 index 000000000..042c74a5a --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/DappError.tsx @@ -0,0 +1,53 @@ +import React, { ReactNode, useCallback } from 'react'; +import { Image } from 'antd'; +import { useTranslation } from 'react-i18next'; +import Empty from '../../../assets/icons/empty.svg'; +import styles from './Layout.module.scss'; +import { Button } from '@lace/common'; + +type DappErrorProps = { + title: string; + description: ReactNode; + closeButtonLabel?: string; + onCloseClick?: () => void; + containerTestId: string; + imageTestId: string; + titleTestId: string; + descriptionTestId: string; + closeButtonTestId: string; +}; +export const DappError = ({ + title, + description, + closeButtonLabel, + onCloseClick, + containerTestId, + imageTestId, + titleTestId, + descriptionTestId, + closeButtonTestId +}: DappErrorProps): React.ReactElement => { + const { t } = useTranslation(); + const handleClose = useCallback(() => { + onCloseClick?.(); + }, [onCloseClick]); + + return ( +
+
+ +
+ {title} +
+
+ {description} +
+
+
+ +
+
+ ); +}; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/DappTransactionFail.tsx b/apps/browser-extension-wallet/src/features/dapp/components/DappTransactionFail.tsx index 963bc3cda..2db3ec9a9 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/DappTransactionFail.tsx +++ b/apps/browser-extension-wallet/src/features/dapp/components/DappTransactionFail.tsx @@ -24,8 +24,8 @@ export const DappTransactionFail = (): React.ReactElement => { }, [analytics]); return ( -
-
+
+
{t('dapp.sign.failure.title')} diff --git a/apps/browser-extension-wallet/src/features/dapp/components/DappTransactionSuccess.tsx b/apps/browser-extension-wallet/src/features/dapp/components/DappTransactionSuccess.tsx index c69b4bf66..42650cdf2 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/DappTransactionSuccess.tsx +++ b/apps/browser-extension-wallet/src/features/dapp/components/DappTransactionSuccess.tsx @@ -25,8 +25,8 @@ export const DappTransactionSuccess = (): React.ReactElement => { }, [analytics]); return ( -
-
+
+
{t('browserView.transaction.success.youCanSafelyCloseThisPanel')} diff --git a/apps/browser-extension-wallet/src/features/dapp/components/Layout.module.scss b/apps/browser-extension-wallet/src/features/dapp/components/Layout.module.scss index edad913a6..58262eb76 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/Layout.module.scss +++ b/apps/browser-extension-wallet/src/features/dapp/components/Layout.module.scss @@ -26,7 +26,7 @@ padding-top: size_unit(4); } -.noWalletContainer { +.dappErrorContainer { align-items: center; display: flex; flex-direction: column; @@ -34,7 +34,7 @@ justify-content: space-between; width: 100%; - .noWalletContent { + .dappErrorContent { padding: 0 size_unit(3); display: flex; flex: 1; @@ -45,7 +45,7 @@ .heading { color: var(--text-color-secondary); font-size: var(--bodyLarge); - font-weight: 800; + font-weight: 600; letter-spacing: 0.02em; line-height: size_unit(3); margin-top: size_unit(2); @@ -55,6 +55,7 @@ .description { color: var(--text-color-secondary); font-size: var(--bodySmall); + font-weight: 500; letter-spacing: 0.02em; line-height: size_unit(3); margin-top: size_unit(2); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/NoWallet.tsx b/apps/browser-extension-wallet/src/features/dapp/components/NoWallet.tsx deleted file mode 100644 index 86050bb7e..000000000 --- a/apps/browser-extension-wallet/src/features/dapp/components/NoWallet.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import { Image } from 'antd'; -import { useTranslation } from 'react-i18next'; -import Empty from '../../../assets/icons/empty.svg'; -import styles from './Layout.module.scss'; -import { Button } from '@lace/common'; -import { tabs } from 'webextension-polyfill'; - -const openCreatePage = () => { - tabs.create({ url: 'app.html#/setup' }); - window.close(); -}; - -export const NoWallet = (): React.ReactElement => { - const { t } = useTranslation(); - - return ( -
-
- -
- {t('dapp.noWallet.heading')} -
-
- {t('dapp.noWallet.description')} -
-
-
- -
-
- ); -}; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/collateral/CreateCollateral.tsx b/apps/browser-extension-wallet/src/features/dapp/components/collateral/CreateCollateral.tsx index 7fe3a76c7..a979c6a80 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/collateral/CreateCollateral.tsx +++ b/apps/browser-extension-wallet/src/features/dapp/components/collateral/CreateCollateral.tsx @@ -129,7 +129,10 @@ export const CreateCollateral = ({ })}
{renderAmountInfo( - `${Wallet.util.lovelacesToAdaString(collateralTx.fee.toString())} ${cardanoCoin.symbol}`, + Wallet.util.getFormattedAmount({ + amount: collateralTx.fee.toString(), + cardanoCoin + }), `${Wallet.util.convertAdaToFiat({ ada: Wallet.util.lovelacesToAdaString(collateralTx.fee.toString()), fiat: priceResult?.cardano?.price || 0 diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmDRepRegistrationContainer.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmDRepRegistrationContainer.tsx new file mode 100644 index 000000000..7cba19e55 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmDRepRegistrationContainer.tsx @@ -0,0 +1,62 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ConfirmDRepRegistration } from '@lace/core'; +import { certificateInspectorFactory, depositPaidWithSymbol, drepIDasBech32FromHash } from './utils'; +import { Wallet } from '@lace/cardano'; +import { useWalletStore } from '@src/stores'; +import { useViewsFlowContext } from '@providers'; +import { Skeleton } from 'antd'; + +const { CertificateType } = Wallet.Cardano; + +export const ConfirmDRepRegistrationContainer = (): React.ReactElement => { + const { t } = useTranslation(); + const { + walletUI: { cardanoCoin } + } = useWalletStore(); + const { + signTxRequest: { request }, + dappInfo + } = useViewsFlowContext(); + + const [certificate, setCertificate] = useState(); + + useEffect(() => { + const getCertificateData = async () => { + const txCertificate = await certificateInspectorFactory( + CertificateType.RegisterDelegateRepresentative + )(request?.transaction.toCore()); + setCertificate(txCertificate); + }; + + getCertificateData(); + }, [request]); + + if (!certificate) { + return ; + } + + const depositPaidWithCardanoSymbol = depositPaidWithSymbol(certificate.deposit, cardanoCoin); + + // TODO: might be changed in scope of https://input-output.atlassian.net/browse/LW-9034 + return ( + + ); +}; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmDRepRetirementContainer.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmDRepRetirementContainer.tsx new file mode 100644 index 000000000..2a146d782 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmDRepRetirementContainer.tsx @@ -0,0 +1,93 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ConfirmDRepRetirement } from '@lace/core'; +import { certificateInspectorFactory, depositPaidWithSymbol, disallowSignTx, drepIDasBech32FromHash } from './utils'; +import { Wallet } from '@lace/cardano'; +import { useWalletStore } from '@src/stores'; +import { useGetOwnPubDRepKeyHash } from './hooks'; +import { Skeleton } from 'antd'; +import { DappError } from '../DappError'; +import { useViewsFlowContext } from '@providers'; + +const { CertificateType } = Wallet.Cardano; + +interface Props { + onError: () => void; +} + +export const ConfirmDRepRetirementContainer = ({ onError }: Props): React.ReactElement => { + const { t } = useTranslation(); + const { + walletUI: { cardanoCoin } + } = useWalletStore(); + const { + signTxRequest: { request }, + dappInfo + } = useViewsFlowContext(); + const [certificate, setCertificate] = useState(); + const { loading: loadingOwnPubDRepKeyHash, ownPubDRepKeyHash } = useGetOwnPubDRepKeyHash(); + + useEffect(() => { + if (!request) return; + const getCertificateData = async () => { + const txCertificate = + await certificateInspectorFactory( + CertificateType.UnregisterDelegateRepresentative + )(request.transaction.toCore()); + setCertificate(txCertificate); + }; + + getCertificateData(); + }, [request]); + + const isNotOwnDRepKey = certificate?.dRepCredential.hash !== ownPubDRepKeyHash; + + useEffect(() => { + if (ownPubDRepKeyHash && certificate && isNotOwnDRepKey) { + disallowSignTx(request, true); + onError(); + } + }, [ownPubDRepKeyHash, isNotOwnDRepKey, onError, request, certificate]); + + const onCloseClick = useCallback(() => { + window.close(); + }, []); + + if (!certificate || loadingOwnPubDRepKeyHash) { + return ; + } + + const depositPaidWithCardanoSymbol = depositPaidWithSymbol(certificate.deposit, cardanoCoin); + + if (isNotOwnDRepKey) { + return ( + + ); + } + + return ( + + ); +}; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmDRepUpdateContainer.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmDRepUpdateContainer.tsx new file mode 100644 index 000000000..691529f97 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmDRepUpdateContainer.tsx @@ -0,0 +1,52 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ConfirmDRepUpdate } from '@lace/core'; +import { certificateInspectorFactory, drepIDasBech32FromHash } from './utils'; +import { Wallet } from '@lace/cardano'; +import { useViewsFlowContext } from '@providers'; +import { Skeleton } from 'antd'; + +const { CertificateType } = Wallet.Cardano; + +export const ConfirmDRepUpdateContainer = (): React.ReactElement => { + const { t } = useTranslation(); + const { + signTxRequest: { request }, + dappInfo + } = useViewsFlowContext(); + const [certificate, setCertificate] = useState(); + + useEffect(() => { + const getCertificateData = async () => { + const txCertificate = await certificateInspectorFactory( + CertificateType.UpdateDelegateRepresentative + )(request.transaction.toCore()); + setCertificate(txCertificate); + }; + + getCertificateData(); + }, [request]); + + if (!certificate) { + return ; + } + + return ( + + ); +}; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmStakeRegistrationDelegationContainer.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmStakeRegistrationDelegationContainer.tsx new file mode 100644 index 000000000..0d991fbf1 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmStakeRegistrationDelegationContainer.tsx @@ -0,0 +1,59 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ConfirmStakeRegistrationDelegation } from '@lace/core'; +import { certificateInspectorFactory, depositPaidWithSymbol } from './utils'; +import { Wallet } from '@lace/cardano'; +import { useWalletStore } from '@src/stores'; +import { useViewsFlowContext } from '@providers'; +import { Skeleton } from 'antd'; + +const { CertificateType, RewardAddress } = Wallet.Cardano; + +export const ConfirmStakeRegistrationDelegationContainer = (): React.ReactElement => { + const { + walletUI: { cardanoCoin }, + currentChain + } = useWalletStore(); + const { t } = useTranslation(); + const { + signTxRequest: { request }, + dappInfo + } = useViewsFlowContext(); + const [certificate, setCertificate] = useState(); + + useEffect(() => { + const getCertificateData = async () => { + const txCertificate = await certificateInspectorFactory( + CertificateType.StakeRegistrationDelegation + )(request.transaction.toCore()); + setCertificate(txCertificate); + }; + + getCertificateData(); + }, [request]); + + if (!certificate) { + return ; + } + const depositPaidWithCardanoSymbol = depositPaidWithSymbol(certificate.deposit, cardanoCoin); + return ( + + ); +}; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmStakeVoteDelegationContainer.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmStakeVoteDelegationContainer.tsx new file mode 100644 index 000000000..df1b8eca7 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmStakeVoteDelegationContainer.tsx @@ -0,0 +1,63 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ConfirmStakeVoteDelegation } from '@lace/core'; +import { certificateInspectorFactory, drepIDasBech32FromHash } from './utils'; +import { Wallet } from '@lace/cardano'; +import { useWalletStore } from '@src/stores'; +import { useViewsFlowContext } from '@providers'; +import { Skeleton } from 'antd'; + +const { CertificateType, RewardAddress } = Wallet.Cardano; + +export const ConfirmStakeVoteDelegationContainer = (): React.ReactElement => { + const { t } = useTranslation(); + const { currentChain } = useWalletStore(); + const { + signTxRequest: { request }, + dappInfo + } = useViewsFlowContext(); + const [certificate, setCertificate] = useState(); + + useEffect(() => { + const getCertificateData = async () => { + const txCertificate = await certificateInspectorFactory( + CertificateType.StakeVoteDelegation + )(request.transaction.toCore()); + setCertificate(txCertificate); + }; + + getCertificateData(); + }, [request]); + + if (!certificate) { + return ; + } + + const dRep = certificate.dRep; + + return ( + + ); +}; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmStakeVoteRegistrationDelegationContainer.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmStakeVoteRegistrationDelegationContainer.tsx new file mode 100644 index 000000000..55fcd6639 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmStakeVoteRegistrationDelegationContainer.tsx @@ -0,0 +1,70 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ConfirmStakeVoteRegistrationDelegation } from '@lace/core'; +import { certificateInspectorFactory, depositPaidWithSymbol, drepIDasBech32FromHash } from './utils'; +import { Wallet } from '@lace/cardano'; +import { useWalletStore } from '@src/stores'; +import { useViewsFlowContext } from '@providers'; +import { Skeleton } from 'antd'; + +const { CertificateType, RewardAddress } = Wallet.Cardano; + +export const ConfirmStakeVoteRegistrationDelegationContainer = (): React.ReactElement => { + const { t } = useTranslation(); + const { + walletUI: { cardanoCoin }, + currentChain + } = useWalletStore(); + const { + signTxRequest: { request }, + dappInfo + } = useViewsFlowContext(); + const [certificate, setCertificate] = useState(); + + useEffect(() => { + const getCertificateData = async () => { + const txCertificate = + await certificateInspectorFactory( + CertificateType.StakeVoteRegistrationDelegation + )(request.transaction.toCore()); + setCertificate(txCertificate); + }; + + getCertificateData(); + }, [request]); + + if (!certificate) { + return ; + } + + const dRep = certificate.dRep; + const depositPaidWithCardanoSymbol = depositPaidWithSymbol(certificate.deposit, cardanoCoin); + + return ( + + ); +}; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/ConfirmTransaction.module.scss b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.module.scss similarity index 56% rename from apps/browser-extension-wallet/src/features/dapp/components/ConfirmTransaction.module.scss rename to apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.module.scss index cfac6eebe..3376b96ac 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/ConfirmTransaction.module.scss +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.module.scss @@ -1,28 +1,25 @@ -@import '../../../../../../packages/common/src/ui/styles/theme.scss'; -@import '../../../../src/styles/rules/flex.scss'; - -.actions { - display: flex; - gap: size_unit(1); - justify-content: space-evenly; - flex-direction: column; -} +@import '../../../../../../../packages/common/src/ui/styles/theme.scss'; +@import '../../../../../src/styles/rules/flex.scss'; .spaceBetween { justify-content: space-between; padding-top: size_unit(2); } +.layoutError { + padding: 0; +} + .actions { - background-color: var(--bg-color-body); @extend %flex-column; - justify-content: center; + background-color: var(--bg-color-body); gap: size_unit(1); padding: size_unit(2) size_unit(3) size_unit(2) size_unit(3); border-top: 2px solid var(--light-mode-light-grey-plus, var(--dark-mode-mid-grey)); - margin: 0 size_unit(-3) size_unit(-2) size_unit(-3); + margin: size_unit(4) size_unit(-3) size_unit(-2) size_unit(-3); position: sticky; bottom: 0; + z-index: 10; .actionBtn { width: 100%; } 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 new file mode 100644 index 000000000..a49bcd69a --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransaction.tsx @@ -0,0 +1,125 @@ +import React, { useEffect, useState } from 'react'; +import cn from 'classnames'; +import { Button, PostHogAction } from '@lace/common'; +import { useTranslation } from 'react-i18next'; +import { Layout } from '../Layout'; +import { useViewsFlowContext } from '@providers/ViewFlowProvider'; +import styles from './ConfirmTransaction.module.scss'; +import { Wallet } from '@lace/cardano'; +import { useWalletStore } from '@stores'; +import { useDisallowSignTx, useSignWithHardwareWallet, useOnBeforeUnload } from './hooks'; +import { getTxType } from './utils'; +import { ConfirmTransactionContent } from './ConfirmTransactionContent'; +import { TX_CREATION_TYPE_KEY, TxCreationType } from '@providers/AnalyticsProvider/analyticsTracker'; +import { txSubmitted$ } from '@providers/AnalyticsProvider/onChain'; +import { useAnalyticsContext } from '@providers'; +import { signingCoordinator } from '@lib/wallet-api-ui'; +import { senderToDappInfo } from '@src/utils/senderToDappInfo'; +import { exposeApi, RemoteApiPropertyType } from '@cardano-sdk/web-extension'; +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(); + const { + utils: { setNextView }, + setDappInfo, + signTxRequest: { request: req, set: setSignTxRequest } + } = useViewsFlowContext(); + + const { walletType, isHardwareWallet } = useWalletStore(); + const analytics = useAnalyticsContext(); + const [confirmTransactionError, setConfirmTransactionError] = useState(false); + const disallowSignTx = useDisallowSignTx(req); + const { isConfirmingTx, signWithHardwareWallet } = useSignWithHardwareWallet(req); + const [txType, setTxType] = useState(); + + useEffect(() => { + const fetchTxType = async () => { + if (!req) return; + const type = await getTxType(req.transaction.toCore()); + setTxType(type); + }; + fetchTxType(); + }, [req]); + + const onConfirm = () => { + analytics.sendEventToPostHog(PostHogAction.SendTransactionSummaryConfirmClick, { + [TX_CREATION_TYPE_KEY]: TxCreationType.External + }); + + txSubmitted$.next({ + id: req.transaction.getId().toString(), + date: new Date().toString(), + creationType: TxCreationType.External + }); + + isHardwareWallet ? signWithHardwareWallet() : setNextView(); + }; + + useEffect(() => { + const subscription = signingCoordinator.transactionWitnessRequest$.pipe(take(1)).subscribe(async (r) => { + setDappInfo(await senderToDappInfo(r.signContext.sender)); + setSignTxRequest(r); + }); + + const api = exposeApi>( + { + api$: of({ + async readyToSignTx(): Promise { + return Promise.resolve(true); + } + }), + baseChannel: DAPP_CHANNELS.userPrompt, + properties: { readyToSignTx: RemoteApiPropertyType.MethodReturningPromise } + }, + { logger: console, runtime } + ); + + return () => { + subscription.unsubscribe(); + api.shutdown(); + }; + }, [setSignTxRequest, setDappInfo]); + + useOnBeforeUnload(disallowSignTx); + + return ( + + {req && txType ? ( + setConfirmTransactionError(true)} /> + ) : ( + + )} + {!confirmTransactionError && ( +
+ + +
+ )} +
+ ); +}; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransactionContent.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransactionContent.tsx new file mode 100644 index 000000000..fb1699047 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmTransactionContent.tsx @@ -0,0 +1,46 @@ +import React, { useMemo } from 'react'; +import { Wallet } from '@lace/cardano'; +import { ConfirmDRepRegistrationContainer } from './ConfirmDRepRegistrationContainer'; +import { DappTransactionContainer } from './DappTransactionContainer'; +import { ConfirmDRepRetirementContainer } from './ConfirmDRepRetirementContainer'; +import { ConfirmVoteDelegationContainer } from './ConfirmVoteDelegationContainer'; +import { VotingProceduresContainer } from './VotingProceduresContainer'; +import { ConfirmDRepUpdateContainer } from './ConfirmDRepUpdateContainer'; +import { ConfirmVoteRegistrationDelegationContainer } from './ConfirmVoteRegistrationDelegationContainer'; +import { ConfirmStakeRegistrationDelegationContainer } from './ConfirmStakeRegistrationDelegationContainer'; +import { ConfirmStakeVoteRegistrationDelegationContainer } from './ConfirmStakeVoteRegistrationDelegationContainer'; +import { ConfirmStakeVoteDelegationContainer } from './ConfirmStakeVoteDelegationContainer'; +import { ProposalProceduresContainer } from './ProposalProceduresContainer'; + +interface Props { + txType: Wallet.Cip30TxType; + onError?: () => void; +} + +export const ConfirmTransactionContent = ({ txType, onError }: Props): React.ReactElement => { + const containerPerTypeMap: Record< + Wallet.Cip30TxType, + (props: { onError?: () => void } | never) => React.ReactElement + > = useMemo( + () => ({ + [Wallet.Cip30TxType.DRepRegistration]: ConfirmDRepRegistrationContainer, + [Wallet.Cip30TxType.DRepRetirement]: ConfirmDRepRetirementContainer, + [Wallet.Cip30TxType.DRepUpdate]: ConfirmDRepUpdateContainer, + [Wallet.Cip30TxType.VoteDelegation]: ConfirmVoteDelegationContainer, + [Wallet.Cip30TxType.VotingProcedures]: VotingProceduresContainer, + [Wallet.Cip30TxType.VoteRegistrationDelegation]: ConfirmVoteRegistrationDelegationContainer, + [Wallet.Cip30TxType.StakeRegistrationDelegation]: ConfirmStakeRegistrationDelegationContainer, + [Wallet.Cip30TxType.StakeVoteDelegationRegistration]: ConfirmStakeVoteRegistrationDelegationContainer, + [Wallet.Cip30TxType.StakeVoteDelegation]: ConfirmStakeVoteDelegationContainer, + [Wallet.Cip30TxType.ProposalProcedures]: ProposalProceduresContainer, + Send: undefined, + Mint: undefined, + Burn: undefined + }), + [] + ); + + const Container = containerPerTypeMap[txType] || DappTransactionContainer; + + return ; +}; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmVoteDelegationContainer.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmVoteDelegationContainer.tsx new file mode 100644 index 000000000..8590fd0fe --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmVoteDelegationContainer.tsx @@ -0,0 +1,57 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ConfirmVoteDelegation } from '@lace/core'; +import { certificateInspectorFactory, drepIDasBech32FromHash } from './utils'; +import { Wallet } from '@lace/cardano'; +import { useViewsFlowContext } from '@providers'; +import { Skeleton } from 'antd'; + +const { CertificateType } = Wallet.Cardano; + +export const ConfirmVoteDelegationContainer = (): React.ReactElement => { + const { t } = useTranslation(); + const { + signTxRequest: { request }, + dappInfo + } = useViewsFlowContext(); + const [certificate, setCertificate] = useState(); + + useEffect(() => { + const getCertificateData = async () => { + const txCertificate = await certificateInspectorFactory( + CertificateType.VoteDelegation + )(request.transaction.toCore()); + setCertificate(txCertificate); + }; + + getCertificateData(); + }, [request]); + + if (!certificate) { + return ; + } + + const { dRep } = certificate; + + return ( + + ); +}; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmVoteRegistrationDelegationContainer.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmVoteRegistrationDelegationContainer.tsx new file mode 100644 index 000000000..60a84e13b --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ConfirmVoteRegistrationDelegationContainer.tsx @@ -0,0 +1,66 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ConfirmVoteRegistrationDelegation } from '@lace/core'; +import { certificateInspectorFactory, depositPaidWithSymbol, drepIDasBech32FromHash } from './utils'; +import { Wallet } from '@lace/cardano'; +import { useWalletStore } from '@src/stores'; +import { useViewsFlowContext } from '@providers'; +import { Skeleton } from 'antd'; + +const { CertificateType, RewardAddress } = Wallet.Cardano; + +export const ConfirmVoteRegistrationDelegationContainer = (): React.ReactElement => { + const { t } = useTranslation(); + const { + walletUI: { cardanoCoin }, + currentChain: { networkId } + } = useWalletStore(); + const { + signTxRequest: { request }, + dappInfo + } = useViewsFlowContext(); + const [certificate, setCertificate] = useState(); + + useEffect(() => { + if (!request) return; + const getCertificateData = async () => { + const txCertificate = await certificateInspectorFactory( + CertificateType.VoteRegistrationDelegation + )(request.transaction.toCore()); + setCertificate(txCertificate); + }; + + getCertificateData(); + }, [request]); + + if (!certificate) { + return ; + } + + const { dRep, deposit, stakeCredential } = certificate; + const depositPaidWithCardanoSymbol = depositPaidWithSymbol(deposit, cardanoCoin); + + return ( + + ); +}; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/ConfirmTransaction.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/DappTransactionContainer.tsx similarity index 58% rename from apps/browser-extension-wallet/src/features/dapp/components/ConfirmTransaction.tsx rename to apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/DappTransactionContainer.tsx index 006298b5b..a6fd22419 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/ConfirmTransaction.tsx +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/DappTransactionContainer.tsx @@ -1,39 +1,27 @@ -import React, { useState, useEffect, useMemo, useCallback } from 'react'; -import { Button, PostHogAction, useObservable } from '@lace/common'; -import { useTranslation } from 'react-i18next'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useObservable } from '@lace/common'; +import { useWalletStore } from '@stores'; +import { Skeleton } from 'antd'; import { DappTransaction } from '@lace/core'; -import { Layout } from './Layout'; -import { useViewsFlowContext } from '@providers/ViewFlowProvider'; -import { sectionTitle, DAPP_VIEWS } from '../config'; -import styles from './ConfirmTransaction.module.scss'; -import { Wallet } from '@lace/cardano'; +import { TokenInfo, getAssetsInformation } from '@src/utils/get-assets-information'; import { useAddressBookContext, withAddressBookContext } from '@src/features/address-book/context'; -import { useWalletStore } from '@stores'; -import { AddressListType } from '@views/browser/features/activity'; -import { exposeApi, RemoteApiPropertyType, WalletType } from '@cardano-sdk/web-extension'; -import { DAPP_CHANNELS } from '@src/utils/constants'; -import { runtime } from 'webextension-polyfill'; -import { useFetchCoinPrice, useRedirection } from '@hooks'; +import { AddressListType } from '@src/views/browser-view/features/activity'; +import { useCurrencyStore } from '@providers/currency'; +import { useFetchCoinPrice } from '@hooks'; +import { useViewsFlowContext } from '@providers'; +import { Wallet } from '@lace/cardano'; import { - assetsBurnedInspector, - assetsMintedInspector, - createTxInspector, AssetsMintedInspection, + createTxInspector, + assetsMintedInspector, + assetsBurnedInspector, MintedAsset } from '@cardano-sdk/core'; -import { Skeleton } from 'antd'; -import { dAppRoutePaths } from '@routes'; -import type { UserPromptService } from '@lib/scripts/background/services'; -import { of, take } from 'rxjs'; -import { getAssetsInformation, TokenInfo } from '@src/utils/get-assets-information'; -import { useCurrencyStore, useAnalyticsContext } from '@providers'; -import { TX_CREATION_TYPE_KEY, TxCreationType } from '@providers/AnalyticsProvider/analyticsTracker'; -import { txSubmitted$ } from '@providers/AnalyticsProvider/onChain'; -import { signingCoordinator } from '@lib/wallet-api-ui'; -import { senderToDappInfo } from '@src/utils/senderToDappInfo'; import { useComputeTxCollateral } from '@hooks/useComputeTxCollateral'; -const DAPP_TOAST_DURATION = 50; +interface Props { + errorMessage?: string; +} const convertMetadataArrayToObj = (arr: unknown[]): Record => { const result: Record = {}; @@ -74,37 +62,28 @@ const getAssetNameFromMintMetadata = (asset: MintedAsset, metadata: Wallet.Carda } }; -// eslint-disable-next-line sonarjs/cognitive-complexity -export const ConfirmTransaction = withAddressBookContext((): React.ReactElement => { - const { - utils: { setNextView }, - signTxRequest: { request: req, set: setSignTxRequest } - } = useViewsFlowContext(); - const { t } = useTranslation(); +// eslint-disable-next-line complexity, sonarjs/cognitive-complexity +export const DappTransactionContainer = withAddressBookContext(({ errorMessage }: Props): React.ReactElement => { const { walletInfo, inMemoryWallet, - walletType, - isHardwareWallet, blockchainProvider: { assetProvider }, walletUI: { cardanoCoin }, walletState } = useWalletStore(); - const { fiatCurrency } = useCurrencyStore(); - const { list: addressList } = useAddressBookContext(); - const { priceResult } = useFetchCoinPrice(); - const analytics = useAnalyticsContext(); - + const { + signTxRequest: { request }, + dappInfo + } = useViewsFlowContext(); + const currencyStore = useCurrencyStore(); + const coinPrice = useFetchCoinPrice(); + const { list: addressList } = useAddressBookContext() as { list: AddressListType[] }; + const tx = useMemo(() => request?.transaction.toCore(), [request?.transaction]); const assets = useObservable(inMemoryWallet.assetInfo$); - const redirectToSignFailure = useRedirection(dAppRoutePaths.dappTxSignFailure); - const redirectToSignSuccess = useRedirection(dAppRoutePaths.dappTxSignSuccess); - const [isConfirmingTx, setIsConfirmingTx] = useState(); const [assetsInfo, setAssetsInfo] = useState(); - const [dappInfo, setDappInfo] = useState(); - const tx = useMemo(() => req?.transaction.toCore(), [req?.transaction]); + const txCollateral = useComputeTxCollateral(walletState, tx); - // All assets' ids in the transaction body. Used to fetch their info from cardano services const assetIds = useMemo(() => { if (!tx) return []; const uniqueAssetIds = new Set(); @@ -136,56 +115,6 @@ export const ConfirmTransaction = withAddressBookContext((): React.ReactElement } }, [assetIds, assetProvider, assets]); - const cancelTransaction = useCallback( - async (close = false) => { - await req.reject('User rejected to sign'); - close && setTimeout(() => window.close(), DAPP_TOAST_DURATION); - }, - [req] - ); - - window.addEventListener('beforeunload', cancelTransaction); - - const signWithHardwareWallet = async () => { - setIsConfirmingTx(true); - try { - if (req.walletType !== WalletType.Ledger && req.walletType !== WalletType.Trezor) { - throw new Error('Invalid state: expected hw wallet'); - } - await req.sign(); - redirectToSignSuccess(); - } catch (error) { - console.error('signWithHardwareWallet error', error); - cancelTransaction(false); - redirectToSignFailure(); - } - }; - - useEffect(() => { - const subscription = signingCoordinator.transactionWitnessRequest$.pipe(take(1)).subscribe(async (r) => { - setDappInfo(await senderToDappInfo(r.signContext.sender)); - setSignTxRequest(r); - }); - - const api = exposeApi>( - { - api$: of({ - async readyToSignTx(): Promise { - return Promise.resolve(true); - } - }), - baseChannel: DAPP_CHANNELS.userPrompt, - properties: { readyToSignTx: RemoteApiPropertyType.MethodReturningPromise } - }, - { logger: console, runtime } - ); - - return () => { - subscription.unsubscribe(); - api.shutdown(); - }; - }, [setSignTxRequest]); - const createMintedList = useCallback( (mintedAssets: AssetsMintedInspection) => { if (!assetsInfo) return []; @@ -247,7 +176,7 @@ export const ConfirmTransaction = withAddressBookContext((): React.ReactElement const { minted, burned } = await inspector(tx as Wallet.Cardano.HydratedTx); const isMintTransaction = minted.length > 0 || burned.length > 0; - const txType = isMintTransaction ? 'Mint' : 'Send'; + const txType = isMintTransaction ? Wallet.Cip30TxType.Mint : Wallet.Cip30TxType.Send; const externalOutputs = tx.body.outputs.filter((output) => { if (txType === 'Send') { @@ -283,53 +212,16 @@ export const ConfirmTransaction = withAddressBookContext((): React.ReactElement getTxSummary(); }, [tx, walletInfo.addresses, createAssetList, createMintedList, addressToNameMap, setTxSummary, txCollateral]); - const onConfirm = () => { - analytics.sendEventToPostHog(PostHogAction.SendTransactionSummaryConfirmClick, { - [TX_CREATION_TYPE_KEY]: TxCreationType.External - }); - - txSubmitted$.next({ - id: req.transaction.getId().toString(), - date: new Date().toString(), - creationType: TxCreationType.External - }); - - isHardwareWallet ? signWithHardwareWallet() : setNextView(); - }; - - return ( - - {req && txSummary ? ( - - ) : ( - - )} -
- - -
-
+ return tx && txSummary ? ( + + ) : ( + ); }); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/NonRegisteredUserModal/NonRegisteredUserModal.module.scss b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/NonRegisteredUserModal/NonRegisteredUserModal.module.scss new file mode 100644 index 000000000..656d81f9b --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/NonRegisteredUserModal/NonRegisteredUserModal.module.scss @@ -0,0 +1,52 @@ +@import '../../../../../../../../packages/common/src/ui/styles/theme.scss'; +@import '../../../../../../../../packages/common/src/ui/styles/abstracts/_typography'; +@import '../../../../../../../../packages/common/src/ui/styles/abstracts/mixins'; +@import '../../../../../styles/rules/modal.scss'; + +.continueInBrowser { + @extend %modal-globals; +} + +.modal { + &:global(.ant-modal) { + max-width: calc(100vw - #{size_unit(6)}) !important; + max-height: calc(100vh - 80px) !important; + } + :global(.ant-modal-content) { + background: var(--color-white, var(--dark-mode-light-black)) !important; + border-radius: size_unit(2) !important; + box-shadow: var(--shadows-card-pop-up) !important; + max-height: calc(100vh - 80px) !important; + } + :global(.ant-modal-body) { + display: flex; + flex-direction: column; + padding: size_unit(5) !important; + max-height: calc(100vh - 80px) !important; + } +} + +.container { + display: flex; + flex-direction: column; + gap: size_unit(3); + text-align: center +} + +.title { + @include text-subHeading-bold; + color: var(--text-color-primary) !important; + margin-bottom: 0 !important; +} + +.description { + @include text-body-medium; + color: var(--text-color-secondary) !important; +} + +.buttons { + display: flex; + gap: size_unit(2); + flex-direction: column; + width: 100%; +} diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/NonRegisteredUserModal/NonRegisteredUserModal.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/NonRegisteredUserModal/NonRegisteredUserModal.tsx new file mode 100644 index 000000000..57edad87c --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/NonRegisteredUserModal/NonRegisteredUserModal.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Modal, Typography } from 'antd'; +import { Button } from '@lace/common'; +import styles from './NonRegisteredUserModal.module.scss'; + +const { Title, Text } = Typography; + +interface NonRegisteredUserModalProps { + visible: boolean; + onConfirm: () => void; + onClose: () => void; +} + +export const NonRegisteredUserModal = ({ + visible, + onConfirm, + onClose +}: NonRegisteredUserModalProps): React.ReactElement => { + const { t } = useTranslation(); + + return ( + +
+ + {t('core.VotingProcedures.NonRegisteredUserModal.title')} + + + {t('core.VotingProcedures.NonRegisteredUserModal.description')} + +
+ + +
+
+
+ ); +}; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ProposalProceduresContainer.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ProposalProceduresContainer.tsx new file mode 100644 index 000000000..b72eb6d91 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/ProposalProceduresContainer.tsx @@ -0,0 +1,66 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Wallet } from '@lace/cardano'; +import { proposalProceduresInspector } from './utils'; +import { HardForkInitiationActionContainer } from './proposal-procedures/HardForkInitiationActionContainer'; +import { InfoActionContainer } from './proposal-procedures/InfoActionContainer'; +import { NewConstitutionActionContainer } from './proposal-procedures/NewConstitutionActionContainer'; +import { NoConfidenceActionContainer } from './proposal-procedures/NoConfidenceActionContainer'; +import { ParameterChangeActionContainer } from './proposal-procedures/ParameterChangeActionContainer'; +import { TreasuryWithdrawalsActionContainer } from './proposal-procedures/TreasuryWithdrawalsActionContainer'; +import { UpdateCommitteeActionContainer } from './proposal-procedures/UpdateCommitteeActionContainer'; +import { useViewsFlowContext } from '@providers'; +import { SignTxData } from './types'; + +export const ProposalProceduresContainer = (): React.ReactElement => { + const [proposalProcedures, setProposalProcedures] = useState([]); + const { + signTxRequest: { request }, + dappInfo + } = useViewsFlowContext(); + + useEffect(() => { + const proposalProcedureData = async () => { + const procedures = await proposalProceduresInspector(request.transaction.toCore()); + setProposalProcedures(procedures); + }; + + proposalProcedureData(); + }, [request]); + + const containerPerTypeMap: Record< + Wallet.Cardano.GovernanceActionType, + (props: { + dappInfo: SignTxData['dappInfo']; + governanceAction: Wallet.Cardano.GovernanceAction; + deposit: Wallet.Cardano.ProposalProcedure['deposit']; + rewardAccount: Wallet.Cardano.ProposalProcedure['rewardAccount']; + anchor: Wallet.Cardano.ProposalProcedure['anchor']; + errorMessage?: string; + }) => React.ReactElement + > = useMemo( + () => ({ + [Wallet.Cardano.GovernanceActionType.hard_fork_initiation_action]: HardForkInitiationActionContainer, + [Wallet.Cardano.GovernanceActionType.info_action]: InfoActionContainer, + [Wallet.Cardano.GovernanceActionType.new_constitution]: NewConstitutionActionContainer, + [Wallet.Cardano.GovernanceActionType.no_confidence]: NoConfidenceActionContainer, + [Wallet.Cardano.GovernanceActionType.parameter_change_action]: ParameterChangeActionContainer, + [Wallet.Cardano.GovernanceActionType.treasury_withdrawals_action]: TreasuryWithdrawalsActionContainer, + [Wallet.Cardano.GovernanceActionType.update_committee]: UpdateCommitteeActionContainer + }), + [] + ); + + return ( + <> + {proposalProcedures.map(({ deposit, rewardAccount, anchor, governanceAction }) => { + const Container = containerPerTypeMap[governanceAction.__typename]; + return ( + + ); + })} + + ); +}; 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 new file mode 100644 index 000000000..771506372 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/VotingProceduresContainer.tsx @@ -0,0 +1,94 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { VotingProcedures } from '@lace/core'; +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(); + const { + 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 () => { + const txVotingProcedures = await votingProceduresInspector(request.transaction.toCore()); + setVotingProcedures(txVotingProcedures); + }; + + getVotingProcedures(); + }, [request]); + + useEffect(() => { + if (!walletState?.transactions.history || userAckNonRegisteredState) return; + setIsNonRegisteredUserModalVisible(!hasValidDrepRegistration(walletState.transactions.history)); + }, [walletState?.transactions.history, userAckNonRegisteredState]); + + const explorerBaseUrl = useCexplorerBaseUrl(); + + return ( + <> + { + setUserAckNonRegisteredState(true); + setIsNonRegisteredUserModalVisible(false); + }} + onClose={() => disallowSignTx(true)} + /> + { + const voterType = getVoterType(votingProcedure.voter.__typename); + + return { + voter: { + type: t(`core.VotingProcedures.voterTypes.${voterType}`), + dRepId: getDRepId(votingProcedure.voter) + }, + 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') + }} + /> + + ); +}; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmDRepRegistrationContainer.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmDRepRegistrationContainer.test.tsx new file mode 100644 index 000000000..1dc94ae50 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmDRepRegistrationContainer.test.tsx @@ -0,0 +1,150 @@ +/* eslint-disable unicorn/no-null */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable import/imports-first */ +const mockUseWalletStore = jest.fn(); +const t = jest.fn().mockImplementation((res) => res); +const mockUseTranslation = jest.fn(() => ({ t })); +const mockConfirmDRepRegistration = jest.fn(); +import * as React from 'react'; +import { cleanup, render } from '@testing-library/react'; +import { ConfirmDRepRegistrationContainer } from '../ConfirmDRepRegistrationContainer'; +import '@testing-library/jest-dom'; +import { BehaviorSubject } from 'rxjs'; +import { act } from 'react-dom/test-utils'; +import { buildMockTx } from '@src/utils/mocks/tx'; +import { Wallet } from '@lace/cardano'; +import { getWrapper } from '../testing.utils'; +import { depositPaidWithSymbol, drepIDasBech32FromHash } from '../utils'; +import { TransactionWitnessRequest } from '@cardano-sdk/web-extension'; + +const { Cardano, Crypto } = Wallet; + +const assetInfo$ = new BehaviorSubject(new Map()); +const available$ = new BehaviorSubject([]); + +const inMemoryWallet = { + assetInfo$, + balance: { + utxo: { + available$ + } + } +}; + +const cardanoCoinMock = { + name: 'Cardano', + symbol: 'cardanoCoinMockSymbol' +}; + +jest.mock('@src/stores', () => ({ + ...jest.requireActual('@src/stores'), + useWalletStore: mockUseWalletStore +})); + +const certificate: Wallet.Cardano.Certificate = { + __typename: Cardano.CertificateType.RegisterDelegateRepresentative, + dRepCredential: { + type: Cardano.CredentialType.KeyHash, + hash: Crypto.Hash28ByteBase16(Buffer.from('dRepCredentialHashdRepCreden').toString('hex')) + }, + deposit: BigInt('1000'), + anchor: { + url: 'anchorUrl', + dataHash: Crypto.Hash32ByteBase16(Buffer.from('anchorDataHashanchorDataHashanch').toString('hex')) + } +}; +const tx = buildMockTx({ + certificates: [certificate] +}); + +const dappInfo: Wallet.DappInfo = { + name: 'dappName', + logo: 'dappLogo', + url: 'dappUrl' +}; + +const request = { + transaction: { + toCore: jest.fn().mockReturnValue(tx) + } as any +} as TransactionWitnessRequest; + +jest.mock('@providers', () => ({ + ...jest.requireActual('@providers'), + useViewsFlowContext: () => ({ + signTxRequest: { request }, + dappInfo + }) +})); + +jest.mock('@lace/core', () => { + const original = jest.requireActual('@lace/core'); + return { + __esModule: true, + ...original, + ConfirmDRepRegistration: mockConfirmDRepRegistration + }; +}); + +jest.mock('react-i18next', () => { + const original = jest.requireActual('react-i18next'); + return { + __esModule: true, + ...original, + useTranslation: mockUseTranslation + }; +}); + +describe('Testing ConfirmDRepRegistrationContainer component', () => { + beforeEach(() => { + mockUseWalletStore.mockReset(); + mockUseWalletStore.mockImplementation(() => ({ + inMemoryWallet, + walletUI: { cardanoCoin: cardanoCoinMock }, + walletInfo: {} + })); + mockConfirmDRepRegistration.mockReset(); + mockConfirmDRepRegistration.mockReturnValue(); + mockUseTranslation.mockReset(); + mockUseTranslation.mockImplementation(() => ({ t })); + }); + + afterEach(() => { + jest.resetModules(); + jest.resetAllMocks(); + cleanup(); + }); + + test('should render ConfirmDRepRegistration component with proper props', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render(, { + wrapper: getWrapper() + })); + }); + + expect(queryByTestId('ConfirmDRepRegistration')).toBeInTheDocument(); + expect(mockConfirmDRepRegistration).toHaveBeenLastCalledWith( + { + dappInfo, + metadata: { + depositPaid: depositPaidWithSymbol(certificate.deposit, cardanoCoinMock as Wallet.CoinId), + drepId: drepIDasBech32FromHash(certificate.dRepCredential.hash), + hash: certificate.anchor?.dataHash, + url: certificate.anchor?.url + }, + translations: { + metadata: t('core.DRepRegistration.metadata'), + labels: { + depositPaid: t('core.DRepRegistration.depositPaid'), + drepId: t('core.DRepRegistration.drepId'), + hash: t('core.DRepRegistration.hash'), + url: t('core.DRepRegistration.url') + } + } + }, + {} + ); + }); +}); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmDRepRetirementContainer.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmDRepRetirementContainer.test.tsx new file mode 100644 index 000000000..bf927ebba --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmDRepRetirementContainer.test.tsx @@ -0,0 +1,222 @@ +/* eslint-disable sonarjs/no-identical-functions */ +/* eslint-disable unicorn/no-null */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable import/imports-first */ + +const mockUseWalletStore = jest.fn(); +const t = jest.fn().mockImplementation((res) => res); +const mockUseTranslation = jest.fn(() => ({ t })); +const mockConfirmDRepRetirement = jest.fn(); +const mockDappError = jest.fn(); +const mockDisallowSignTx = jest.fn(); +const mockUseGetOwnPubDRepKeyHash = jest.fn(); +const flowContextMock = jest.fn(); + +import * as React from 'react'; +import { cleanup, render } from '@testing-library/react'; +import { ConfirmDRepRetirementContainer } from '../ConfirmDRepRetirementContainer'; +import '@testing-library/jest-dom'; +import { BehaviorSubject } from 'rxjs'; +import { act } from 'react-dom/test-utils'; +import { buildMockTx } from '@src/utils/mocks/tx'; +import { Wallet } from '@lace/cardano'; +import { getWrapper } from '../testing.utils'; +import { depositPaidWithSymbol, drepIDasBech32FromHash } from '../utils'; +import { TransactionWitnessRequest } from '@cardano-sdk/web-extension'; + +const { Cardano, Crypto } = Wallet; + +const assetInfo$ = new BehaviorSubject(new Map()); +const available$ = new BehaviorSubject([]); + +const hash = Crypto.Hash28ByteBase16(Buffer.from('dRepCredentialHashdRepCreden').toString('hex')); +const getPubDRepKey = async () => await hash; + +const inMemoryWallet = { + getPubDRepKey, + assetInfo$, + balance: { + utxo: { + available$ + } + } +}; + +const cardanoCoinMock = { + name: 'Cardano', + symbol: 'cardanoCoinMockSymbol' +}; + +const dappInfo = { + name: 'dappName', + logo: 'dappLogo', + url: 'dappUrl' +}; +const certificate: Wallet.Cardano.Certificate = { + __typename: Cardano.CertificateType.UnregisterDelegateRepresentative, + dRepCredential: { + type: Cardano.CredentialType.KeyHash, + hash + }, + deposit: BigInt('1000') +}; +const tx = buildMockTx({ + certificates: [certificate] +}); + +const request = { + transaction: { + toCore: jest.fn().mockReturnValue(tx) + } as any +} as TransactionWitnessRequest; + +jest.mock('@providers', () => ({ + ...jest.requireActual('@providers'), + useViewsFlowContext: flowContextMock +})); + +jest.mock('../hooks.ts', () => { + const original = jest.requireActual('../hooks.ts'); + return { + __esModule: true, + ...original, + useGetOwnPubDRepKeyHash: mockUseGetOwnPubDRepKeyHash + }; +}); + +jest.mock('../utils.ts', () => { + const original = jest.requireActual('../utils.ts'); + return { + __esModule: true, + ...original, + disallowSignTx: mockDisallowSignTx + }; +}); + +jest.mock('@src/stores', () => ({ + ...jest.requireActual('@src/stores'), + useWalletStore: mockUseWalletStore +})); + +jest.mock('@lace/core', () => { + const original = jest.requireActual('@lace/core'); + return { + __esModule: true, + ...original, + ConfirmDRepRetirement: mockConfirmDRepRetirement + }; +}); + +jest.mock('../../DappError', () => { + const original = jest.requireActual('../../DappError'); + return { + __esModule: true, + ...original, + DappError: mockDappError + }; +}); + +jest.mock('react-i18next', () => { + const original = jest.requireActual('react-i18next'); + return { + __esModule: true, + ...original, + useTranslation: mockUseTranslation + }; +}); + +describe('Testing ConfirmDRepRetirementContainer component', () => { + beforeEach(() => { + mockUseWalletStore.mockReset(); + mockUseGetOwnPubDRepKeyHash.mockReset(); + mockUseGetOwnPubDRepKeyHash.mockImplementation(() => ({ + loading: false, + ownPubDRepKeyHash: hash + })); + mockUseWalletStore.mockImplementation(() => ({ + inMemoryWallet, + walletUI: { cardanoCoin: cardanoCoinMock }, + walletInfo: {} + })); + flowContextMock.mockReset(); + flowContextMock.mockImplementation(() => ({ + signTxRequest: { request }, + dappInfo + })); + + mockConfirmDRepRetirement.mockReset(); + mockConfirmDRepRetirement.mockReturnValue(); + mockDappError.mockReset(); + mockDappError.mockReturnValue(); + mockUseTranslation.mockReset(); + mockUseTranslation.mockImplementation(() => ({ t })); + }); + + afterEach(() => { + cleanup(); + }); + + // eslint-disable-next-line unicorn/consistent-function-scoping + const onErrorMock = jest.fn(); + const props = { onError: onErrorMock }; + + test('should render ConfirmDRepRetirementContainer component with proper props', async () => { + let queryByTestId: any; + await act(async () => { + ({ queryByTestId } = render(, { + wrapper: getWrapper() + })); + }); + + expect(queryByTestId('ConfirmDRepRetirementContainer')).toBeInTheDocument(); + expect(mockConfirmDRepRetirement).toHaveBeenLastCalledWith( + { + dappInfo, + metadata: { + depositReturned: depositPaidWithSymbol(certificate.deposit, cardanoCoinMock as Wallet.CoinId), + drepId: drepIDasBech32FromHash(certificate.dRepCredential.hash) + }, + translations: { + metadata: t('core.DRepRetirement.metadata'), + labels: { + depositReturned: t('core.DRepRetirement.depositReturned'), + drepId: t('core.DRepRetirement.drepId') + } + } + }, + {} + ); + }); + + test('should render ConfirmDRepRetirementContainer component with proper error for own retirement', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render(, { + wrapper: getWrapper() + })); + }); + + expect(queryByTestId('ConfirmDRepRetirementContainer')).toBeInTheDocument(); + }); + + test('should render ConfirmDRepRetirementContainer component with proper error for not own retirement', async () => { + mockUseGetOwnPubDRepKeyHash.mockReset(); + mockUseGetOwnPubDRepKeyHash.mockImplementation(() => ({ + loading: false, + ownPubDRepKeyHash: Crypto.Hash28ByteBase16(Buffer.from('WRONG_dRepCredentialHashdRep').toString('hex')) + })); + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render(, { + wrapper: getWrapper() + })); + }); + + expect(queryByTestId('DappError')).toBeInTheDocument(); + expect(onErrorMock).toBeCalledTimes(1); + expect(mockDisallowSignTx).toBeCalledTimes(1); + expect(mockDisallowSignTx).toBeCalledWith(request, true); + }); +}); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmDRepUpdateContainer.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmDRepUpdateContainer.test.tsx new file mode 100644 index 000000000..650361e93 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmDRepUpdateContainer.test.tsx @@ -0,0 +1,147 @@ +/* eslint-disable unicorn/no-null */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable import/imports-first */ +const mockUseWalletStore = jest.fn(); +const t = jest.fn().mockImplementation((res) => res); +const mockUseTranslation = jest.fn(() => ({ t })); +const mockConfirmDRepUpdate = jest.fn(); +import * as React from 'react'; +import { cleanup, render } from '@testing-library/react'; +import { ConfirmDRepUpdateContainer } from '../ConfirmDRepUpdateContainer'; +import '@testing-library/jest-dom'; +import { BehaviorSubject } from 'rxjs'; +import { act } from 'react-dom/test-utils'; +import { buildMockTx } from '@src/utils/mocks/tx'; +import { Wallet } from '@lace/cardano'; +import { getWrapper } from '../testing.utils'; +import { drepIDasBech32FromHash } from '../utils'; +import { TransactionWitnessRequest } from '@cardano-sdk/web-extension'; + +const { Cardano, Crypto } = Wallet; + +const assetInfo$ = new BehaviorSubject(new Map()); +const available$ = new BehaviorSubject([]); + +const inMemoryWallet = { + assetInfo$, + balance: { + utxo: { + available$ + } + } +}; + +const cardanoCoinMock = { + name: 'Cardano', + symbol: 'cardanoCoinMockSymbol' +}; + +const dappInfo: Wallet.DappInfo = { + name: 'dappName', + logo: 'dappLogo', + url: 'dappUrl' +}; + +const certificate: Wallet.Cardano.Certificate = { + __typename: Cardano.CertificateType.UpdateDelegateRepresentative, + dRepCredential: { + type: Cardano.CredentialType.KeyHash, + hash: Crypto.Hash28ByteBase16(Buffer.from('dRepCredentialHashdRepCreden').toString('hex')) + }, + anchor: { + url: 'anchorUrl', + dataHash: Crypto.Hash32ByteBase16(Buffer.from('anchorDataHashanchorDataHashanch').toString('hex')) + } +}; +const tx = buildMockTx({ + certificates: [certificate] +}); + +const request = { + transaction: { + toCore: jest.fn().mockReturnValue(tx) + } as any +} as TransactionWitnessRequest; + +jest.mock('@providers', () => ({ + ...jest.requireActual('@providers'), + useViewsFlowContext: () => ({ + signTxRequest: { request }, + dappInfo + }) +})); + +jest.mock('@src/stores', () => ({ + ...jest.requireActual('@src/stores'), + useWalletStore: mockUseWalletStore +})); + +jest.mock('@lace/core', () => { + const original = jest.requireActual('@lace/core'); + return { + __esModule: true, + ...original, + ConfirmDRepUpdate: mockConfirmDRepUpdate + }; +}); + +jest.mock('react-i18next', () => { + const original = jest.requireActual('react-i18next'); + return { + __esModule: true, + ...original, + useTranslation: mockUseTranslation + }; +}); + +describe('Testing ConfirmDRepUpdateContainer component', () => { + beforeEach(() => { + mockUseWalletStore.mockReset(); + mockUseWalletStore.mockImplementation(() => ({ + inMemoryWallet, + walletUI: { cardanoCoin: cardanoCoinMock }, + walletInfo: {} + })); + mockConfirmDRepUpdate.mockReset(); + mockConfirmDRepUpdate.mockReturnValue(); + mockUseTranslation.mockReset(); + mockUseTranslation.mockImplementation(() => ({ t })); + }); + + afterEach(() => { + jest.resetModules(); + jest.resetAllMocks(); + cleanup(); + }); + + test('should render ConfirmDRepUpdate component with proper props', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render(, { + wrapper: getWrapper() + })); + }); + + expect(queryByTestId('ConfirmDRepUpdate')).toBeInTheDocument(); + expect(mockConfirmDRepUpdate).toHaveBeenLastCalledWith( + { + dappInfo, + metadata: { + drepId: drepIDasBech32FromHash(certificate.dRepCredential.hash), + hash: certificate.anchor?.dataHash, + url: certificate.anchor?.url + }, + translations: { + metadata: t('core.DRepUpdate.metadata'), + labels: { + drepId: t('core.DRepUpdate.drepId'), + hash: t('core.DRepUpdate.hash'), + url: t('core.DRepUpdate.url') + } + } + }, + {} + ); + }); +}); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmStakeRegistrationDelegationContainer.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmStakeRegistrationDelegationContainer.test.tsx new file mode 100644 index 000000000..318361616 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmStakeRegistrationDelegationContainer.test.tsx @@ -0,0 +1,166 @@ +/* eslint-disable unicorn/no-null */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable import/imports-first */ +const mockUseWalletStore = jest.fn(); +const t = jest.fn().mockImplementation((res) => res); +const mockUseTranslation = jest.fn(() => ({ t })); +const mockConfirmStakeRegistrationDelegation = jest.fn(); +const mockLovelacesToAdaString = jest.fn(); +import * as React from 'react'; +import { cleanup, render } from '@testing-library/react'; +import { ConfirmStakeRegistrationDelegationContainer } from '../ConfirmStakeRegistrationDelegationContainer'; +import '@testing-library/jest-dom'; +import { BehaviorSubject } from 'rxjs'; +import { act } from 'react-dom/test-utils'; +import { buildMockTx } from '@src/utils/mocks/tx'; +import { Wallet } from '@lace/cardano'; +import { getWrapper } from '../testing.utils'; +import { TransactionWitnessRequest } from '@cardano-sdk/web-extension'; +import { depositPaidWithSymbol } from '../utils'; + +const REWARD_ACCOUNT = Wallet.Cardano.RewardAccount('stake_test1uqrw9tjymlm8wrwq7jk68n6v7fs9qz8z0tkdkve26dylmfc2ux2hj'); +const STAKE_KEY_HASH = Wallet.Cardano.RewardAccount.toHash(REWARD_ACCOUNT); + +const assetInfo$ = new BehaviorSubject(new Map()); +const available$ = new BehaviorSubject([]); + +const inMemoryWallet = { + assetInfo$, + balance: { + utxo: { + available$ + } + } +}; + +const cardanoCoinMock = { + name: 'Cardano', + symbol: 'cardanoCoinMockSymbol' +}; + +const dappInfo = { + name: 'dappName', + logo: 'dappLogo', + url: 'dappUrl' +}; +const certificate: Wallet.Cardano.Certificate = { + __typename: Wallet.Cardano.CertificateType.StakeRegistrationDelegation, + poolId: Wallet.Cardano.PoolId('pool126zlx7728y7xs08s8epg9qp393kyafy9rzr89g4qkvv4cv93zem'), + stakeCredential: { + type: Wallet.Cardano.CredentialType.KeyHash, + hash: Wallet.Crypto.Hash28ByteBase16(STAKE_KEY_HASH) + }, + deposit: BigInt('100000') +}; +const tx = buildMockTx({ + certificates: [certificate] +}); + +const request = { + transaction: { + toCore: jest.fn().mockReturnValue(tx) + } as any +} as TransactionWitnessRequest; + +jest.mock('@providers', () => ({ + ...jest.requireActual('@providers'), + useViewsFlowContext: () => ({ + signTxRequest: { request }, + dappInfo + }) +})); + +jest.mock('@src/stores', () => ({ + ...jest.requireActual('@src/stores'), + useWalletStore: mockUseWalletStore +})); + +jest.mock('@lace/core', () => { + const original = jest.requireActual('@lace/core'); + return { + __esModule: true, + ...original, + ConfirmStakeRegistrationDelegation: mockConfirmStakeRegistrationDelegation + }; +}); + +jest.mock('react-i18next', () => { + const original = jest.requireActual('react-i18next'); + return { + __esModule: true, + ...original, + useTranslation: mockUseTranslation + }; +}); + +jest.mock('@lace/cardano', () => { + const actual = jest.requireActual('@lace/cardano'); + return { + __esModule: true, + ...actual, + Wallet: { + ...actual.Wallet, + util: { + ...actual.Wallet.util, + lovelacesToAdaString: mockLovelacesToAdaString + } + } + }; +}); + +describe('Testing ConfirmStakeRegistrationDelegationContainer component', () => { + beforeEach(() => { + mockUseWalletStore.mockReset(); + mockUseWalletStore.mockImplementation(() => ({ + inMemoryWallet, + walletUI: { cardanoCoin: cardanoCoinMock }, + walletInfo: {}, + currentChain: { + networkId: 0 + } + })); + mockConfirmStakeRegistrationDelegation.mockReset(); + mockConfirmStakeRegistrationDelegation.mockReturnValue(); + mockUseTranslation.mockReset(); + mockUseTranslation.mockImplementation(() => ({ t })); + mockLovelacesToAdaString.mockReset(); + mockLovelacesToAdaString.mockImplementation((val) => val); + }); + + afterEach(() => { + jest.resetModules(); + jest.resetAllMocks(); + cleanup(); + }); + + test('should render ConfirmStakeRegistrationDelegation component with proper props', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render(, { + wrapper: getWrapper() + })); + }); + + expect(queryByTestId('ConfirmStakeRegistrationDelegation')).toBeInTheDocument(); + expect(mockConfirmStakeRegistrationDelegation).toHaveBeenLastCalledWith( + { + dappInfo, + metadata: { + poolId: certificate.poolId, + stakeKeyHash: 'stake_test1uqrw9tjymlm8wrwq7jk68n6v7fs9qz8z0tkdkve26dylmfc2ux2hj', + depositPaid: depositPaidWithSymbol(certificate.deposit, cardanoCoinMock as Wallet.CoinId) + }, + translations: { + metadata: t('core.StakeRegistrationDelegation.metadata'), + labels: { + poolId: t('core.StakeRegistrationDelegation.poolId'), + stakeKeyHash: t('core.StakeRegistrationDelegation.stakeKeyHash'), + depositPaid: t('core.StakeRegistrationDelegation.depositPaid') + } + } + }, + {} + ); + }); +}); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmStakeVoteDelegationContainer.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmStakeVoteDelegationContainer.test.tsx new file mode 100644 index 000000000..3de25766c --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmStakeVoteDelegationContainer.test.tsx @@ -0,0 +1,186 @@ +/* eslint-disable unicorn/no-null */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable import/imports-first */ +const mockUseWalletStore = jest.fn(); +const t = jest.fn().mockImplementation((res) => res); +const mockUseTranslation = jest.fn(() => ({ t })); +const mockConfirmStakeVoteDelegation = jest.fn(); +const mockIsDRepAlwaysAbstain = jest.fn(); +const mockIsDRepAlwaysNoConfidence = jest.fn(); +const mockIsDRepCredential = jest.fn(); +import * as React from 'react'; +import { cleanup, render } from '@testing-library/react'; +import { ConfirmStakeVoteDelegationContainer } from '../ConfirmStakeVoteDelegationContainer'; +import '@testing-library/jest-dom'; +import { BehaviorSubject } from 'rxjs'; +import { act } from 'react-dom/test-utils'; +import { buildMockTx } from '@src/utils/mocks/tx'; +import { Wallet } from '@lace/cardano'; +import { getWrapper } from '../testing.utils'; +import { drepIDasBech32FromHash } from '../utils'; +import { TransactionWitnessRequest } from '@cardano-sdk/web-extension'; + +const REWARD_ACCOUNT = Wallet.Cardano.RewardAccount('stake_test1uqrw9tjymlm8wrwq7jk68n6v7fs9qz8z0tkdkve26dylmfc2ux2hj'); +const STAKE_KEY_HASH = Wallet.Cardano.RewardAccount.toHash(REWARD_ACCOUNT); + +const assetInfo$ = new BehaviorSubject(new Map()); +const available$ = new BehaviorSubject([]); + +const inMemoryWallet = { + assetInfo$, + balance: { + utxo: { + available$ + } + } +}; + +const cardanoCoinMock = { + name: 'Cardano', + symbol: 'cardanoCoinMockSymbol' +}; + +const dappInfo = { + name: 'dappName', + logo: 'dappLogo', + url: 'dappUrl' +}; + +const certificate: Wallet.Cardano.Certificate = { + __typename: Wallet.Cardano.CertificateType.StakeVoteDelegation, + poolId: Wallet.Cardano.PoolId('pool126zlx7728y7xs08s8epg9qp393kyafy9rzr89g4qkvv4cv93zem'), + stakeCredential: { + type: Wallet.Cardano.CredentialType.KeyHash, + hash: Wallet.Crypto.Hash28ByteBase16(STAKE_KEY_HASH) + }, + dRep: { + type: Wallet.Cardano.CredentialType.KeyHash, + hash: Wallet.Crypto.Hash28ByteBase16(Buffer.from('dRepCredentialHashdRepCreden').toString('hex')) + } +}; +const tx = buildMockTx({ + certificates: [certificate] +}); + +const request = { + transaction: { + toCore: jest.fn().mockReturnValue(tx) + } as any +} as TransactionWitnessRequest; + +jest.mock('@providers', () => ({ + ...jest.requireActual('@providers'), + useViewsFlowContext: () => ({ + signTxRequest: { request }, + dappInfo + }) +})); + +jest.mock('@src/stores', () => ({ + ...jest.requireActual('@src/stores'), + useWalletStore: mockUseWalletStore +})); + +jest.mock('@lace/core', () => { + const original = jest.requireActual('@lace/core'); + return { + __esModule: true, + ...original, + ConfirmStakeVoteDelegation: mockConfirmStakeVoteDelegation + }; +}); + +jest.mock('react-i18next', () => { + const original = jest.requireActual('react-i18next'); + return { + __esModule: true, + ...original, + useTranslation: mockUseTranslation + }; +}); + +jest.mock('@lace/cardano', () => { + const actual = jest.requireActual('@lace/cardano'); + return { + __esModule: true, + ...actual, + Wallet: { + ...actual.Wallet, + Cardano: { + ...actual.Wallet.Cardano, + isDRepAlwaysAbstain: mockIsDRepAlwaysAbstain, + isDRepAlwaysNoConfidence: mockIsDRepAlwaysNoConfidence, + isDRepCredential: mockIsDRepCredential + } + } + }; +}); + +const isDRepAlwaysAbstainMocked = 'isDRepAlwaysAbstainMocked'; +const isDRepAlwaysNoConfidenceMocked = 'isDRepAlwaysNoConfidenceMocked'; + +describe('Testing ConfirmStakeVoteDelegationContainer component', () => { + beforeEach(() => { + mockUseWalletStore.mockReset(); + mockUseWalletStore.mockImplementation(() => ({ + inMemoryWallet, + walletUI: { cardanoCoin: cardanoCoinMock }, + walletInfo: {}, + currentChain: { + networkId: 0 + } + })); + mockConfirmStakeVoteDelegation.mockReset(); + mockConfirmStakeVoteDelegation.mockReturnValue(); + mockUseTranslation.mockReset(); + mockUseTranslation.mockImplementation(() => ({ t })); + mockIsDRepAlwaysAbstain.mockReset(); + mockIsDRepAlwaysAbstain.mockImplementation(() => isDRepAlwaysAbstainMocked); + mockIsDRepAlwaysNoConfidence.mockReset(); + mockIsDRepAlwaysNoConfidence.mockImplementation(() => isDRepAlwaysNoConfidenceMocked); + mockIsDRepCredential.mockReset(); + mockIsDRepCredential.mockImplementation(() => true); + }); + + afterEach(() => { + jest.resetModules(); + jest.resetAllMocks(); + cleanup(); + }); + + test('should render ConfirmStakeVoteDelegation component with proper props', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render(, { + wrapper: getWrapper() + })); + }); + + expect(queryByTestId('ConfirmStakeVoteDelegation')).toBeInTheDocument(); + expect(mockConfirmStakeVoteDelegation).toHaveBeenLastCalledWith( + { + dappInfo, + metadata: { + poolId: certificate.poolId, + stakeKeyHash: 'stake_test1uqrw9tjymlm8wrwq7jk68n6v7fs9qz8z0tkdkve26dylmfc2ux2hj', + alwaysAbstain: isDRepAlwaysAbstainMocked, + alwaysNoConfidence: isDRepAlwaysNoConfidenceMocked, + drepId: drepIDasBech32FromHash((certificate.dRep as Wallet.Cardano.Credential).hash) + }, + translations: { + metadata: t('core.StakeVoteDelegation.metadata'), + option: t('core.StakeVoteDelegation.option'), + labels: { + poolId: t('core.StakeVoteDelegation.poolId'), + stakeKeyHash: t('core.StakeVoteDelegation.stakeKeyHash'), + drepId: t('core.StakeVoteDelegation.drepId'), + alwaysAbstain: t('core.StakeVoteDelegation.alwaysAbstain'), + alwaysNoConfidence: t('core.StakeVoteDelegation.alwaysNoConfidence') + } + } + }, + {} + ); + }); +}); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmStakeVoteRegistrationDelegationContainer.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmStakeVoteRegistrationDelegationContainer.test.tsx new file mode 100644 index 000000000..1528d4542 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmStakeVoteRegistrationDelegationContainer.test.tsx @@ -0,0 +1,197 @@ +/* eslint-disable unicorn/no-null */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable import/imports-first */ +const mockUseWalletStore = jest.fn(); +const t = jest.fn().mockImplementation((res) => res); +const mockUseTranslation = jest.fn(() => ({ t })); +const mockConfirmStakeVoteRegistrationDelegation = jest.fn(); +const mockIsDRepAlwaysAbstain = jest.fn(); +const mockIsDRepAlwaysNoConfidence = jest.fn(); +const mockIsDRepCredential = jest.fn(); +const mockLovelacesToAdaString = jest.fn(); +import * as React from 'react'; +import { cleanup, render } from '@testing-library/react'; +import { ConfirmStakeVoteRegistrationDelegationContainer } from '../ConfirmStakeVoteRegistrationDelegationContainer'; +import '@testing-library/jest-dom'; +import { BehaviorSubject } from 'rxjs'; +import { act } from 'react-dom/test-utils'; +import { buildMockTx } from '@src/utils/mocks/tx'; +import { Wallet } from '@lace/cardano'; +import { getWrapper } from '../testing.utils'; +import { depositPaidWithSymbol, drepIDasBech32FromHash } from '../utils'; +import { TransactionWitnessRequest } from '@cardano-sdk/web-extension'; + +const REWARD_ACCOUNT = Wallet.Cardano.RewardAccount('stake_test1uqrw9tjymlm8wrwq7jk68n6v7fs9qz8z0tkdkve26dylmfc2ux2hj'); +const STAKE_KEY_HASH = Wallet.Cardano.RewardAccount.toHash(REWARD_ACCOUNT); + +const assetInfo$ = new BehaviorSubject(new Map()); +const available$ = new BehaviorSubject([]); + +const inMemoryWallet = { + assetInfo$, + balance: { + utxo: { + available$ + } + } +}; + +const cardanoCoinMock = { + name: 'Cardano', + symbol: 'cardanoCoinMockSymbol' +}; + +const dappInfo = { + name: 'dappName', + logo: 'dappLogo', + url: 'dappUrl' +}; +const certificate: Wallet.Cardano.Certificate = { + __typename: Wallet.Cardano.CertificateType.StakeVoteRegistrationDelegation, + poolId: Wallet.Cardano.PoolId('pool126zlx7728y7xs08s8epg9qp393kyafy9rzr89g4qkvv4cv93zem'), + stakeCredential: { + type: Wallet.Cardano.CredentialType.KeyHash, + hash: Wallet.Crypto.Hash28ByteBase16(STAKE_KEY_HASH) + }, + dRep: { + type: Wallet.Cardano.CredentialType.KeyHash, + hash: Wallet.Crypto.Hash28ByteBase16(Buffer.from('dRepCredentialHashdRepCreden').toString('hex')) + }, + deposit: BigInt('100000') +}; +const tx = buildMockTx({ + certificates: [certificate] +}); + +const request = { + transaction: { + toCore: jest.fn().mockReturnValue(tx) + } as any +} as TransactionWitnessRequest; + +jest.mock('@providers', () => ({ + ...jest.requireActual('@providers'), + useViewsFlowContext: () => ({ + signTxRequest: { request }, + dappInfo + }) +})); + +jest.mock('@src/stores', () => ({ + ...jest.requireActual('@src/stores'), + useWalletStore: mockUseWalletStore +})); + +jest.mock('@lace/core', () => { + const original = jest.requireActual('@lace/core'); + return { + __esModule: true, + ...original, + ConfirmStakeVoteRegistrationDelegation: mockConfirmStakeVoteRegistrationDelegation + }; +}); + +jest.mock('react-i18next', () => { + const original = jest.requireActual('react-i18next'); + return { + __esModule: true, + ...original, + useTranslation: mockUseTranslation + }; +}); + +jest.mock('@lace/cardano', () => { + const actual = jest.requireActual('@lace/cardano'); + return { + __esModule: true, + ...actual, + Wallet: { + ...actual.Wallet, + util: { + ...actual.Wallet.util, + lovelacesToAdaString: mockLovelacesToAdaString + }, + Cardano: { + ...actual.Wallet.Cardano, + isDRepAlwaysAbstain: mockIsDRepAlwaysAbstain, + isDRepAlwaysNoConfidence: mockIsDRepAlwaysNoConfidence, + isDRepCredential: mockIsDRepCredential + } + } + }; +}); + +const isDRepAlwaysAbstainMocked = 'isDRepAlwaysAbstainMocked'; +const isDRepAlwaysNoConfidenceMocked = 'isDRepAlwaysNoConfidenceMocked'; + +describe('Testing ConfirmStakeVoteRegistrationDelegationContainer component', () => { + beforeEach(() => { + mockUseWalletStore.mockReset(); + mockUseWalletStore.mockImplementation(() => ({ + inMemoryWallet, + walletUI: { cardanoCoin: cardanoCoinMock }, + walletInfo: {}, + currentChain: { + networkId: 0 + } + })); + mockConfirmStakeVoteRegistrationDelegation.mockReset(); + mockConfirmStakeVoteRegistrationDelegation.mockReturnValue( + + ); + mockUseTranslation.mockReset(); + mockUseTranslation.mockImplementation(() => ({ t })); + mockLovelacesToAdaString.mockReset(); + mockLovelacesToAdaString.mockImplementation((val) => val); + mockIsDRepAlwaysAbstain.mockReset(); + mockIsDRepAlwaysAbstain.mockImplementation(() => isDRepAlwaysAbstainMocked); + mockIsDRepAlwaysNoConfidence.mockReset(); + mockIsDRepAlwaysNoConfidence.mockImplementation(() => isDRepAlwaysNoConfidenceMocked); + mockIsDRepCredential.mockReset(); + mockIsDRepCredential.mockImplementation(() => true); + }); + + afterEach(() => { + jest.resetModules(); + jest.resetAllMocks(); + cleanup(); + }); + + test('should render ConfirmStakeVoteRegistrationDelegation component with proper props', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render(, { + wrapper: getWrapper() + })); + }); + + expect(queryByTestId('ConfirmStakeVoteRegistrationDelegation')).toBeInTheDocument(); + expect(mockConfirmStakeVoteRegistrationDelegation).toHaveBeenLastCalledWith( + { + dappInfo, + metadata: { + poolId: certificate.poolId, + stakeKeyHash: 'stake_test1uqrw9tjymlm8wrwq7jk68n6v7fs9qz8z0tkdkve26dylmfc2ux2hj', + depositPaid: depositPaidWithSymbol(certificate.deposit, cardanoCoinMock as Wallet.CoinId), + alwaysAbstain: isDRepAlwaysAbstainMocked, + alwaysNoConfidence: isDRepAlwaysNoConfidenceMocked, + drepId: drepIDasBech32FromHash((certificate.dRep as Wallet.Cardano.Credential).hash) + }, + translations: { + metadata: t('core.StakeVoteDelegationRegistration.metadata'), + option: t('core.StakeVoteDelegationRegistration.option'), + labels: { + poolId: t('core.StakeVoteDelegationRegistration.poolId'), + stakeKeyHash: t('core.StakeVoteDelegationRegistration.stakeKeyHash'), + drepId: t('core.StakeVoteDelegationRegistration.drepId'), + alwaysAbstain: t('core.StakeVoteDelegationRegistration.alwaysAbstain'), + alwaysNoConfidence: t('core.StakeVoteDelegationRegistration.alwaysNoConfidence'), + depositPaid: t('core.StakeVoteDelegationRegistration.depositPaid') + } + } + }, + {} + ); + }); +}); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmTransaction.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmTransaction.test.tsx new file mode 100644 index 000000000..b7560a74d --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmTransaction.test.tsx @@ -0,0 +1,248 @@ +/* eslint-disable max-statements */ +/* eslint-disable sonarjs/no-identical-functions */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable import/imports-first */ +const mockGetKeyAgentType = jest.fn(); +const mockUseWalletStore = jest.fn(); +const mockExposeApi = jest.fn(() => ({ shutdown: jest.fn() })); +const mockConfirmTransactionContent = jest.fn(() => ); +const mockGetTxType = jest.fn(); +const mockUseDisallowSignTx = jest.fn(); +const mockUseViewsFlowContext = jest.fn(); +const mockUseSignWithHardwareWallet = jest.fn(); +const mockUseOnBeforeUnload = jest.fn(); +const mockCreateTxInspector = jest.fn().mockReturnValue(() => ({ minted: [] as any, burned: [] as any })); +import * as React from 'react'; +import { cleanup, render, act, fireEvent } from '@testing-library/react'; +import { ConfirmTransaction } from '../ConfirmTransaction'; +import '@testing-library/jest-dom'; +import { BehaviorSubject } from 'rxjs'; +import { Wallet } from '@lace/cardano'; +import { getWrapper } from '../testing.utils'; + +const assetInfo$ = new BehaviorSubject(new Map()); +const available$ = new BehaviorSubject([]); + +const assetProvider = { + getAsset: () => ({}), + getAssets: (): any[] => [] +}; +const inMemoryWallet = { + assetInfo$, + balance: { + utxo: { + available$ + } + } +}; + +jest.mock('@src/stores', () => ({ + ...jest.requireActual('@src/stores'), + useWalletStore: mockUseWalletStore +})); + +jest.mock('@stores', () => ({ + ...jest.requireActual('@stores'), + useWalletStore: mockUseWalletStore +})); + +jest.mock('@cardano-sdk/web-extension', () => { + const original = jest.requireActual('@cardano-sdk/web-extension'); + return { + __esModule: true, + ...original, + exposeApi: mockExposeApi + }; +}); + +jest.mock('@cardano-sdk/core', () => { + const original = jest.requireActual('@cardano-sdk/core'); + return { + __esModule: true, + ...original, + createTxInspector: mockCreateTxInspector + }; +}); + +jest.mock('@lace/common', () => { + const original = jest.requireActual('@lace/common'); + return { + __esModule: true, + ...original, + useSearchParams: () => ({}) + }; +}); + +jest.mock('../ConfirmTransactionContent', () => { + const original = jest.requireActual('../ConfirmTransactionContent'); + return { + __esModule: true, + ...original, + ConfirmTransactionContent: mockConfirmTransactionContent + }; +}); + +jest.mock('../utils.ts', () => { + const original = jest.requireActual('../utils.ts'); + return { + __esModule: true, + ...original, + getTxType: mockGetTxType + }; +}); + +jest.mock('../hooks.ts', () => { + const original = jest.requireActual('../hooks.ts'); + return { + __esModule: true, + ...original, + useDisallowSignTx: mockUseDisallowSignTx, + useSignWithHardwareWallet: mockUseSignWithHardwareWallet, + useOnBeforeUnload: mockUseOnBeforeUnload + }; +}); + +jest.mock('@providers/ViewFlowProvider', () => { + const original = jest.requireActual('@providers/ViewFlowProvider'); + return { + __esModule: true, + ...original, + useViewsFlowContext: mockUseViewsFlowContext + }; +}); + +const testIds = { + dappTransactionConfirm: 'dapp-transaction-confirm', + layoutTitle: 'layout-title', + dappTransactionCancel: 'dapp-transaction-cancel' +}; + +describe('Testing ConfirmTransaction component', () => { + window.ResizeObserver = ResizeObserver; + + beforeEach(() => { + mockUseSignWithHardwareWallet.mockReset(); + mockUseSignWithHardwareWallet.mockReturnValue({}); + mockUseViewsFlowContext.mockReset(); + mockUseViewsFlowContext.mockReturnValue({ + utils: {}, + signTxRequest: { + request: { + transaction: { + toCore: jest.fn().mockReturnValue({ id: 'test-tx-id' }), + getId: jest.fn().mockReturnValue({ id: 'test-tx-id' }) + } + } + } + }); + mockConfirmTransactionContent.mockReset(); + mockConfirmTransactionContent.mockImplementation(() => ); + }); + + afterEach(() => { + cleanup(); + }); + + test('Should render proper state for inMemory wallet', async () => { + let queryByTestId: any; + + const txType = 'txType'; + mockGetKeyAgentType.mockReset(); + mockGetKeyAgentType.mockReturnValue(Wallet.KeyManagement.KeyAgentType.InMemory); + mockUseWalletStore.mockReset(); + mockUseWalletStore.mockImplementation(() => ({ + getKeyAgentType: mockGetKeyAgentType, + inMemoryWallet, + walletUI: {}, + walletInfo: {}, + blockchainProvider: { assetProvider } + })); + mockGetTxType.mockReset(); + mockGetTxType.mockReturnValue(txType); + + const signTxData = { tx: { id: 'test-tx-id' } }; + const disallowSignTx = jest.fn(); + mockUseDisallowSignTx.mockReset(); + mockUseDisallowSignTx.mockReturnValue(disallowSignTx); + const setNextViewMock = jest.fn(); + mockUseViewsFlowContext.mockReset(); + mockUseViewsFlowContext.mockReturnValue({ + utils: { setNextView: setNextViewMock }, + signTxRequest: { + request: { + transaction: { + getId: jest.fn().mockReturnValue({ id: 'test-tx-id' }), + toCore: jest.fn().mockReturnValue(signTxData.tx) + } + } + } + }); + + await act(async () => { + ({ queryByTestId } = render(, { + wrapper: getWrapper() + })); + }); + + expect(queryByTestId(testIds.layoutTitle)).toHaveTextContent(txType); + expect(queryByTestId('ConfirmTransactionContent')).toBeInTheDocument(); + expect(mockConfirmTransactionContent).toHaveBeenLastCalledWith( + { + txType, + onError: expect.any(Function) + }, + {} + ); + expect(mockUseOnBeforeUnload).toHaveBeenCalledWith(disallowSignTx); + expect(queryByTestId(testIds.dappTransactionConfirm)).toHaveTextContent('Confirm'); + expect(queryByTestId(testIds.dappTransactionConfirm)).not.toBeDisabled(); + expect(queryByTestId(testIds.dappTransactionCancel)).toHaveTextContent('Cancel'); + + await act(async () => { + fireEvent.click(queryByTestId(testIds.dappTransactionCancel)); + }); + + expect(disallowSignTx).toHaveBeenCalledWith(true); + + await act(async () => { + fireEvent.click(queryByTestId(testIds.dappTransactionConfirm)); + }); + + expect(setNextViewMock).toHaveBeenCalled(); + }); + + test('Should render proper state for hardware wallet', async () => { + let queryByTestId: any; + + mockGetKeyAgentType.mockReset(); + mockGetKeyAgentType.mockReturnValue(Wallet.KeyManagement.KeyAgentType.Ledger); + mockUseWalletStore.mockReset(); + mockUseWalletStore.mockImplementation(() => ({ + getKeyAgentType: mockGetKeyAgentType, + inMemoryWallet, + isHardwareWallet: true, + walletType: 'Ledger', + walletUI: {}, + walletInfo: {}, + blockchainProvider: { assetProvider } + })); + + const signWithHardwareWalletMock = jest.fn(); + mockUseSignWithHardwareWallet.mockReset(); + mockUseSignWithHardwareWallet.mockReturnValue({ signWithHardwareWallet: signWithHardwareWalletMock }); + + await act(async () => { + ({ queryByTestId } = render(, { + wrapper: getWrapper() + })); + }); + + expect(queryByTestId(testIds.dappTransactionConfirm)).toHaveTextContent('Confirm with Ledger'); + + await act(async () => { + fireEvent.click(queryByTestId(testIds.dappTransactionConfirm)); + }); + + expect(signWithHardwareWalletMock).toHaveBeenCalled(); + }); +}); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmTransactionContent.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmTransactionContent.test.tsx new file mode 100644 index 000000000..0327b5654 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmTransactionContent.test.tsx @@ -0,0 +1,399 @@ +/* eslint-disable unicorn/no-null */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable import/imports-first */ +import { Wallet } from '@lace/cardano'; + +const mockSkeleton = jest.fn(() => ); +const mockConfirmDRepRegistrationContainer = jest.fn(() => ); +const mockConfirmDRepRetirementContainer = jest.fn(() => ); +const mockConfirmDRepUpdateContainer = jest.fn(() => ); +const mockConfirmVoteDelegationContainer = jest.fn(() => ); +const mockVotingProceduresContainer = jest.fn(() => ); +const mockProposalProceduresContainer = jest.fn(() => ); +const mockConfirmVoteRegistrationDelegationContainer = jest.fn(() => ( + +)); +const mockConfirmStakeRegistrationDelegationContainer = jest.fn(() => ( + +)); +const mockConfirmStakeVoteRegistrationDelegationContainer = jest.fn(() => ( + +)); +const mockConfirmStakeVoteDelegationContainer = jest.fn(() => ( + +)); +const mockDappTransactionContainer = jest.fn(() => ); +import * as React from 'react'; +import { cleanup, render } from '@testing-library/react'; +import { ConfirmTransactionContent } from '../ConfirmTransactionContent'; +import '@testing-library/jest-dom'; +import { act } from 'react-dom/test-utils'; + +jest.mock('antd', () => { + const original = jest.requireActual('antd'); + return { + __esModule: true, + ...original, + Skeleton: mockSkeleton + }; +}); + +jest.mock('../ConfirmDRepRegistrationContainer', () => { + const original = jest.requireActual('../ConfirmDRepRegistrationContainer'); + return { + __esModule: true, + ...original, + ConfirmDRepRegistrationContainer: mockConfirmDRepRegistrationContainer + }; +}); + +jest.mock('../ConfirmDRepRetirementContainer', () => { + const original = jest.requireActual('../ConfirmDRepRetirementContainer'); + return { + __esModule: true, + ...original, + ConfirmDRepRetirementContainer: mockConfirmDRepRetirementContainer + }; +}); + +jest.mock('../ConfirmDRepUpdateContainer', () => { + const original = jest.requireActual('../ConfirmDRepUpdateContainer'); + return { + __esModule: true, + ...original, + ConfirmDRepUpdateContainer: mockConfirmDRepUpdateContainer + }; +}); + +jest.mock('../ConfirmVoteDelegationContainer', () => { + const original = jest.requireActual('../ConfirmVoteDelegationContainer'); + return { + __esModule: true, + ...original, + ConfirmVoteDelegationContainer: mockConfirmVoteDelegationContainer + }; +}); + +jest.mock('../VotingProceduresContainer', () => { + const original = jest.requireActual('../VotingProceduresContainer'); + return { + __esModule: true, + ...original, + VotingProceduresContainer: mockVotingProceduresContainer + }; +}); + +jest.mock('../ProposalProceduresContainer', () => { + const original = jest.requireActual('../ProposalProceduresContainer'); + return { + __esModule: true, + ...original, + ProposalProceduresContainer: mockProposalProceduresContainer + }; +}); + +jest.mock('../ConfirmVoteRegistrationDelegationContainer', () => { + const original = jest.requireActual('../ConfirmVoteRegistrationDelegationContainer'); + return { + __esModule: true, + ...original, + ConfirmVoteRegistrationDelegationContainer: mockConfirmVoteRegistrationDelegationContainer + }; +}); + +jest.mock('../ConfirmStakeRegistrationDelegationContainer', () => { + const original = jest.requireActual('../ConfirmStakeRegistrationDelegationContainer'); + return { + __esModule: true, + ...original, + ConfirmStakeRegistrationDelegationContainer: mockConfirmStakeRegistrationDelegationContainer + }; +}); + +jest.mock('../ConfirmStakeVoteRegistrationDelegationContainer', () => { + const original = jest.requireActual('../ConfirmStakeVoteRegistrationDelegationContainer'); + return { + __esModule: true, + ...original, + ConfirmStakeVoteRegistrationDelegationContainer: mockConfirmStakeVoteRegistrationDelegationContainer + }; +}); + +jest.mock('../ConfirmStakeVoteDelegationContainer', () => { + const original = jest.requireActual('../ConfirmStakeVoteDelegationContainer'); + return { + __esModule: true, + ...original, + ConfirmStakeVoteDelegationContainer: mockConfirmStakeVoteDelegationContainer + }; +}); + +jest.mock('../DappTransactionContainer', () => { + const original = jest.requireActual('../DappTransactionContainer'); + return { + __esModule: true, + ...original, + DappTransactionContainer: mockDappTransactionContainer + }; +}); + +describe('Testing ConfirmTransactionContent component', () => { + afterEach(() => { + cleanup(); + }); + + test('should render ConfirmDRepRegistrationContainer with proper props', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render()); + }); + + expect(queryByTestId('skeleton')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepRegistrationContainer')).toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepRetirementContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepUpdateContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmVoteDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('VotingProceduresContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ProposalProceduresContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmVoteRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeVoteRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeVoteDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('DappTransactionContainer')).not.toBeInTheDocument(); + expect(mockConfirmDRepRegistrationContainer).toHaveBeenLastCalledWith({}, {}); + }); + + test('should render ConfirmDRepRetirementContainer with proper props', async () => { + let queryByTestId: any; + const props = { onError: jest.fn() }; + + await act(async () => { + ({ queryByTestId } = render( + + )); + }); + + expect(queryByTestId('skeleton')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepRegistrationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepRetirementContainer')).toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepUpdateContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmVoteDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('VotingProceduresContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ProposalProceduresContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmVoteRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeVoteRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeVoteDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('DappTransactionContainer')).not.toBeInTheDocument(); + expect(mockConfirmDRepRetirementContainer).toHaveBeenLastCalledWith(props, {}); + }); + + test('should render ConfirmDRepUpdateContainer with proper props', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render()); + }); + + expect(queryByTestId('skeleton')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepRegistrationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepRetirementContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepUpdateContainer')).toBeInTheDocument(); + expect(queryByTestId('ConfirmVoteDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('VotingProceduresContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ProposalProceduresContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmVoteRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeVoteRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeVoteDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('DappTransactionContainer')).not.toBeInTheDocument(); + expect(mockConfirmDRepUpdateContainer).toHaveBeenLastCalledWith({}, {}); + }); + + test('should render ConfirmVoteDelegationContainer with proper props', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render()); + }); + + expect(queryByTestId('skeleton')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepRegistrationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepRetirementContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepUpdateContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmVoteDelegationContainer')).toBeInTheDocument(); + expect(queryByTestId('VotingProceduresContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ProposalProceduresContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmVoteRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeVoteRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeVoteDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('DappTransactionContainer')).not.toBeInTheDocument(); + expect(mockConfirmVoteDelegationContainer).toHaveBeenLastCalledWith({}, {}); + }); + + test('should render VotingProceduresContainer with proper props', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render()); + }); + + expect(queryByTestId('skeleton')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepRegistrationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepRetirementContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepUpdateContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmVoteDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('VotingProceduresContainer')).toBeInTheDocument(); + expect(queryByTestId('ProposalProceduresContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmVoteRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeVoteRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeVoteDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('DappTransactionContainer')).not.toBeInTheDocument(); + expect(mockVotingProceduresContainer).toHaveBeenLastCalledWith({}, {}); + }); + + test('should render ProposalProceduresContainer with proper props', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render( + + )); + }); + + expect(queryByTestId('skeleton')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepRegistrationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepRetirementContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepUpdateContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmVoteDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('VotingProceduresContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ProposalProceduresContainer')).toBeInTheDocument(); + expect(queryByTestId('ConfirmVoteRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeVoteRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeVoteDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('DappTransactionContainer')).not.toBeInTheDocument(); + expect(mockProposalProceduresContainer).toHaveBeenLastCalledWith({}, {}); + }); + + test('should render ConfirmVoteRegistrationDelegationContainer with proper props', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render( + + )); + }); + + expect(queryByTestId('skeleton')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepRegistrationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepRetirementContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepUpdateContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmVoteDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('VotingProceduresContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ProposalProceduresContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmVoteRegistrationDelegationContainer')).toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeVoteRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeVoteDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('DappTransactionContainer')).not.toBeInTheDocument(); + expect(mockConfirmVoteRegistrationDelegationContainer).toHaveBeenLastCalledWith({}, {}); + }); + + test('should render ConfirmStakeRegistrationDelegationContainer with proper props', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render( + + )); + }); + + expect(queryByTestId('skeleton')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepRegistrationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepRetirementContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepUpdateContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmVoteDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('VotingProceduresContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ProposalProceduresContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmVoteRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeRegistrationDelegationContainer')).toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeVoteRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeVoteDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('DappTransactionContainer')).not.toBeInTheDocument(); + expect(mockConfirmStakeRegistrationDelegationContainer).toHaveBeenLastCalledWith({}, {}); + }); + + test('should render ConfirmStakeVoteRegistrationDelegationContainer with proper props', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render( + + )); + }); + + expect(queryByTestId('skeleton')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepRegistrationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepRetirementContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepUpdateContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmVoteDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('VotingProceduresContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ProposalProceduresContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmVoteRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeVoteRegistrationDelegationContainer')).toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeVoteDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('DappTransactionContainer')).not.toBeInTheDocument(); + expect(mockConfirmStakeVoteRegistrationDelegationContainer).toHaveBeenLastCalledWith({}, {}); + }); + + test('should render ConfirmStakeVoteDelegationContainer with proper props', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render( + + )); + }); + + expect(queryByTestId('skeleton')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepRegistrationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepRetirementContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepUpdateContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmVoteDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('VotingProceduresContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ProposalProceduresContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmVoteRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeVoteRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeVoteDelegationContainer')).toBeInTheDocument(); + expect(queryByTestId('DappTransactionContainer')).not.toBeInTheDocument(); + expect(mockConfirmStakeVoteDelegationContainer).toHaveBeenLastCalledWith({}, {}); + }); + + test('should render DappTransactionContainer with proper props', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render()); + }); + + expect(queryByTestId('skeleton')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepRegistrationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepRetirementContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmDRepUpdateContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmVoteDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('VotingProceduresContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ProposalProceduresContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmVoteRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeVoteRegistrationDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('ConfirmStakeVoteDelegationContainer')).not.toBeInTheDocument(); + expect(queryByTestId('DappTransactionContainer')).toBeInTheDocument(); + expect(mockDappTransactionContainer).toHaveBeenLastCalledWith({}, {}); + }); +}); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmVoteDelegationContainer.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmVoteDelegationContainer.test.tsx new file mode 100644 index 000000000..0f2083c18 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmVoteDelegationContainer.test.tsx @@ -0,0 +1,262 @@ +/* eslint-disable unicorn/no-null */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable import/imports-first */ +/* eslint-disable sonarjs/no-identical-functions */ +const mockUseWalletStore = jest.fn(); +const t = jest.fn().mockImplementation((res) => res); +const mockUseTranslation = jest.fn(() => ({ t })); +const mockConfirmVoteDelegation = jest.fn(); +const mockUseViewsFlowContext = jest.fn(); +import * as React from 'react'; +import { cleanup, render } from '@testing-library/react'; +import { ConfirmVoteDelegationContainer } from '../ConfirmVoteDelegationContainer'; +import '@testing-library/jest-dom'; +import { BehaviorSubject } from 'rxjs'; +import { act } from 'react-dom/test-utils'; +import { buildMockTx } from '@src/utils/mocks/tx'; +import { Wallet } from '@lace/cardano'; +import { getWrapper } from '../testing.utils'; +import { drepIDasBech32FromHash } from '../utils'; + +const REWARD_ACCOUNT = Wallet.Cardano.RewardAccount('stake_test1uqrw9tjymlm8wrwq7jk68n6v7fs9qz8z0tkdkve26dylmfc2ux2hj'); +const STAKE_KEY_HASH = Wallet.Cardano.RewardAccount.toHash(REWARD_ACCOUNT); + +const assetInfo$ = new BehaviorSubject(new Map()); +const available$ = new BehaviorSubject([]); + +const inMemoryWallet = { + assetInfo$, + balance: { + utxo: { + available$ + } + } +}; + +const cardanoCoinMock = { + name: 'Cardano', + symbol: 'cardanoCoinMockSymbol' +}; + +const dappInfo = { + name: 'dappName', + logo: 'dappLogo', + url: 'dappUrl' +}; + +jest.mock('@providers', () => ({ + ...jest.requireActual('@providers'), + useViewsFlowContext: mockUseViewsFlowContext +})); + +jest.mock('@src/stores', () => ({ + ...jest.requireActual('@src/stores'), + useWalletStore: mockUseWalletStore +})); + +jest.mock('@lace/core', () => { + const original = jest.requireActual('@lace/core'); + return { + __esModule: true, + ...original, + ConfirmVoteDelegation: mockConfirmVoteDelegation + }; +}); + +jest.mock('react-i18next', () => { + const original = jest.requireActual('react-i18next'); + return { + __esModule: true, + ...original, + useTranslation: mockUseTranslation + }; +}); + +describe('Testing ConfirmVoteDelegationContainer component', () => { + beforeEach(() => { + mockUseWalletStore.mockReset(); + mockUseWalletStore.mockImplementation(() => ({ + inMemoryWallet, + walletUI: { cardanoCoin: cardanoCoinMock }, + walletInfo: {} + })); + mockUseViewsFlowContext.mockReset(); + mockConfirmVoteDelegation.mockReset(); + mockConfirmVoteDelegation.mockReturnValue(); + mockUseTranslation.mockReset(); + mockUseTranslation.mockImplementation(() => ({ t })); + }); + + afterEach(() => { + jest.resetModules(); + jest.resetAllMocks(); + cleanup(); + }); + + test('should render ConfirmVoteDelegation component with DRep ID', async () => { + let queryByTestId: any; + const certificate: Wallet.Cardano.Certificate = { + __typename: Wallet.Cardano.CertificateType.VoteDelegation, + dRep: { + type: Wallet.Cardano.CredentialType.KeyHash, + hash: Wallet.Crypto.Hash28ByteBase16(Buffer.from('dRepCredentialHashdRepCreden').toString('hex')) + }, + stakeCredential: { + type: Wallet.Cardano.CredentialType.KeyHash, + hash: Wallet.Crypto.Hash28ByteBase16(STAKE_KEY_HASH) + } + }; + + mockUseViewsFlowContext.mockImplementation(() => ({ + signTxRequest: { + request: { + transaction: { + toCore: jest.fn().mockReturnValue( + buildMockTx({ + certificates: [certificate] + }) + ) + } + } + }, + dappInfo + })); + const dRep = certificate.dRep; + + await act(async () => { + ({ queryByTestId } = render(, { + wrapper: getWrapper() + })); + }); + + expect(queryByTestId('ConfirmVoteDelegation')).toBeInTheDocument(); + expect(mockConfirmVoteDelegation).toHaveBeenLastCalledWith( + { + dappInfo, + metadata: { + alwaysAbstain: false, + alwaysNoConfidence: false, + drepId: drepIDasBech32FromHash((dRep as unknown as Wallet.Cardano.Credential).hash) + }, + translations: { + metadata: t('core.VoteDelegation.metadata'), + option: t('core.VoteDelegation.option'), + labels: { + drepId: t('core.VoteDelegation.drepId'), + alwaysAbstain: t('core.VoteDelegation.alwaysAbstain'), + alwaysNoConfidence: t('core.VoteDelegation.alwaysNoConfidence') + } + } + }, + {} + ); + }); + + test('should render ConfirmVoteDelegation component with AlwaysAbstain', async () => { + let queryByTestId: any; + const certificate: Wallet.Cardano.Certificate = { + __typename: Wallet.Cardano.CertificateType.VoteDelegation, + dRep: { __typename: 'AlwaysAbstain' }, + stakeCredential: { + type: Wallet.Cardano.CredentialType.KeyHash, + hash: Wallet.Crypto.Hash28ByteBase16(STAKE_KEY_HASH) + } + }; + + mockUseViewsFlowContext.mockImplementation(() => ({ + signTxRequest: { + request: { + transaction: { + toCore: jest.fn().mockReturnValue( + buildMockTx({ + certificates: [certificate] + }) + ) + } + } + }, + dappInfo + })); + + await act(async () => { + ({ queryByTestId } = render(, { + wrapper: getWrapper() + })); + }); + + expect(queryByTestId('ConfirmVoteDelegation')).toBeInTheDocument(); + expect(mockConfirmVoteDelegation).toHaveBeenLastCalledWith( + { + dappInfo, + metadata: { + alwaysAbstain: true, + alwaysNoConfidence: false + }, + translations: { + metadata: t('core.VoteDelegation.metadata'), + option: t('core.VoteDelegation.option'), + labels: { + drepId: t('core.VoteDelegation.drepId'), + alwaysAbstain: t('core.VoteDelegation.alwaysAbstain'), + alwaysNoConfidence: t('core.VoteDelegation.alwaysNoConfidence') + } + } + }, + {} + ); + }); + + test('should render ConfirmVoteDelegation component with AlwaysAbstain', async () => { + let queryByTestId: any; + const certificate: Wallet.Cardano.Certificate = { + __typename: Wallet.Cardano.CertificateType.VoteDelegation, + dRep: { __typename: 'AlwaysNoConfidence' }, + stakeCredential: { + type: Wallet.Cardano.CredentialType.KeyHash, + hash: Wallet.Crypto.Hash28ByteBase16(STAKE_KEY_HASH) + } + }; + + mockUseViewsFlowContext.mockImplementation(() => ({ + signTxRequest: { + request: { + transaction: { + toCore: jest.fn().mockReturnValue( + buildMockTx({ + certificates: [certificate] + }) + ) + } + } + }, + dappInfo + })); + + await act(async () => { + ({ queryByTestId } = render(, { + wrapper: getWrapper() + })); + }); + + expect(queryByTestId('ConfirmVoteDelegation')).toBeInTheDocument(); + expect(mockConfirmVoteDelegation).toHaveBeenLastCalledWith( + { + dappInfo, + metadata: { + alwaysAbstain: false, + alwaysNoConfidence: true + }, + translations: { + metadata: t('core.VoteDelegation.metadata'), + option: t('core.VoteDelegation.option'), + labels: { + drepId: t('core.VoteDelegation.drepId'), + alwaysAbstain: t('core.VoteDelegation.alwaysAbstain'), + alwaysNoConfidence: t('core.VoteDelegation.alwaysNoConfidence') + } + } + }, + {} + ); + }); +}); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmVoteRegistrationDelegationContainer.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmVoteRegistrationDelegationContainer.test.tsx new file mode 100644 index 000000000..cf88de426 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ConfirmVoteRegistrationDelegationContainer.test.tsx @@ -0,0 +1,193 @@ +/* eslint-disable unicorn/no-null */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable import/imports-first */ +const mockUseWalletStore = jest.fn(); +const t = jest.fn().mockImplementation((res) => res); +const mockUseTranslation = jest.fn(() => ({ t })); +const mockConfirmVoteRegistrationDelegation = jest.fn(); +const mockIsDRepAlwaysAbstain = jest.fn(); +const mockIsDRepAlwaysNoConfidence = jest.fn(); +const mockIsDRepCredential = jest.fn(); +const mockLovelacesToAdaString = jest.fn(); + +import * as React from 'react'; +import { cleanup, render } from '@testing-library/react'; +import { ConfirmVoteRegistrationDelegationContainer } from '../ConfirmVoteRegistrationDelegationContainer'; +import '@testing-library/jest-dom'; +import { BehaviorSubject } from 'rxjs'; +import { act } from 'react-dom/test-utils'; +import { buildMockTx } from '@src/utils/mocks/tx'; +import { Wallet } from '@lace/cardano'; +import { getWrapper } from '../testing.utils'; +import { depositPaidWithSymbol, drepIDasBech32FromHash } from '../utils'; +import { TransactionWitnessRequest } from '@cardano-sdk/web-extension'; + +const REWARD_ACCOUNT = Wallet.Cardano.RewardAccount('stake_test1uqrw9tjymlm8wrwq7jk68n6v7fs9qz8z0tkdkve26dylmfc2ux2hj'); +const STAKE_KEY_HASH = Wallet.Cardano.RewardAccount.toHash(REWARD_ACCOUNT); + +const assetInfo$ = new BehaviorSubject(new Map()); +const available$ = new BehaviorSubject([]); + +const inMemoryWallet = { + assetInfo$, + balance: { + utxo: { + available$ + } + } +}; + +const cardanoCoinMock = { + name: 'Cardano', + symbol: 'cardanoCoinMockSymbol' +}; + +const dappInfo = { + name: 'dappName', + logo: 'dappLogo', + url: 'dappUrl' +}; +const certificate: Wallet.Cardano.Certificate = { + __typename: Wallet.Cardano.CertificateType.VoteRegistrationDelegation, + stakeCredential: { + type: Wallet.Cardano.CredentialType.KeyHash, + hash: Wallet.Crypto.Hash28ByteBase16(STAKE_KEY_HASH) + }, + dRep: { + type: Wallet.Cardano.CredentialType.KeyHash, + hash: Wallet.Crypto.Hash28ByteBase16(Buffer.from('dRepCredentialHashdRepCreden').toString('hex')) + }, + deposit: BigInt('100000') +}; +const tx = buildMockTx({ + certificates: [certificate] +}); + +const request = { + transaction: { + toCore: jest.fn().mockReturnValue(tx) + } as any +} as TransactionWitnessRequest; + +jest.mock('@providers', () => ({ + ...jest.requireActual('@providers'), + useViewsFlowContext: () => ({ + signTxRequest: { request }, + dappInfo + }) +})); + +jest.mock('@src/stores', () => ({ + ...jest.requireActual('@src/stores'), + useWalletStore: mockUseWalletStore +})); + +jest.mock('@lace/core', () => { + const original = jest.requireActual('@lace/core'); + return { + __esModule: true, + ...original, + ConfirmVoteRegistrationDelegation: mockConfirmVoteRegistrationDelegation + }; +}); + +jest.mock('react-i18next', () => { + const original = jest.requireActual('react-i18next'); + return { + __esModule: true, + ...original, + useTranslation: mockUseTranslation + }; +}); + +jest.mock('@lace/cardano', () => { + const actual = jest.requireActual('@lace/cardano'); + return { + __esModule: true, + ...actual, + Wallet: { + ...actual.Wallet, + util: { + ...actual.Wallet.util, + lovelacesToAdaString: mockLovelacesToAdaString + }, + Cardano: { + ...actual.Wallet.Cardano, + isDRepAlwaysAbstain: mockIsDRepAlwaysAbstain, + isDRepAlwaysNoConfidence: mockIsDRepAlwaysNoConfidence, + isDRepCredential: mockIsDRepCredential + } + } + }; +}); + +const isDRepAlwaysAbstainMocked = 'isDRepAlwaysAbstainMocked'; +const isDRepAlwaysNoConfidenceMocked = 'isDRepAlwaysNoConfidenceMocked'; + +describe('Testing ConfirmVoteRegistrationDelegationContainer component', () => { + beforeEach(() => { + mockUseWalletStore.mockReset(); + mockUseWalletStore.mockImplementation(() => ({ + inMemoryWallet, + walletUI: { cardanoCoin: cardanoCoinMock }, + walletInfo: {}, + currentChain: { + networkId: 0 + } + })); + mockConfirmVoteRegistrationDelegation.mockReset(); + mockConfirmVoteRegistrationDelegation.mockReturnValue(); + mockUseTranslation.mockReset(); + mockUseTranslation.mockImplementation(() => ({ t })); + mockLovelacesToAdaString.mockReset(); + mockLovelacesToAdaString.mockImplementation((val) => val); + mockIsDRepAlwaysAbstain.mockReset(); + mockIsDRepAlwaysAbstain.mockImplementation(() => isDRepAlwaysAbstainMocked); + mockIsDRepAlwaysNoConfidence.mockReset(); + mockIsDRepAlwaysNoConfidence.mockImplementation(() => isDRepAlwaysNoConfidenceMocked); + mockIsDRepCredential.mockReset(); + mockIsDRepCredential.mockImplementation(() => true); + }); + + afterEach(() => { + jest.resetModules(); + jest.resetAllMocks(); + cleanup(); + }); + + test('should render ConfirmVoteRegistrationDelegation component with proper props', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render(, { + wrapper: getWrapper() + })); + }); + + expect(queryByTestId('ConfirmVoteRegistrationDelegation')).toBeInTheDocument(); + expect(mockConfirmVoteRegistrationDelegation).toHaveBeenLastCalledWith( + { + dappInfo, + metadata: { + depositPaid: depositPaidWithSymbol(certificate.deposit, cardanoCoinMock as Wallet.CoinId), + stakeKeyHash: 'stake_test1uqrw9tjymlm8wrwq7jk68n6v7fs9qz8z0tkdkve26dylmfc2ux2hj', + alwaysAbstain: isDRepAlwaysAbstainMocked, + alwaysNoConfidence: isDRepAlwaysNoConfidenceMocked, + drepId: drepIDasBech32FromHash((certificate.dRep as Wallet.Cardano.Credential).hash) + }, + translations: { + metadata: t('core.VoteRegistrationDelegation.metadata'), + option: t('core.VoteRegistrationDelegation.option'), + labels: { + drepId: t('core.VoteRegistrationDelegation.drepId'), + alwaysAbstain: t('core.VoteRegistrationDelegation.alwaysAbstain'), + alwaysNoConfidence: t('core.VoteRegistrationDelegation.alwaysNoConfidence'), + depositPaid: t('core.VoteRegistrationDelegation.depositPaid'), + stakeKeyHash: t('core.VoteRegistrationDelegation.stakeKeyHash') + } + } + }, + {} + ); + }); +}); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/DappTransactionContainer.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/DappTransactionContainer.test.tsx new file mode 100644 index 000000000..0922f14c4 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/DappTransactionContainer.test.tsx @@ -0,0 +1,305 @@ +/* eslint-disable no-magic-numbers */ +/* eslint-disable sonarjs/no-identical-functions */ +/* eslint-disable unicorn/no-null */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable import/imports-first */ +import * as CurrencyProvider from '@providers/currency'; +import * as UseFetchCoinPrice from '@hooks/useFetchCoinPrice'; +import * as UseComputeTxCollateral from '@hooks/useComputeTxCollateral'; +import * as GetAssetsInformation from '@src/utils/get-assets-information'; + +const mockSkeleton = jest.fn(() => ); +const mockUseWalletStore = jest.fn(); +const t = jest.fn().mockImplementation((res) => res); +const mockUseTranslation = jest.fn(() => ({ t })); +const mockDappTransaction = jest.fn(); +const mockUseViewsFlowContext = jest.fn(); +const mockGetAssetsInformation = jest.fn().mockReturnValue(Promise.resolve(new Map())); +const mockWithAddressBookContext = jest.fn((children) => children); +const mockUseCurrencyStore = jest.fn().mockReturnValue({ fiatCurrency: { code: 'usd', symbol: '$' } }); +const mockUseFetchCoinPrice = jest.fn().mockReturnValue({ priceResult: { cardano: { price: 2 }, tokens: new Map() } }); +const mockUseComputeTxCollateral = jest.fn().mockReturnValue(BigInt(1_000_000)); +import * as React from 'react'; +import { cleanup, render } from '@testing-library/react'; +import { DappTransactionContainer } from '../DappTransactionContainer'; +import '@testing-library/jest-dom'; +import { BehaviorSubject } from 'rxjs'; +import { act } from 'react-dom/test-utils'; +import { buildMockTx } from '@src/utils/mocks/tx'; +import { Wallet } from '@lace/cardano'; +import { SignTxData } from '../types'; +import { getWrapper } from '../testing.utils'; +import { TransactionWitnessRequest } from '@cardano-sdk/web-extension'; +import { cardanoCoin } from '@src/utils/constants'; + +const { Cardano, Crypto } = Wallet; + +const assetProvider = { + getAssets: jest.fn(() => ['assets']) +}; +const walletInfo = { + name: 'wall', + addresses: [{ address: 'address' }] +}; +const mockedAssetsInfo = new Map([['id', 'data']]); +const assetInfo$ = new BehaviorSubject(mockedAssetsInfo); +const available$ = new BehaviorSubject([]); + +const inMemoryWallet = { + assetInfo$, + balance: { + utxo: { + available$ + } + } +}; + +jest.mock('@src/stores', () => ({ + ...jest.requireActual('@src/stores'), + useWalletStore: mockUseWalletStore +})); + +jest.mock('@hooks/useFetchCoinPrice', (): typeof UseFetchCoinPrice => ({ + ...jest.requireActual('@hooks/useFetchCoinPrice'), + useFetchCoinPrice: mockUseFetchCoinPrice +})); + +jest.mock('@hooks/useComputeTxCollateral', (): typeof UseComputeTxCollateral => ({ + ...jest.requireActual('@hooks/useComputeTxCollateral'), + useComputeTxCollateral: mockUseComputeTxCollateral +})); + +jest.mock('@src/utils/get-assets-information', (): typeof GetAssetsInformation => ({ + ...jest.requireActual('@src/utils/get-assets-information'), + getAssetsInformation: mockGetAssetsInformation +})); + +jest.mock('@providers/currency', (): typeof CurrencyProvider => ({ + ...jest.requireActual('@providers/currency'), + useCurrencyStore: mockUseCurrencyStore +})); + +jest.mock('@lace/core', () => { + const original = jest.requireActual('@lace/core'); + return { + __esModule: true, + ...original, + DappTransaction: mockDappTransaction + }; +}); + +jest.mock('react-i18next', () => { + const original = jest.requireActual('react-i18next'); + return { + __esModule: true, + ...original, + useTranslation: mockUseTranslation + }; +}); + +const addressList = ['addressList']; +jest.mock('@src/features/address-book/context', () => ({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...jest.requireActual('@src/features/address-book/context'), + withAddressBookContext: mockWithAddressBookContext, + useAddressBookContext: () => ({ list: addressList }) +})); + +jest.mock('antd', () => { + const original = jest.requireActual('antd'); + return { + __esModule: true, + ...original, + Skeleton: mockSkeleton + }; +}); + +const dappInfo = { + name: 'dappName', + logo: 'dappLogo', + url: 'dappUrl' +}; + +const certificate: Wallet.Cardano.Certificate = { + __typename: Cardano.CertificateType.RegisterDelegateRepresentative, + dRepCredential: { + type: Cardano.CredentialType.KeyHash, + hash: Crypto.Hash28ByteBase16(Buffer.from('dRepCredentialHashdRepCreden').toString('hex')) + }, + deposit: BigInt('1000'), + anchor: { + url: 'anchorUrl', + dataHash: Crypto.Hash32ByteBase16(Buffer.from('anchorDataHashanchorDataHashanch').toString('hex')) + } +}; +const tx = buildMockTx({ + certificates: [certificate] +}); + +const request = { + transaction: { + toCore: jest.fn().mockReturnValue(tx) + } as any +} as TransactionWitnessRequest; + +jest.mock('@providers', () => ({ + ...jest.requireActual('@providers'), + useViewsFlowContext: mockUseViewsFlowContext +})); + +describe('Testing DappTransactionContainer component', () => { + beforeEach(() => { + mockUseWalletStore.mockReset(); + mockUseWalletStore.mockImplementation(() => ({ + inMemoryWallet, + blockchainProvider: { assetProvider }, + walletInfo, + walletUI: { cardanoCoin } + })); + mockDappTransaction.mockReset(); + mockDappTransaction.mockReturnValue(); + mockUseTranslation.mockReset(); + mockUseTranslation.mockImplementation(() => ({ t })); + mockWithAddressBookContext.mockReset(); + mockWithAddressBookContext.mockImplementation((children) => children); + mockSkeleton.mockReset(); + mockSkeleton.mockImplementation(() => ); + mockUseViewsFlowContext.mockReset(); + mockUseViewsFlowContext.mockImplementation(() => ({ + signTxRequest: { request }, + dappInfo + })); + }); + + afterEach(() => { + jest.resetModules(); + jest.resetAllMocks(); + cleanup(); + }); + + test('should render DappTransaction component with proper props', async () => { + let queryByTestId: any; + + const errorMessage = 'errorMessage'; + const props = { errorMessage }; + + const txSummary = { + burnedAssets: [], + collateral: '1.00', + fee: '0.17', + mintedAssets: [ + { + amount: '3', + name: 'asset1rqluyux4nxv6kjashz626c8usp8g88unmqwnyh', + ticker: 'asset1rqluyux4nxv6kjashz626c8usp8g88unmqwnyh' + } + ], + outputs: [ + { + coins: '5.00', + recipient: + 'addr_test1qpfhhfy2qgls50r9u4yh0l7z67xpg0a5rrhkmvzcuqrd0znuzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q9gw0lz' + }, + { + assets: [ + { + amount: '3', + name: '659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba8254534c41', + ticker: undefined + }, + { + amount: '4', + name: '6b8d07d69639e9413dd637a1a815a7323c69c86abbafb66dbfdb1aa7', + ticker: undefined + } + ], + coins: '2.00', + recipient: + 'addr_test1qpfhhfy2qgls50r9u4yh0l7z67xpg0a5rrhkmvzcuqrd0znuzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q9gw0lz' + }, + { + assets: [ + { + amount: '6', + name: '659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba8254534c41', + ticker: undefined + } + ], + coins: '2.00', + recipient: + 'addr_test1qpfhhfy2qgls50r9u4yh0l7z67xpg0a5rrhkmvzcuqrd0znuzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q9gw0lz' + }, + { + assets: [ + { + amount: '1', + name: '659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba8254534c41', + ticker: undefined + } + ], + coins: '2.00', + recipient: + 'addr_test1qq585l3hyxgj3nas2v3xymd23vvartfhceme6gv98aaeg9muzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q2g7k3g' + } + ], + type: 'Mint' + } as Wallet.Cip30SignTxSummary; + + await act(async () => { + ({ queryByTestId } = render(, { + wrapper: getWrapper() + })); + }); + + expect(queryByTestId('DappTransaction')).toBeInTheDocument(); + expect(mockDappTransaction).toHaveBeenLastCalledWith( + { + dappInfo, + transaction: txSummary, + fiatCurrencyCode: 'usd', + fiatCurrencyPrice: 2, + errorMessage, + coinSymbol: 'ADA' + }, + {} + ); + }); + + test('should render loader in case there is no tx data', async () => { + let queryByTestId: any; + + mockUseViewsFlowContext.mockReset(); + mockUseViewsFlowContext.mockImplementation(() => ({ + signTxRequest: {}, + dappInfo + })); + + const signTxData = { tx: { body: {} } } as unknown as SignTxData; + + await act(async () => { + ({ queryByTestId } = render(, { + wrapper: getWrapper() + })); + }); + + expect(queryByTestId('DappTransaction')).not.toBeInTheDocument(); + expect(queryByTestId('skeleton')).toBeInTheDocument(); + }); + + test('should render loader in case there is no txSummary', async () => { + let queryByTestId: any; + + mockUseCurrencyStore.mockRestore(); + + const signTxData = { tx: { body: {} } } as unknown as SignTxData; + + await act(async () => { + ({ queryByTestId } = render(, { + wrapper: getWrapper() + })); + }); + + expect(queryByTestId('DappTransaction')).not.toBeInTheDocument(); + expect(queryByTestId('skeleton')).toBeInTheDocument(); + }); +}); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ProposalProceduresContainer.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ProposalProceduresContainer.test.tsx new file mode 100644 index 000000000..21faa2faa --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/ProposalProceduresContainer.test.tsx @@ -0,0 +1,191 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-magic-numbers */ +/* eslint-disable import/imports-first */ +const mockHardForkInitiationActionContainer = jest.fn(() => ); +const mockInfoActionContainer = jest.fn(() => ); +const mockNewConstitutionActionContainer = jest.fn(() => ); +const mockNoConfidenceActionContainer = jest.fn(() => ); +const mockParameterChangeActionContainer = jest.fn(() => ); +const mockTreasuryWithdrawalsActionContainer = jest.fn(() => ); +const mockUpdateCommitteeActionContainer = jest.fn(() => ); +import { Wallet } from '@lace/cardano'; +import * as React from 'react'; +import { cleanup, render } from '@testing-library/react'; +import { ProposalProceduresContainer } from '../ProposalProceduresContainer'; +import '@testing-library/jest-dom'; +import { act } from 'react-dom/test-utils'; +import { buildMockTx } from '@src/utils/mocks/tx'; +import { TransactionWitnessRequest } from '@cardano-sdk/web-extension'; + +jest.mock('../proposal-procedures/HardForkInitiationActionContainer', () => { + const original = jest.requireActual('../proposal-procedures/HardForkInitiationActionContainer'); + return { + __esModule: true, + ...original, + HardForkInitiationActionContainer: mockHardForkInitiationActionContainer + }; +}); + +jest.mock('../proposal-procedures/InfoActionContainer', () => { + const original = jest.requireActual('../proposal-procedures/InfoActionContainer'); + return { + __esModule: true, + ...original, + InfoActionContainer: mockInfoActionContainer + }; +}); + +jest.mock('../proposal-procedures/NewConstitutionActionContainer', () => { + const original = jest.requireActual('../proposal-procedures/NewConstitutionActionContainer'); + return { + __esModule: true, + ...original, + NewConstitutionActionContainer: mockNewConstitutionActionContainer + }; +}); + +jest.mock('../proposal-procedures/NoConfidenceActionContainer', () => { + const original = jest.requireActual('../proposal-procedures/NoConfidenceActionContainer'); + return { + __esModule: true, + ...original, + NoConfidenceActionContainer: mockNoConfidenceActionContainer + }; +}); + +jest.mock('../proposal-procedures/ParameterChangeActionContainer', () => { + const original = jest.requireActual('../proposal-procedures/ParameterChangeActionContainer'); + return { + __esModule: true, + ...original, + ParameterChangeActionContainer: mockParameterChangeActionContainer + }; +}); + +jest.mock('../proposal-procedures/TreasuryWithdrawalsActionContainer', () => { + const original = jest.requireActual('../proposal-procedures/TreasuryWithdrawalsActionContainer'); + return { + __esModule: true, + ...original, + TreasuryWithdrawalsActionContainer: mockTreasuryWithdrawalsActionContainer + }; +}); + +jest.mock('../proposal-procedures/UpdateCommitteeActionContainer', () => { + const original = jest.requireActual('../proposal-procedures/UpdateCommitteeActionContainer'); + return { + __esModule: true, + ...original, + UpdateCommitteeActionContainer: mockUpdateCommitteeActionContainer + }; +}); + +const dappInfo = { + name: 'dappName', + logo: 'dappLogo', + url: 'dappUrl' +}; +const tx = buildMockTx(); +const deposit = BigInt('10000'); +const rewardAccount = Wallet.Cardano.RewardAccount('stake_test1uqrw9tjymlm8wrwq7jk68n6v7fs9qz8z0tkdkve26dylmfc2ux2hj'); +const anchor = { + url: 'anchorUrl', + dataHash: Wallet.Crypto.Hash32ByteBase16(Buffer.from('anchorDataHashanchorDataHashanch').toString('hex')) +}; + +const hardForkInitiationAction = { + __typename: Wallet.Cardano.GovernanceActionType.hard_fork_initiation_action +} as Wallet.Cardano.HardForkInitiationAction; +const infoAction = { + __typename: Wallet.Cardano.GovernanceActionType.info_action +} as Wallet.Cardano.InfoAction; +const newConstitution = { + __typename: Wallet.Cardano.GovernanceActionType.new_constitution +} as Wallet.Cardano.NewConstitution; +const noConfidence = { + __typename: Wallet.Cardano.GovernanceActionType.no_confidence +} as Wallet.Cardano.NoConfidence; +const parameterChangeAction = { + __typename: Wallet.Cardano.GovernanceActionType.parameter_change_action +} as Wallet.Cardano.ParameterChangeAction; +const treasuryWithdrawalsAction = { + __typename: Wallet.Cardano.GovernanceActionType.treasury_withdrawals_action +} as Wallet.Cardano.TreasuryWithdrawalsAction; +const updateCommittee = { + __typename: Wallet.Cardano.GovernanceActionType.update_committee +} as Wallet.Cardano.UpdateCommittee; + +const proposalProcedures = [ + { deposit, rewardAccount, anchor, governanceAction: hardForkInitiationAction }, + { deposit, rewardAccount, anchor, governanceAction: infoAction }, + { deposit, rewardAccount, anchor, governanceAction: newConstitution }, + { deposit, rewardAccount, anchor, governanceAction: noConfidence }, + { deposit, rewardAccount, anchor, governanceAction: parameterChangeAction }, + { deposit, rewardAccount, anchor, governanceAction: treasuryWithdrawalsAction }, + { deposit, rewardAccount, anchor, governanceAction: updateCommittee } +]; + +const request = { + transaction: { + toCore: jest.fn().mockReturnValue({ ...tx, body: { ...tx.body, proposalProcedures } }) + } as any +} as TransactionWitnessRequest; + +jest.mock('@providers', () => ({ + ...jest.requireActual('@providers'), + useViewsFlowContext: () => ({ + signTxRequest: { request }, + dappInfo + }) +})); + +describe('Testing ProposalProceduresContainer component', () => { + afterEach(() => { + jest.clearAllMocks(); + cleanup(); + }); + + test('should render proper procedure', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render()); + }); + + expect(queryByTestId('HardForkInitiationActionContainer')).toBeInTheDocument(); + expect(queryByTestId('InfoActionContainer')).toBeInTheDocument(); + expect(queryByTestId('NewConstitutionActionContainer')).toBeInTheDocument(); + expect(queryByTestId('NoConfidenceActionContainer')).toBeInTheDocument(); + expect(queryByTestId('ParameterChangeActionContainer')).toBeInTheDocument(); + expect(queryByTestId('TreasuryWithdrawalsActionContainer')).toBeInTheDocument(); + expect(queryByTestId('UpdateCommitteeActionContainer')).toBeInTheDocument(); + + const expectedProps = { dappInfo, deposit, rewardAccount, anchor }; + + expect(mockHardForkInitiationActionContainer).toHaveBeenLastCalledWith( + { ...expectedProps, governanceAction: hardForkInitiationAction }, + {} + ); + expect(mockInfoActionContainer).toHaveBeenLastCalledWith({ ...expectedProps, governanceAction: infoAction }, {}); + expect(mockNewConstitutionActionContainer).toHaveBeenLastCalledWith( + { ...expectedProps, governanceAction: newConstitution }, + {} + ); + expect(mockNoConfidenceActionContainer).toHaveBeenLastCalledWith( + { ...expectedProps, governanceAction: noConfidence }, + {} + ); + expect(mockParameterChangeActionContainer).toHaveBeenLastCalledWith( + { ...expectedProps, governanceAction: parameterChangeAction }, + {} + ); + expect(mockTreasuryWithdrawalsActionContainer).toHaveBeenLastCalledWith( + { ...expectedProps, governanceAction: treasuryWithdrawalsAction }, + {} + ); + expect(mockUpdateCommitteeActionContainer).toHaveBeenLastCalledWith( + { ...expectedProps, governanceAction: updateCommittee }, + {} + ); + }); +}); 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 new file mode 100644 index 000000000..bfb5f9646 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/VotingProceduresContainer.test.tsx @@ -0,0 +1,338 @@ +/* eslint-disable sonarjs/no-identical-functions */ +/* eslint-disable unicorn/consistent-destructuring */ +/* eslint-disable unicorn/no-null */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable import/imports-first */ +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, waitFor } from '@testing-library/react'; +import { VotingProceduresContainer } from '../VotingProceduresContainer'; +import '@testing-library/jest-dom'; +import { act } from 'react-dom/test-utils'; +import { buildMockTx } from '@src/utils/mocks/tx'; +import { Wallet } from '@lace/cardano'; +import { getWrapper } from '../testing.utils'; +import { getVoterType, getVote, VoterTypeEnum, VotesEnum } from '@src/utils/tx-inspection'; +import { drepIDasBech32FromHash } from '../utils'; +import { TransactionWitnessRequest } from '@cardano-sdk/web-extension'; + +jest.mock('@src/stores', () => ({ + ...jest.requireActual('@src/stores'), + useWalletStore: mockUseWalletStore +})); + +jest.mock('@src/config', () => { + const original = jest.requireActual('@src/config'); + return { + ...original, + config: () => ({ + ...original.config(), + CEXPLORER_BASE_URL: { Preprod: mockPreprodCexplorerBaseUrl }, + CEXPLORER_URL_PATHS: { Tx: mockCexplorerUrlPathsTx } + }) + }; +}); + +jest.mock('@lace/core', () => { + const original = jest.requireActual('@lace/core'); + return { + __esModule: true, + ...original, + VotingProcedures: mockVotingProcedures + }; +}); + +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 { + __esModule: true, + ...original, + useTranslation: mockUseTranslation + }; +}); + +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', + url: 'dappUrl' +}; +const tx = buildMockTx(); +const constitutionalCommitteeKeyHashVoter: Wallet.Cardano.ConstitutionalCommitteeKeyHashVoter = { + __typename: Wallet.Cardano.VoterType.ccHotKeyHash, + credential: { + type: Wallet.Cardano.CredentialType.KeyHash, + hash: Wallet.Crypto.Hash28ByteBase16(Buffer.from('dRepCredentialHashdRepCreden').toString('hex')) + } +}; +const constitutionalCommitteeScriptHashVoter: Wallet.Cardano.ConstitutionalCommitteeScriptHashVoter = { + __typename: Wallet.Cardano.VoterType.ccHotScriptHash, + credential: { + type: Wallet.Cardano.CredentialType.ScriptHash, + hash: Wallet.Crypto.Hash28ByteBase16(Buffer.from('dRepCredentialHashdRepCreden').toString('hex')) + } +}; +const drepKeyHashVoter: Wallet.Cardano.DrepKeyHashVoter = { + __typename: Wallet.Cardano.VoterType.dRepKeyHash, + credential: { + type: Wallet.Cardano.CredentialType.KeyHash, + hash: Wallet.Crypto.Hash28ByteBase16(Buffer.from('dRepCredentialHashdRepCreden').toString('hex')) + } +}; +const drepScriptHashVoter: Wallet.Cardano.DrepScriptHashVoter = { + __typename: Wallet.Cardano.VoterType.dRepScriptHash, + credential: { + type: Wallet.Cardano.CredentialType.ScriptHash, + hash: Wallet.Crypto.Hash28ByteBase16(Buffer.from('dRepCredentialHashdRepCreden').toString('hex')) + } +}; +const stakePoolKeyHashVoter: Wallet.Cardano.StakePoolKeyHashVoter = { + __typename: Wallet.Cardano.VoterType.stakePoolKeyHash, + credential: { + type: Wallet.Cardano.CredentialType.KeyHash, + hash: Wallet.Crypto.Hash28ByteBase16(Buffer.from('dRepCredentialHashdRepCreden').toString('hex')) + } +}; + +const voters = [ + constitutionalCommitteeKeyHashVoter, + constitutionalCommitteeScriptHashVoter, + drepKeyHashVoter, + drepScriptHashVoter, + stakePoolKeyHashVoter +]; + +const votes = [ + Wallet.Cardano.Vote.yes, + Wallet.Cardano.Vote.no, + Wallet.Cardano.Vote.abstain, + Wallet.Cardano.Vote.yes, + Wallet.Cardano.Vote.no +]; + +const votingProcedures = voters.map((voter, index) => ({ + voter, + votes: [ + { + actionId: { + id: Wallet.Cardano.TransactionId(`724a0a88b9470a714fc5bf84daf5851fa259a9b89e1a5453f6f5cd6595ad982${index}`), + actionIndex: 0 + }, + votingProcedure: { + vote: votes[index], + ...(index && { + anchor: { + url: `anchorUrl${index}`, + dataHash: Wallet.Crypto.Hash32ByteBase16( + Buffer.from(`anchorDataHashanchorDataHashanc${index}`).toString('hex') + ) + } + }) + } + } + ] +})); + +const request = { + transaction: { + toCore: jest.fn().mockReturnValue({ ...tx, body: { ...tx.body, votingProcedures } }) + } as any +} as TransactionWitnessRequest; + +jest.mock('@providers', () => ({ + ...jest.requireActual('@providers'), + useViewsFlowContext: () => ({ + signTxRequest: { request }, + dappInfo + }) +})); + +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(() => { + cleanup(); + }); + + test('should render VotingProcedures component with proper props', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render(, { + wrapper: getWrapper() + })); + }); + + expect(queryByTestId('VotingProcedures')).toBeInTheDocument(); + // eslint-disable-next-line unicorn/consistent-function-scoping + const getExpectedDrepId = (type: string) => (hash: Wallet.Crypto.Hash28ByteBase16) => + type === VoterTypeEnum.DREP ? drepIDasBech32FromHash(hash) : hash.toString(); + expect(mockVotingProcedures).toHaveBeenLastCalledWith( + { + dappInfo, + data: voters.map(({ __typename }, index) => ({ + voter: { + type: t(`core.VotingProcedures.voterTypes.${getVoterType(__typename)}`), + dRepId: getExpectedDrepId(getVoterType(__typename))(voters[index].credential.hash) + }, + votes: votingProcedures[index].votes.map((vote) => ({ + actionId: { + index: vote.actionId.actionIndex, + txHash: vote.actionId.id.toString(), + txHashUrl: `${mockPreprodCexplorerBaseUrl}/${mockCexplorerUrlPathsTx}/${vote.actionId.id}` + }, + 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() + } + } + })) + })), + 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') + } + }, + {} + ); + }); + + 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 + ); + expect(getVoterType(constitutionalCommitteeScriptHashVoter.__typename)).toEqual( + VoterTypeEnum.CONSTITUTIONAL_COMMITTEE + ); + expect(getVoterType(drepKeyHashVoter.__typename)).toEqual(VoterTypeEnum.DREP); + expect(getVoterType(drepScriptHashVoter.__typename)).toEqual(VoterTypeEnum.DREP); + expect(getVoterType(stakePoolKeyHashVoter.__typename)).toEqual(VoterTypeEnum.SPO); + }); + + test('testing getVote', () => { + expect(getVote(Wallet.Cardano.Vote.yes)).toEqual(VotesEnum.YES); + expect(getVote(Wallet.Cardano.Vote.no)).toEqual(VotesEnum.NO); + expect(getVote(Wallet.Cardano.Vote.abstain)).toEqual(VotesEnum.ABSTAIN); + }); +}); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/hooks.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/hooks.test.tsx new file mode 100644 index 000000000..56f0beb42 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/hooks.test.tsx @@ -0,0 +1,324 @@ +/* eslint-disable no-magic-numbers */ +/* eslint-disable unicorn/no-null */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable import/imports-first */ +/* eslint-disable sonarjs/no-identical-functions */ + +const mockPubDRepKeyToHash = jest.fn(); +const mockDisallowSignTx = jest.fn(); +const mockAllowSignTx = jest.fn(); +const mockEstablishDeviceConnection = jest.fn(); +const mockGetTransactionAssetsId = jest.fn(); +const mockGetAssetsInformation = jest.fn(); +const mockCalculateAssetBalance = jest.fn(); +const mockLovelacesToAdaString = jest.fn(); +const mockUseWalletStore = jest.fn(); +import { act, cleanup } from '@testing-library/react'; +import { useCreateAssetList, useGetOwnPubDRepKeyHash, useOnBeforeUnload, useSignWithHardwareWallet } from '../hooks'; +import { renderHook } from '@testing-library/react-hooks'; +import { Wallet } from '@lace/cardano'; +import * as hooks from '@hooks'; +import { dAppRoutePaths } from '@routes/wallet-paths'; +import { TokenInfo } from '@src/utils/get-assets-information'; +import * as Core from '@cardano-sdk/core'; +import { TransactionWitnessRequest, WalletType } from '@cardano-sdk/web-extension'; + +jest.mock('@stores', () => ({ + ...jest.requireActual('@stores'), + useWalletStore: mockUseWalletStore +})); + +jest.mock('@cardano-sdk/core', () => ({ + ...jest.requireActual('@cardano-sdk/core'), + createTxInspector: jest.fn() +})); + +jest.mock('@hooks', () => { + const original = jest.requireActual('@hooks'); + return { + __esModule: true, + ...original + }; +}); + +jest.mock('@cardano-sdk/hardware-ledger', () => { + const original = jest.requireActual('@cardano-sdk/hardware-ledger'); + return { + __esModule: true, + ...original, + LedgerKeyAgent: { + ...original.LedgerKeyAgent, + establishDeviceConnection: mockEstablishDeviceConnection + } + }; +}); + +jest.mock('../utils.ts', () => { + const original = jest.requireActual('../utils.ts'); + return { + __esModule: true, + ...original, + pubDRepKeyToHash: mockPubDRepKeyToHash, + disallowSignTx: mockDisallowSignTx, + allowSignTx: mockAllowSignTx + }; +}); + +jest.mock('@src/stores/slices', () => { + const original = jest.requireActual('@src/stores/slices'); + return { + __esModule: true, + ...original, + getTransactionAssetsId: mockGetTransactionAssetsId + }; +}); + +jest.mock('@src/utils/get-assets-information', () => { + const original = jest.requireActual('@src/utils/get-assets-information'); + return { + __esModule: true, + ...original, + getAssetsInformation: mockGetAssetsInformation + }; +}); + +jest.mock('@lace/cardano', () => { + const actual = jest.requireActual('@lace/cardano'); + return { + __esModule: true, + ...actual, + Wallet: { + ...actual.Wallet, + util: { + ...actual.Wallet.util, + calculateAssetBalance: mockCalculateAssetBalance, + lovelacesToAdaString: mockLovelacesToAdaString + } + } + }; +}); + +const _listeners: { type: string; listener: EventListenerOrEventListenerObject }[] = []; + +const addEventListenerOriginal = window.addEventListener; + +const patchAddEventListener = () => { + window.addEventListener = (type: any, listener: any) => { + _listeners.push({ type, listener }); + addEventListenerOriginal.call(window, type, listener); + }; +}; + +const removeEventListeners = () => { + for (const { type, listener } of _listeners) { + window.removeEventListener(type, listener); + } +}; + +describe('Testing hooks', () => { + afterEach(() => { + jest.resetModules(); + jest.resetAllMocks(); + cleanup(); + }); + + test('useCreateAssetList', async () => { + const assetsIds = ['id1', 'id2', 'id3', 'id4', 'id5'] as Wallet.Cardano.AssetId[]; + mockGetTransactionAssetsId.mockReset(); + mockGetTransactionAssetsId.mockReturnValue(assetsIds); + const tokenInfo = new Map([ + [assetsIds[0], null], + [ + assetsIds[1], + { + name: '', + tokenMetadata: { ticker: `${assetsIds[1]}_tokenMetadata_ticker` }, + nftMetadata: { name: `${assetsIds[1]}_nftMetadata_name` } + } + ], + [ + assetsIds[2], + { + name: `${assetsIds[2]}_name`, + tokenMetadata: { ticker: `${assetsIds[2]}_tokenMetadata_ticker` }, + nftMetadata: { name: `${assetsIds[2]}_nftMetadata_name` } + } + ], + [ + assetsIds[3], + { + name: `${assetsIds[3]}_name`, + tokenMetadata: null, + nftMetadata: { name: `${assetsIds[3]}_nftMetadata_name` } + } + ], + [ + assetsIds[4], + { + name: '' + } + ] + ]) as TokenInfo; + mockGetAssetsInformation.mockReset(); + mockGetAssetsInformation.mockImplementation(async () => await tokenInfo); + mockCalculateAssetBalance.mockReset(); + mockCalculateAssetBalance.mockImplementation((value, walletAsset) => `${value}_${walletAsset?.name || 'default'}`); + + const outputs = 'outputs' as unknown as Wallet.Cardano.TxOut[]; + const assetProvider = 'assetProvider' as unknown as Core.AssetProvider; + const assets = new Map([ + [ + assetsIds[0], + { + name: `${assetsIds[0]}_name_from_assets`, + tokenMetadata: { ticker: `${assetsIds[0]}_tokenMetadata_ticker_from_assets` } + } + ], + [ + assetsIds[1], + { + name: `${assetsIds[1]}_name_from_assets`, + tokenMetadata: null, + nftMetadata: { name: `${assetsIds[2]}_nftMetadata_name_from_assets` } + } + ], + [assetsIds[2], null], + [assetsIds[3], null], + [ + assetsIds[4], + { + ...tokenInfo.get(assetsIds[4]) + } + ] + ]) as unknown as TokenInfo; + + const tokenMap = new Map(assetsIds.map((id) => [id, `${id}_balance`])) as unknown as Wallet.Cardano.TokenMap; + tokenMap.set('id5' as Wallet.Cardano.AssetId, 'id5_balance' as unknown as bigint); + + let hook: any; + await act(async () => { + hook = renderHook(() => useCreateAssetList({ outputs, assets, assetProvider })); + }); + expect(hook.result.current(tokenMap)).toEqual([ + // should use assets info + { + name: assets.get(assetsIds[0]).name, + ticker: assets.get(assetsIds[0]).tokenMetadata.ticker, + amount: `${tokenMap.get(assetsIds[0])}_${assets.get(assetsIds[0]).name}` + }, + { + name: assets.get(assetsIds[1]).name, + ticker: assets.get(assetsIds[1]).nftMetadata.name, + amount: `${tokenMap.get(assetsIds[1])}_${assets.get(assetsIds[1]).name}` + }, + // should use assetProvider info + { + name: tokenInfo.get(assetsIds[2]).name, + ticker: tokenInfo.get(assetsIds[2]).tokenMetadata.ticker, + amount: `${tokenMap.get(assetsIds[2])}_${tokenInfo.get(assetsIds[2]).name}` + }, + { + name: tokenInfo.get(assetsIds[3]).name, + ticker: tokenInfo.get(assetsIds[3]).nftMetadata.name, + amount: `${tokenMap.get(assetsIds[3])}_${tokenInfo.get(assetsIds[3]).name}` + }, + { + name: 'id5', + amount: `${tokenMap.get('id5' as Wallet.Cardano.AssetId)}_${'default'}`, + ticker: undefined + } + ]); + }); + + test('useSignWithHardwareWallet', async () => { + const redirectToSignFailure = jest.fn(); + const useRedirectionSpy = jest.spyOn(hooks, 'useRedirection').mockImplementation(() => redirectToSignFailure); + mockEstablishDeviceConnection.mockReset(); + mockEstablishDeviceConnection.mockImplementation(async () => await true); + const mockSign = jest.fn().mockImplementation(async () => mockEstablishDeviceConnection()); + const hook = renderHook(() => + useSignWithHardwareWallet({ + sign: mockSign as any, + requestContext: {} as any, + reject: jest.fn(), + signContext: {} as any, + transaction: {} as any, + walletType: WalletType.Ledger + } as TransactionWitnessRequest) + ); + await hook.waitFor(() => { + expect(hook.result.current.isConfirmingTx).toBeFalsy; + expect(useRedirectionSpy).toHaveBeenLastCalledWith(dAppRoutePaths.dappTxSignSuccess); + }); + + await act(async () => { + await hook.result.current.signWithHardwareWallet(); + }); + + await hook.waitFor(() => { + expect(hook.result.current.isConfirmingTx).toBe(true); + expect(mockSign).toHaveBeenCalledTimes(1); + expect(mockEstablishDeviceConnection).toHaveBeenCalledTimes(1); + expect(mockDisallowSignTx).not.toHaveBeenCalled(); + }); + + mockEstablishDeviceConnection.mockReset(); + mockEstablishDeviceConnection.mockImplementation(async () => { + throw new Error('error'); + }); + + await hook.rerender(); + + await act(async () => { + try { + await hook.result.current.signWithHardwareWallet(); + } catch { + expect(hook.result.current.isConfirmingTx).toBe(true); + expect(mockSign).toHaveBeenCalledTimes(1); + expect(mockDisallowSignTx).toHaveBeenCalledTimes(1); + expect(mockDisallowSignTx).toHaveBeenLastCalledWith(false); + expect(redirectToSignFailure).toHaveBeenCalledTimes(1); + } + }); + }); + + test('useOnBeforeUnload', async () => { + patchAddEventListener(); + const cb = jest.fn(); + const hook = renderHook(() => useOnBeforeUnload(cb)); + + await hook.waitFor(() => { + window.dispatchEvent(new Event('beforeunload')); + expect(cb).toHaveBeenCalledTimes(1); + }); + + hook.unmount(); + + window.dispatchEvent(new Event('beforeunload')); + expect(cb).toHaveBeenCalledTimes(1); + + removeEventListeners(); + }); + + test('useGetOwnPubDRepKeyHash', async () => { + const ed25519PublicKeyHexMock = 'ed25519PublicKeyHexMock'; + mockPubDRepKeyToHash.mockReset(); + mockPubDRepKeyToHash.mockImplementation(async (val: Wallet.Crypto.Ed25519PublicKeyHex) => await val); + mockUseWalletStore.mockReset(); + mockUseWalletStore.mockReturnValue({ + inMemoryWallet: { + getPubDRepKey: jest.fn(async () => await ed25519PublicKeyHexMock) + } + }); + + let hook: any; + await act(async () => { + hook = renderHook(() => useGetOwnPubDRepKeyHash()); + expect(hook.result.current.loading).toEqual(true); + }); + + await hook.waitFor(() => { + expect(hook.result.current.ownPubDRepKeyHash).toEqual(ed25519PublicKeyHexMock); + }); + }); +}); 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 new file mode 100644 index 000000000..ca920d6f4 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/__tests__/utils.test.tsx @@ -0,0 +1,229 @@ +/* eslint-disable unicorn/consistent-function-scoping */ +/* eslint-disable import/imports-first */ +const mockDRepID = jest.fn(); +const mockHexBlob = (val: string) => val; +mockHexBlob.toTypedBech32 = (prefix: string, value: string) => `${prefix}${value}`; +/* eslint-disable unicorn/no-useless-undefined */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-magic-numbers */ +/* eslint-disable max-statements */ +import { cleanup } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { Wallet } from '@lace/cardano'; +import * as Core from '@cardano-sdk/core'; +import { + certificateInspectorFactory, + votingProceduresInspector, + getTxType, + drepIDasBech32FromHash, + pubDRepKeyToHash, + depositPaidWithSymbol, + hasValidDrepRegistration +} from '../utils'; + +jest.mock('@cardano-sdk/core', () => ({ + ...jest.requireActual('@cardano-sdk/core'), + createTxInspector: jest.fn() +})); + +jest.mock('@lace/cardano', () => { + const actual = jest.requireActual('@lace/cardano'); + return { + __esModule: true, + ...actual, + Wallet: { + ...actual.Wallet, + Cardano: { + ...actual.Wallet.Cardano, + DRepID: mockDRepID + }, + HexBlob: mockHexBlob, + Crypto: { + ...actual.Wallet.Crypto, + Hash28ByteBase16: { + ...actual.Wallet.Crypto.Hash28ByteBase16, + fromEd25519KeyHashHex: (drepKeyHex: string) => drepKeyHex + }, + Ed25519PublicKey: { + ...actual.Wallet.Crypto.Ed25519PublicKey, + fromHex: async (val: string) => await { hash: async () => await { hex: () => val } } + } + } + } + }; +}); + +describe('Testing utils', () => { + afterEach(() => { + jest.resetModules(); + jest.resetAllMocks(); + cleanup(); + }); + + test('testing certificateInspectorFactory', async () => { + const VoteDelegationCertificate = { __typename: Wallet.Cardano.CertificateType.VoteDelegation }; + expect( + await certificateInspectorFactory(Wallet.Cardano.CertificateType.VoteDelegation)({ + body: { certificates: [VoteDelegationCertificate] } + } as Wallet.Cardano.Tx) + ).toEqual(VoteDelegationCertificate); + expect( + await certificateInspectorFactory(Wallet.Cardano.CertificateType.VoteRegistrationDelegation)({ + body: { certificates: [VoteDelegationCertificate] } + } as Wallet.Cardano.Tx) + ).toEqual(undefined); + }); + + test('testing votingProceduresInspector', async () => { + const votingProcedures = 'votingProcedures'; + expect(await votingProceduresInspector({ body: { votingProcedures } } as unknown as Wallet.Cardano.Tx)).toEqual( + votingProcedures + ); + expect(await votingProceduresInspector({ body: {} } as Wallet.Cardano.Tx)).toEqual(undefined); + }); + + test('testing getTxType', async () => { + const tx = { body: {} } as Wallet.Cardano.Tx; + const txInspectorCurriedFnPayload = { minted: [], burned: [] } as unknown as any; + const createTxInspectorSpy = jest + .spyOn(Core, 'createTxInspector') + .mockReturnValueOnce(() => ({ ...txInspectorCurriedFnPayload, proposalProcedures: true })); + expect(await getTxType(tx)).toEqual(Wallet.Cip30TxType.ProposalProcedures); + expect(createTxInspectorSpy).toHaveBeenCalledTimes(1); + + createTxInspectorSpy.mockReturnValueOnce(() => ({ ...txInspectorCurriedFnPayload, votingProcedures: true })); + expect(await getTxType(tx)).toEqual(Wallet.Cip30TxType.VotingProcedures); + expect(createTxInspectorSpy).toHaveBeenCalledTimes(2); + + createTxInspectorSpy.mockReturnValueOnce(() => ({ ...txInspectorCurriedFnPayload, minted: { length: 1 } })); + expect(await getTxType(tx)).toEqual(Wallet.Cip30TxType.Mint); + expect(createTxInspectorSpy).toHaveBeenCalledTimes(3); + + createTxInspectorSpy.mockReturnValueOnce(() => ({ ...txInspectorCurriedFnPayload, burned: { length: 1 } })); + expect(await getTxType(tx)).toEqual(Wallet.Cip30TxType.Burn); + expect(createTxInspectorSpy).toHaveBeenCalledTimes(4); + + createTxInspectorSpy.mockReturnValueOnce(() => ({ ...txInspectorCurriedFnPayload, dRepRegistration: true })); + expect(await getTxType(tx)).toEqual(Wallet.Cip30TxType.DRepRegistration); + expect(createTxInspectorSpy).toHaveBeenCalledTimes(5); + + createTxInspectorSpy.mockReturnValueOnce(() => ({ ...txInspectorCurriedFnPayload, dRepRetirement: true })); + expect(await getTxType(tx)).toEqual(Wallet.Cip30TxType.DRepRetirement); + expect(createTxInspectorSpy).toHaveBeenCalledTimes(6); + + createTxInspectorSpy.mockReturnValueOnce(() => ({ ...txInspectorCurriedFnPayload, voteDelegation: true })); + expect(await getTxType(tx)).toEqual(Wallet.Cip30TxType.VoteDelegation); + expect(createTxInspectorSpy).toHaveBeenCalledTimes(7); + + createTxInspectorSpy.mockReturnValueOnce(() => ({ ...txInspectorCurriedFnPayload, dRepUpdate: true })); + expect(await getTxType(tx)).toEqual(Wallet.Cip30TxType.DRepUpdate); + expect(createTxInspectorSpy).toHaveBeenCalledTimes(8); + + createTxInspectorSpy.mockReturnValueOnce(() => ({ ...txInspectorCurriedFnPayload, stakeVoteDelegation: true })); + expect(await getTxType(tx)).toEqual(Wallet.Cip30TxType.StakeVoteDelegation); + expect(createTxInspectorSpy).toHaveBeenCalledTimes(9); + + createTxInspectorSpy.mockReturnValueOnce(() => ({ + ...txInspectorCurriedFnPayload, + voteRegistrationDelegation: true + })); + expect(await getTxType(tx)).toEqual(Wallet.Cip30TxType.VoteRegistrationDelegation); + expect(createTxInspectorSpy).toHaveBeenCalledTimes(10); + + createTxInspectorSpy.mockReturnValueOnce(() => ({ + ...txInspectorCurriedFnPayload, + stakeRegistrationDelegation: true + })); + expect(await getTxType(tx)).toEqual(Wallet.Cip30TxType.StakeRegistrationDelegation); + expect(createTxInspectorSpy).toHaveBeenCalledTimes(11); + + createTxInspectorSpy.mockReturnValueOnce(() => ({ + ...txInspectorCurriedFnPayload, + stakeVoteDelegationRegistration: true + })); + expect(await getTxType(tx)).toEqual(Wallet.Cip30TxType.StakeVoteDelegationRegistration); + expect(createTxInspectorSpy).toHaveBeenCalledTimes(12); + + createTxInspectorSpy.mockReturnValueOnce(() => ({ ...txInspectorCurriedFnPayload })); + expect(await getTxType(tx)).toEqual(Wallet.Cip30TxType.Send); + expect(createTxInspectorSpy).toHaveBeenCalledTimes(13); + }); + + test('testing drepIDasBech32FromHash', () => { + mockDRepID.mockReset(); + mockDRepID.mockImplementation((val) => val); + + const drepID = '_drepID'; + expect(drepIDasBech32FromHash(drepID as Wallet.Crypto.Hash28ByteBase16)).toEqual(`drep${drepID}`); + }); + + test('testing pubDRepKeyToHash', async () => { + const pubDRepKeyHex = '_pubDRepKeyHex'; + expect(await pubDRepKeyToHash(pubDRepKeyHex as Wallet.Crypto.Ed25519PublicKeyHex)).toEqual(pubDRepKeyHex); + }); + + test('depositPaidWithSymbol', () => { + expect(depositPaidWithSymbol(BigInt(20_000), { name: 'Cardano', symbol: 'ada' } as Wallet.CoinId)).toEqual( + '0.02 ada' + ); + + expect(() => depositPaidWithSymbol(BigInt(20_000), { name: 'Unknown', symbol: 'UNK' } as Wallet.CoinId)).toThrow( + '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 new file mode 100644 index 000000000..99785804c --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/hooks.ts @@ -0,0 +1,234 @@ +/* eslint-disable no-console */ +import isPlainObject from 'lodash/isPlainObject'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { AssetProvider, AssetsMintedInspection, MintedAsset } from '@cardano-sdk/core'; +import { dAppRoutePaths } from '@routes'; +import { Wallet } from '@lace/cardano'; +import { useRedirection } from '@hooks'; +import { CardanoTxOut } from '@src/types'; +import { config } from '@src/config'; +import { TokenInfo, getAssetsInformation } from '@src/utils/get-assets-information'; +import { getTransactionAssetsId } from '@src/stores/slices'; +import { allowSignTx, pubDRepKeyToHash, disallowSignTx } from './utils'; +import { useWalletStore } from '@stores'; +import { TransactionWitnessRequest, WalletType } from '@cardano-sdk/web-extension'; + +export const useCreateAssetList = ({ + assets, + outputs, + assetProvider +}: { + assets?: TokenInfo; + outputs?: CardanoTxOut[]; + assetProvider: AssetProvider; +}): ((txAssets: Wallet.Cardano.TokenMap) => Wallet.Cip30SignTxAssetItem[]) => { + const [assetsInfo, setAssetsInfo] = useState(); + const assetIds = useMemo(() => outputs && getTransactionAssetsId(outputs), [outputs]); + + useEffect(() => { + if (assetIds?.length > 0) { + getAssetsInformation(assetIds, assets, { + assetProvider, + extraData: { nftMetadata: true, tokenMetadata: true } + }) + .then((result) => setAssetsInfo(result)) + .catch((error) => { + console.error(error); + }); + } + }, [assetIds, assetProvider, assets]); + + return useCallback( + (txAssets: Wallet.Cardano.TokenMap) => { + if (!assetsInfo) return []; + const assetList: Wallet.Cip30SignTxAssetItem[] = []; + // eslint-disable-next-line unicorn/no-array-for-each + txAssets.forEach(async (value, key) => { + const walletAsset = assets.get(key) || assetsInfo?.get(key); + assetList.push({ + name: walletAsset?.name.toString() || key.toString(), + ticker: walletAsset?.tokenMetadata?.ticker || walletAsset.nftMetadata?.name, + amount: Wallet.util.calculateAssetBalance(value, walletAsset) + }); + }); + return assetList; + }, + [assets, assetsInfo] + ); +}; +const convertMetadataArrayToObj = (arr: unknown[]): Record => { + const result: Record = {}; + for (const item of arr) { + if (isPlainObject(item)) { + Object.assign(result, item); + } + } + return result; +}; + +// eslint-disable-next-line complexity, sonarjs/cognitive-complexity +const getAssetNameFromMintMetadata = (asset: MintedAsset, metadata: Wallet.Cardano.TxMetadata): string | undefined => { + if (!asset || !metadata) return; + const decodedAssetName = Buffer.from(asset.assetName, 'hex').toString(); + + // Tries to find the asset name in the tx metadata under label 721 or 20 + for (const [key, value] of metadata.entries()) { + // eslint-disable-next-line no-magic-numbers + if (key !== BigInt(721) && key !== BigInt(20)) return; + const cip25Metadata = Wallet.cardanoMetadatumToObj(value); + if (!Array.isArray(cip25Metadata)) return; + + // cip25Metadata should be an array containing all policies for the minted assets in the tx + const policyLevelMetadata = convertMetadataArrayToObj(cip25Metadata)[asset.policyId]; + if (!Array.isArray(policyLevelMetadata)) return; + + // policyLevelMetadata should be an array of objects with the minted assets names as key + // e.g. "policyId" = [{ "AssetName1": { ...metadataAsset1 } }, { "AssetName2": { ...metadataAsset2 } }]; + const assetProperties = convertMetadataArrayToObj(policyLevelMetadata)?.[decodedAssetName]; + if (!Array.isArray(assetProperties)) return; + + // assetProperties[decodedAssetName] should be an array of objects with the properties as keys + // e.g. [{ "name": "Asset Name" }, { "description": "An asset" }, ...] + const assetMetadataName = convertMetadataArrayToObj(assetProperties)?.name; + // eslint-disable-next-line consistent-return + return typeof assetMetadataName === 'string' ? assetMetadataName : undefined; + } +}; + +export const useCreateMintedAssetList = ({ + assets, + outputs, + assetProvider, + metadata, + mint +}: { + assets?: TokenInfo; + outputs?: CardanoTxOut[]; + assetProvider: AssetProvider; + mint?: Wallet.Cardano.TokenMap; + metadata?: Wallet.Cardano.TxMetadata; +}): ((txAssets: AssetsMintedInspection) => Wallet.Cip30SignTxAssetItem[]) => { + const [assetsInfo, setAssetsInfo] = useState(); + const assetIds = useMemo(() => outputs && getTransactionAssetsId(outputs, mint), [outputs, mint]); + + // eslint-disable-next-line sonarjs/no-identical-functions + useEffect(() => { + if (assetIds?.length > 0) { + getAssetsInformation(assetIds, assets, { + assetProvider, + extraData: { nftMetadata: true, tokenMetadata: true } + }) + .then((result) => setAssetsInfo(result)) + .catch((error) => { + console.error(error); + }); + } + }, [assetIds, assetProvider, assets]); + + return useCallback( + (mintedAssets: AssetsMintedInspection) => { + if (!assetsInfo) return []; + return mintedAssets.map((asset) => { + const assetId = Wallet.Cardano.AssetId.fromParts(asset.policyId, asset.assetName); + const assetInfo = assets.get(assetId) || assetsInfo?.get(assetId); + // If it's a new asset or the name is being updated we should be getting it from the tx metadata + const metadataName = getAssetNameFromMintMetadata(asset, metadata); + return { + name: assetInfo?.name.toString() || asset.fingerprint || assetId, + ticker: + metadataName ?? + assetInfo?.nftMetadata?.name ?? + assetInfo?.tokenMetadata?.ticker ?? + assetInfo?.tokenMetadata?.name ?? + asset.fingerprint.toString(), + amount: Wallet.util.calculateAssetBalance(asset.quantity, assetInfo) + }; + }); + }, + [assets, assetsInfo, metadata] + ); +}; + +export const useDisallowSignTx = ( + req: TransactionWitnessRequest +): ((close?: boolean) => void) => useCallback((close) => disallowSignTx(req, close), [req]); + +export const useAllowSignTx = ( + req: TransactionWitnessRequest +): (() => void) => useCallback(() => allowSignTx(req), [req]); + +export const useSignWithHardwareWallet = ( + req: TransactionWitnessRequest +): { + signWithHardwareWallet: () => Promise; + isConfirmingTx: boolean; +} => { + const disallow = useDisallowSignTx(req); + const redirectToSignFailure = useRedirection(dAppRoutePaths.dappTxSignFailure); + const redirectToSignSuccess = useRedirection(dAppRoutePaths.dappTxSignSuccess); + const [isConfirmingTx, setIsConfirmingTx] = useState(false); + const signWithHardwareWallet = useCallback(async () => { + setIsConfirmingTx(true); + try { + if (req.walletType !== WalletType.Ledger && req.walletType !== WalletType.Trezor) { + throw new Error('Invalid state: expected hw wallet'); + } + await req.sign(); + redirectToSignSuccess(); + } catch (error) { + console.error('signWithHardwareWallet error', error); + disallow(false); + redirectToSignFailure(); + } + }, [disallow, redirectToSignFailure, redirectToSignSuccess, req]); + return { isConfirmingTx, signWithHardwareWallet }; +}; + +export const useOnBeforeUnload = (callBack: () => void): void => { + useEffect(() => { + window.addEventListener('beforeunload', callBack); + return () => { + window.removeEventListener('beforeunload', callBack); + }; + }, [callBack]); +}; + +type UseGetOwnPubDRepKeyHash = { + loading: boolean; + ownPubDRepKeyHash: Wallet.Crypto.Hash28ByteBase16; +}; + +export const useGetOwnPubDRepKeyHash = (): UseGetOwnPubDRepKeyHash => { + const [ownPubDRepKeyHash, setOwnPubDRepKeyHash] = useState(); + const { inMemoryWallet } = useWalletStore(); + + useEffect(() => { + if (!inMemoryWallet) return; + const get = async () => { + const ownPubDRepKey = await inMemoryWallet.getPubDRepKey(); + const ownDRepKeyHash = await pubDRepKeyToHash(ownPubDRepKey); + + setOwnPubDRepKeyHash(ownDRepKeyHash); + }; + + get(); + }, [inMemoryWallet]); + + return { loading: ownPubDRepKeyHash === undefined, ownPubDRepKeyHash }; +}; + +export const useCexplorerBaseUrl = (): string => { + const [explorerBaseUrl, setExplorerBaseUrl] = useState(''); + const { environmentName } = useWalletStore(); + + const { CEXPLORER_BASE_URL, CEXPLORER_URL_PATHS } = config(); + + useEffect(() => { + const explorerUrl = `${CEXPLORER_BASE_URL[environmentName]}/${CEXPLORER_URL_PATHS.Tx}`; + if (explorerUrl !== explorerBaseUrl) { + setExplorerBaseUrl(explorerUrl); + } + }, [CEXPLORER_BASE_URL, CEXPLORER_URL_PATHS.Tx, environmentName, explorerBaseUrl]); + + return explorerBaseUrl; +}; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/HardForkInitiationActionContainer.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/HardForkInitiationActionContainer.tsx new file mode 100644 index 000000000..c040b672b --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/HardForkInitiationActionContainer.tsx @@ -0,0 +1,96 @@ +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Wallet } from '@lace/cardano'; +import { HardForkInitiationAction } from '@lace/core'; +import { useWalletStore } from '@src/stores'; +import { SignTxData } from '../types'; +import { useCexplorerBaseUrl } from '../hooks'; + +interface Props { + dappInfo: SignTxData['dappInfo']; + governanceAction: Wallet.Cardano.HardForkInitiationAction; + deposit: Wallet.Cardano.ProposalProcedure['deposit']; + rewardAccount: Wallet.Cardano.ProposalProcedure['rewardAccount']; + anchor: Wallet.Cardano.ProposalProcedure['anchor']; + errorMessage?: string; +} + +export const HardForkInitiationActionContainer = ({ + dappInfo, + governanceAction, + deposit, + rewardAccount, + anchor, + errorMessage +}: Props): React.ReactElement => { + const { t } = useTranslation(); + const { + walletUI: { cardanoCoin } + } = useWalletStore(); + + const explorerBaseUrl = useCexplorerBaseUrl(); + + const translations = useMemo[0]['translations']>( + () => ({ + txDetails: { + title: t('core.ProposalProcedure.txDetails.title'), + txType: t('core.ProposalProcedure.txDetails.txType'), + deposit: t('core.ProposalProcedure.txDetails.deposit'), + rewardAccount: t('core.ProposalProcedure.txDetails.rewardAccount') + }, + procedure: { + title: t('core.ProposalProcedure.procedure.title'), + anchor: { + url: t('core.ProposalProcedure.procedure.anchor.url'), + hash: t('core.ProposalProcedure.procedure.anchor.hash') + } + }, + protocolVersion: { + major: t('core.ProposalProcedure.governanceAction.hardForkInitiation.protocolVersion.major'), + minor: t('core.ProposalProcedure.governanceAction.hardForkInitiation.protocolVersion.minor'), + patch: t('core.ProposalProcedure.governanceAction.hardForkInitiation.protocolVersion.patch') + }, + actionId: { + title: t('core.ProposalProcedure.governanceAction.actionId.title'), + index: t('core.ProposalProcedure.governanceAction.actionId.index'), + txId: t('core.ProposalProcedure.governanceAction.actionId.txId') + } + }), + [t] + ); + + const { governanceActionId, protocolVersion } = governanceAction; + + const data: Parameters[0]['data'] = { + txDetails: { + txType: t('core.ProposalProcedure.governanceAction.hardForkInitiation.title'), + deposit: Wallet.util.getFormattedAmount({ + amount: deposit.toString(), + cardanoCoin + }), + rewardAccount + }, + procedure: { + anchor: { + url: anchor.url, + hash: anchor.dataHash, + txHashUrl: `${explorerBaseUrl}/${anchor.dataHash}` + } + }, + protocolVersion: { + major: protocolVersion.major.toString(), + minor: protocolVersion.minor.toString(), + patch: protocolVersion.patch?.toString() + }, + ...(governanceActionId && { + actionId: { + index: governanceActionId.actionIndex.toString(), + id: governanceActionId.id || '' + } + }) + }; + + return ( + + ); +}; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/InfoActionContainer.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/InfoActionContainer.tsx new file mode 100644 index 000000000..f880b2b86 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/InfoActionContainer.tsx @@ -0,0 +1,50 @@ +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Wallet } from '@lace/cardano'; +import { InfoAction } from '@lace/core'; +import { SignTxData } from '../types'; +import { useCexplorerBaseUrl } from '../hooks'; + +interface Props { + dappInfo: SignTxData['dappInfo']; + anchor: Wallet.Cardano.ProposalProcedure['anchor']; + errorMessage?: string; +} + +export const InfoActionContainer = ({ dappInfo, anchor, errorMessage }: Props): React.ReactElement => { + const { t } = useTranslation(); + + const explorerBaseUrl = useCexplorerBaseUrl(); + + const translations = useMemo[0]['translations']>( + () => ({ + txDetails: { + title: t('core.ProposalProcedure.txDetails.title'), + txType: t('core.ProposalProcedure.txDetails.txType') + }, + procedure: { + title: t('core.ProposalProcedure.procedure.title'), + anchor: { + url: t('core.ProposalProcedure.procedure.anchor.url'), + hash: t('core.ProposalProcedure.procedure.anchor.hash') + } + } + }), + [t] + ); + + const data: Parameters[0]['data'] = { + txDetails: { + txType: t('core.ProposalProcedure.governanceAction.infoAction.title') + }, + procedure: { + anchor: { + url: anchor.url, + hash: anchor.dataHash, + txHashUrl: `${explorerBaseUrl}/${anchor.dataHash}` + } + } + }; + + return ; +}; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/NewConstitutionActionContainer.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/NewConstitutionActionContainer.tsx new file mode 100644 index 000000000..aef32fb04 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/NewConstitutionActionContainer.tsx @@ -0,0 +1,101 @@ +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Wallet } from '@lace/cardano'; +import { NewConstitutionAction } from '@lace/core'; +import { useWalletStore } from '@src/stores'; +import { SignTxData } from '../types'; +import { useCexplorerBaseUrl } from '../hooks'; + +interface Props { + dappInfo: SignTxData['dappInfo']; + governanceAction: Wallet.Cardano.NewConstitution; + deposit: Wallet.Cardano.ProposalProcedure['deposit']; + rewardAccount: Wallet.Cardano.ProposalProcedure['rewardAccount']; + anchor: Wallet.Cardano.ProposalProcedure['anchor']; + errorMessage?: string; +} + +export const NewConstitutionActionContainer = ({ + dappInfo, + governanceAction, + deposit, + rewardAccount, + anchor, + errorMessage +}: Props): React.ReactElement => { + const { t } = useTranslation(); + const { + walletUI: { cardanoCoin } + } = useWalletStore(); + + const explorerBaseUrl = useCexplorerBaseUrl(); + + const translations = useMemo[0]['translations']>( + () => ({ + txDetails: { + title: t('core.ProposalProcedure.txDetails.title'), + txType: t('core.ProposalProcedure.txDetails.txType'), + deposit: t('core.ProposalProcedure.txDetails.deposit'), + rewardAccount: t('core.ProposalProcedure.txDetails.rewardAccount') + }, + procedure: { + title: t('core.ProposalProcedure.procedure.title'), + anchor: { + url: t('core.ProposalProcedure.procedure.anchor.url'), + hash: t('core.ProposalProcedure.procedure.anchor.hash') + } + }, + constitution: { + title: t('core.ProposalProcedure.governanceAction.newConstitutionAction.constitution.title'), + anchor: { + dataHash: t('core.ProposalProcedure.governanceAction.newConstitutionAction.constitution.anchor.dataHash'), + url: t('core.ProposalProcedure.governanceAction.newConstitutionAction.constitution.anchor.url') + }, + scriptHash: t('core.ProposalProcedure.governanceAction.newConstitutionAction.constitution.scriptHash') + }, + actionId: { + title: t('core.ProposalProcedure.governanceAction.actionId.title'), + index: t('core.ProposalProcedure.governanceAction.actionId.index'), + txId: t('core.ProposalProcedure.governanceAction.actionId.txId') + } + }), + [t] + ); + + const { governanceActionId, constitution } = governanceAction; + + const data: Parameters[0]['data'] = { + txDetails: { + txType: t('core.ProposalProcedure.governanceAction.newConstitutionAction.title'), + deposit: Wallet.util.getFormattedAmount({ + amount: deposit.toString(), + cardanoCoin + }), + rewardAccount + }, + procedure: { + anchor: { + url: anchor.url, + hash: anchor.dataHash, + txHashUrl: `${explorerBaseUrl}/${anchor.dataHash}` + } + }, + ...(governanceActionId && { + actionId: { + index: governanceActionId.actionIndex.toString(), + id: governanceActionId.id || '' + } + }), + constitution: { + anchor: { + dataHash: constitution.anchor.dataHash.toString(), + url: constitution.anchor.url.toString() + }, + ...(constitution.scriptHash && { scriptHash: constitution.scriptHash.toString() }) + } + }; + + return ( + + ); +}; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/NoConfidenceActionContainer.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/NoConfidenceActionContainer.tsx new file mode 100644 index 000000000..b7abdb9ce --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/NoConfidenceActionContainer.tsx @@ -0,0 +1,84 @@ +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Wallet } from '@lace/cardano'; +import { NoConfidenceAction } from '@lace/core'; +import { useWalletStore } from '@src/stores'; +import { SignTxData } from '../types'; +import { useCexplorerBaseUrl } from '../hooks'; + +interface Props { + dappInfo: SignTxData['dappInfo']; + governanceAction: Wallet.Cardano.NoConfidence; + deposit: Wallet.Cardano.ProposalProcedure['deposit']; + rewardAccount: Wallet.Cardano.ProposalProcedure['rewardAccount']; + anchor: Wallet.Cardano.ProposalProcedure['anchor']; + errorMessage?: string; +} + +export const NoConfidenceActionContainer = ({ + dappInfo, + governanceAction, + deposit, + rewardAccount, + anchor, + errorMessage +}: Props): React.ReactElement => { + const { t } = useTranslation(); + const { + walletUI: { cardanoCoin } + } = useWalletStore(); + + const explorerBaseUrl = useCexplorerBaseUrl(); + + const translations = useMemo[0]['translations']>( + () => ({ + txDetails: { + title: t('core.ProposalProcedure.txDetails.title'), + txType: t('core.ProposalProcedure.txDetails.txType'), + deposit: t('core.ProposalProcedure.txDetails.deposit'), + rewardAccount: t('core.ProposalProcedure.txDetails.rewardAccount') + }, + procedure: { + title: t('core.ProposalProcedure.procedure.title'), + anchor: { + url: t('core.ProposalProcedure.procedure.anchor.url'), + hash: t('core.ProposalProcedure.procedure.anchor.hash') + } + }, + actionId: { + title: t('core.ProposalProcedure.governanceAction.actionId.title'), + index: t('core.ProposalProcedure.governanceAction.actionId.index'), + txId: t('core.ProposalProcedure.governanceAction.actionId.txId') + } + }), + [t] + ); + + const { governanceActionId } = governanceAction; + + const data: Parameters[0]['data'] = { + txDetails: { + txType: t('core.ProposalProcedure.governanceAction.noConfidenceAction.title'), + deposit: Wallet.util.getFormattedAmount({ + amount: deposit.toString(), + cardanoCoin + }), + rewardAccount + }, + procedure: { + anchor: { + url: anchor.url, + hash: anchor.dataHash, + txHashUrl: `${explorerBaseUrl}/${anchor.dataHash}` + } + }, + ...(governanceActionId && { + actionId: { + index: governanceActionId.actionIndex.toString(), + id: governanceActionId.id || '' + } + }) + }; + + return ; +}; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/ParameterChangeActionContainer.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/ParameterChangeActionContainer.tsx new file mode 100644 index 000000000..5497b8d4e --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/ParameterChangeActionContainer.tsx @@ -0,0 +1,344 @@ +/* eslint-disable unicorn/no-array-reduce */ +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { formatPercentages } from '@lace/common'; +import { Wallet } from '@lace/cardano'; +import { ParameterChangeAction } from '@lace/core'; +import { useWalletStore } from '@src/stores'; +import { SignTxData } from '../types'; +import { useCexplorerBaseUrl } from '../hooks'; + +interface Props { + dappInfo: SignTxData['dappInfo']; + governanceAction: Wallet.Cardano.ParameterChangeAction; + deposit: Wallet.Cardano.ProposalProcedure['deposit']; + rewardAccount: Wallet.Cardano.ProposalProcedure['rewardAccount']; + anchor: Wallet.Cardano.ProposalProcedure['anchor']; + errorMessage?: string; +} + +export const ParameterChangeActionContainer = ({ + dappInfo, + governanceAction, + deposit, + rewardAccount, + anchor, + errorMessage +}: Props): React.ReactElement => { + const { t } = useTranslation(); + const { + walletUI: { cardanoCoin } + } = useWalletStore(); + + const explorerBaseUrl = useCexplorerBaseUrl(); + + // TODO: consider encapsulating it inside the component itself, check if all the translations have the fallback to the parent int provider (LW-9920) + const translations = useMemo[0]['translations']>( + () => ({ + txDetails: { + title: t('core.ProposalProcedure.txDetails.title'), + txType: t('core.ProposalProcedure.txDetails.txType'), + deposit: t('core.ProposalProcedure.txDetails.deposit'), + rewardAccount: t('core.ProposalProcedure.txDetails.rewardAccount') + }, + anchor: { + url: t('core.ProposalProcedure.procedure.anchor.url'), + hash: t('core.ProposalProcedure.procedure.anchor.hash') + }, + memory: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.memory'), + step: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.step'), + networkGroup: { + title: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.title'), + maxBBSize: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.maxBBSize'), + maxTxSize: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.maxTxSize'), + maxBHSize: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.maxBHSize'), + maxValSize: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.maxValSize'), + maxTxExUnits: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.maxTxExUnits'), + maxBlockExUnits: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.maxBlockExUnits'), + maxCollateralInputs: t( + 'core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.maxCollateralInputs' + ), + coinsByUTXOByte: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.coinsByUTXOByte'), + tooltip: { + maxBBSize: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.tooltip.maxBBSize'), + maxTxSize: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.tooltip.maxTxSize'), + maxBHSize: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.tooltip.maxBHSize'), + maxValSize: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.tooltip.maxValSize'), + maxTxExUnits: t( + 'core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.tooltip.maxTxExUnits' + ), + maxBlockExUnits: t( + 'core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.tooltip.maxBlockExUnits' + ), + maxCollateralInputs: t( + 'core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.tooltip.maxCollateralInputs' + ), + coinsByUTXOByte: t( + 'core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.tooltip.coinsByUTXOByte' + ) + } + }, + economicGroup: { + title: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.title'), + minFeeA: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.minFeeA'), + minFeeB: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.minFeeB'), + keyDeposit: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.keyDeposit'), + poolDeposit: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.poolDeposit'), + rho: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.rho'), + tau: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.tau'), + minPoolCost: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.minPoolCost'), + coinsPerUTxOByte: t( + 'core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.coinsPerUTxOByte' + ), + prices: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.prices'), + tooltip: { + minFeeA: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.tooltip.minFeeA'), + minFeeB: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.tooltip.minFeeB'), + keyDeposit: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.tooltip.keyDeposit'), + poolDeposit: t( + 'core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.tooltip.poolDeposit' + ), + rho: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.tooltip.rho'), + tau: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.tooltip.tau'), + minPoolCost: t( + 'core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.tooltip.minPoolCost' + ), + coinsPerUTxOByte: t( + 'core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.tooltip.coinsPerUTxOByte' + ), + prices: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.tooltip.prices') + } + }, + technicalGroup: { + title: t('core.ProposalProcedure.governanceAction.technicalGroup.title'), + a0: t('core.ProposalProcedure.governanceAction.technicalGroup.a0'), + eMax: t('core.ProposalProcedure.governanceAction.technicalGroup.eMax'), + nOpt: t('core.ProposalProcedure.governanceAction.technicalGroup.nOpt'), + costModels: t('core.ProposalProcedure.governanceAction.technicalGroup.costModels'), + collateralPercentage: t('core.ProposalProcedure.governanceAction.technicalGroup.collateralPercentage'), + tooltip: { + a0: t('core.ProposalProcedure.governanceAction.technicalGroup.tooltip.a0'), + eMax: t('core.ProposalProcedure.governanceAction.technicalGroup.tooltip.eMax'), + nOpt: t('core.ProposalProcedure.governanceAction.technicalGroup.tooltip.nOpt'), + costModels: t('core.ProposalProcedure.governanceAction.technicalGroup.tooltip.costModels'), + collateralPercentage: t('core.ProposalProcedure.governanceAction.technicalGroup.tooltip.collateralPercentage') + } + }, + governanceGroup: { + title: t('core.ProposalProcedure.governanceAction.governanceGroup.title'), + govActionLifetime: t('core.ProposalProcedure.governanceAction.governanceGroup.govActionLifetime'), + govActionDeposit: t('core.ProposalProcedure.governanceAction.governanceGroup.govActionDeposit'), + drepDeposit: t('core.ProposalProcedure.governanceAction.governanceGroup.drepDeposit'), + drepActivity: t('core.ProposalProcedure.governanceAction.governanceGroup.drepActivity'), + ccMinSize: t('core.ProposalProcedure.governanceAction.governanceGroup.ccMinSize'), + ccMaxTermLength: t('core.ProposalProcedure.governanceAction.governanceGroup.ccMaxTermLength'), + dRepVotingThresholds: { + title: t('core.ProposalProcedure.governanceAction.governanceGroup.dRepVotingThresholds.title'), + motionNoConfidence: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.dRepVotingThresholds.motionNoConfidence' + ), + committeeNormal: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.dRepVotingThresholds.committeeNormal' + ), + committeeNoConfidence: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.dRepVotingThresholds.committeeNoConfidence' + ), + updateConstitution: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.dRepVotingThresholds.updateConstitution' + ), + hardForkInitiation: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.dRepVotingThresholds.hardForkInitiation' + ), + ppNetworkGroup: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.dRepVotingThresholds.ppNetworkGroup' + ), + ppEconomicGroup: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.dRepVotingThresholds.ppEconomicGroup' + ), + ppTechnicalGroup: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.dRepVotingThresholds.ppTechnicalGroup' + ), + ppGovernanceGroup: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.dRepVotingThresholds.ppGovernanceGroup' + ), + treasuryWithdrawal: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.dRepVotingThresholds.treasuryWithdrawal' + ) + }, + tooltip: { + govActionLifetime: t('core.ProposalProcedure.governanceAction.governanceGroup.tooltip.govActionLifetime'), + govActionDeposit: t('core.ProposalProcedure.governanceAction.governanceGroup.tooltip.govActionDeposit'), + drepDeposit: t('core.ProposalProcedure.governanceAction.governanceGroup.tooltip.drepDeposit'), + drepActivity: t('core.ProposalProcedure.governanceAction.governanceGroup.tooltip.drepActivity'), + ccMinSize: t('core.ProposalProcedure.governanceAction.governanceGroup.tooltip.ccMinSize'), + ccMaxTermLength: t('core.ProposalProcedure.governanceAction.governanceGroup.tooltip.ccMaxTermLength'), + dRepVotingThresholds: { + title: t('core.ProposalProcedure.governanceAction.governanceGroup.tooltip.dRepVotingThresholds.title'), + motionNoConfidence: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.tooltip.dRepVotingThresholds.motionNoConfidence' + ), + committeeNormal: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.tooltip.dRepVotingThresholds.committeeNormal' + ), + committeeNoConfidence: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.tooltip.dRepVotingThresholds.committeeNoConfidence' + ), + updateConstitution: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.tooltip.dRepVotingThresholds.updateConstitution' + ), + hardForkInitiation: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.tooltip.dRepVotingThresholds.hardForkInitiation' + ), + ppNetworkGroup: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.tooltip.dRepVotingThresholds.ppNetworkGroup' + ), + ppEconomicGroup: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.tooltip.dRepVotingThresholds.ppEconomicGroup' + ), + ppTechnicalGroup: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.tooltip.dRepVotingThresholds.ppTechnicalGroup' + ), + ppGovernanceGroup: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.tooltip.dRepVotingThresholds.ppGovernanceGroup' + ), + treasuryWithdrawal: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.tooltip.dRepVotingThresholds.treasuryWithdrawal' + ) + } + } + } + }), + [t] + ); + + const { + protocolParamUpdate: { + maxBlockBodySize, + maxTxSize, + maxBlockHeaderSize, + maxValueSize, + maxExecutionUnitsPerTransaction, + maxExecutionUnitsPerBlock, + maxCollateralInputs, + stakeKeyDeposit, + poolDeposit, + minFeeCoefficient, + minFeeConstant, + treasuryExpansion, + monetaryExpansion, + minPoolCost, + coinsPerUtxoByte, + prices, + poolInfluence, + poolRetirementEpochBound, + desiredNumberOfPools, + costModels, + collateralPercentage, + governanceActionDeposit, + dRepDeposit, + governanceActionValidityPeriod, + dRepInactivityPeriod, + minCommitteeSize, + committeeTermLimit, + dRepVotingThresholds: { + motionNoConfidence, + committeeNormal, + commiteeNoConfidence, + updateConstitution, + hardForkInitiation, + ppNetworkGroup, + ppEconomicGroup, + ppTechnicalGroup, + ppGovernanceGroup, + treasuryWithdrawal + } + } + } = governanceAction; + + const data: Parameters[0]['data'] = { + txDetails: { + txType: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.title'), + deposit: Wallet.util.getFormattedAmount({ + amount: deposit.toString(), + cardanoCoin + }), + rewardAccount + }, + anchor: { + url: anchor.url, + hash: anchor.dataHash, + txHashUrl: `${explorerBaseUrl}/${anchor.dataHash}` + }, + protocolParamUpdate: { + maxTxExUnits: { + memory: maxExecutionUnitsPerTransaction.memory.toString(), + step: maxExecutionUnitsPerTransaction.steps.toString() + }, + maxBlockExUnits: { + memory: maxExecutionUnitsPerBlock.memory.toString(), + step: maxExecutionUnitsPerBlock.steps.toString() + }, + networkGroup: { + maxBBSize: maxBlockBodySize.toString(), + maxTxSize: maxTxSize.toString(), + maxBHSize: maxBlockHeaderSize.toString(), + maxValSize: maxValueSize.toString(), + maxCollateralInputs: maxCollateralInputs.toString() + }, + economicGroup: { + minFeeA: minFeeCoefficient.toString(), + minFeeB: minFeeConstant.toString(), + keyDeposit: stakeKeyDeposit.toString(), + poolDeposit: poolDeposit.toString(), + rho: monetaryExpansion, + tau: treasuryExpansion, + minPoolCost: minPoolCost.toString(), + coinsPerUTxOByte: coinsPerUtxoByte.toString(), + price: { + memory: prices.memory.toString(), + step: prices.steps.toString() + } + }, + technicalGroup: { + a0: poolInfluence, + eMax: poolRetirementEpochBound.toString(), + nOpt: desiredNumberOfPools.toString(), + costModels: { + PlutusV1: Object.entries(costModels.get(Wallet.Cardano.PlutusLanguageVersion.V1)).reduce( + (acc, cur) => ({ ...acc, [cur[0]]: cur[1] }), + {} + ), + PlutusV2: Object.entries(costModels.get(Wallet.Cardano.PlutusLanguageVersion.V2)).reduce( + (acc, cur) => ({ ...acc, [cur[0]]: cur[1] }), + {} + ) + }, + collateralPercentage: collateralPercentage.toString() + }, + governanceGroup: { + govActionLifetime: governanceActionValidityPeriod.toString(), + govActionDeposit: governanceActionDeposit.toString(), + drepDeposit: dRepDeposit.toString(), + drepActivity: dRepInactivityPeriod.toString(), + ccMinSize: minCommitteeSize.toString(), + ccMaxTermLength: committeeTermLimit.toString(), + dRepVotingThresholds: { + motionNoConfidence: formatPercentages(motionNoConfidence.numerator / motionNoConfidence.denominator), + committeeNormal: formatPercentages(committeeNormal.numerator / committeeNormal.denominator), + committeeNoConfidence: formatPercentages(commiteeNoConfidence.numerator / commiteeNoConfidence.denominator), + updateToConstitution: formatPercentages(updateConstitution.numerator / updateConstitution.denominator), + hardForkInitiation: formatPercentages(hardForkInitiation.numerator / hardForkInitiation.denominator), + ppNetworkGroup: formatPercentages(ppNetworkGroup.numerator / ppNetworkGroup.denominator), + ppEconomicGroup: formatPercentages(ppEconomicGroup.numerator / ppEconomicGroup.denominator), + ppTechnicalGroup: formatPercentages(ppTechnicalGroup.numerator / ppTechnicalGroup.denominator), + ppGovGroup: formatPercentages(ppGovernanceGroup.numerator / ppGovernanceGroup.denominator), + treasuryWithdrawal: formatPercentages(treasuryWithdrawal.numerator / treasuryWithdrawal.denominator) + } + } + } + }; + + return ( + + ); +}; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/TreasuryWithdrawalsActionContainer.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/TreasuryWithdrawalsActionContainer.tsx new file mode 100644 index 000000000..ed81ffb33 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/TreasuryWithdrawalsActionContainer.tsx @@ -0,0 +1,95 @@ +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Wallet } from '@lace/cardano'; +import { TreasuryWithdrawalsAction } from '@lace/core'; +import { useWalletStore } from '@src/stores'; +import { SignTxData } from '../types'; +import { useCexplorerBaseUrl } from '../hooks'; +import { depositPaidWithSymbol } from '../utils'; + +interface Props { + dappInfo: SignTxData['dappInfo']; + governanceAction: Wallet.Cardano.TreasuryWithdrawalsAction; + deposit: Wallet.Cardano.ProposalProcedure['deposit']; + rewardAccount: Wallet.Cardano.ProposalProcedure['rewardAccount']; + anchor: Wallet.Cardano.ProposalProcedure['anchor']; + errorMessage?: string; +} + +export const TreasuryWithdrawalsActionContainer = ({ + dappInfo, + governanceAction, + deposit, + rewardAccount, + anchor, + errorMessage +}: Props): React.ReactElement => { + const { t } = useTranslation(); + const { + walletUI: { cardanoCoin } + } = useWalletStore(); + + const explorerBaseUrl = useCexplorerBaseUrl(); + + const translations = useMemo[0]['translations']>( + () => ({ + txDetails: { + title: t('core.ProposalProcedure.txDetails.title'), + txType: t('core.ProposalProcedure.txDetails.txType'), + deposit: t('core.ProposalProcedure.txDetails.deposit'), + rewardAccount: t('core.ProposalProcedure.txDetails.rewardAccount') + }, + procedure: { + title: t('core.ProposalProcedure.procedure.title'), + anchor: { + url: t('core.ProposalProcedure.procedure.anchor.url'), + hash: t('core.ProposalProcedure.procedure.anchor.hash') + } + }, + actionId: { + title: t('core.ProposalProcedure.governanceAction.actionId.title'), + index: t('core.ProposalProcedure.governanceAction.actionId.index'), + txId: t('core.ProposalProcedure.governanceAction.actionId.txId') + }, + withdrawals: { + title: t('core.ProposalProcedure.governanceAction.treasuryWithdrawals.title'), + rewardAccount: t('core.ProposalProcedure.governanceAction.treasuryWithdrawals.withdrawals.rewardAccount'), + lovelace: t('core.ProposalProcedure.governanceAction.treasuryWithdrawals.withdrawals.lovelace') + } + }), + [t] + ); + + const { withdrawals } = governanceAction; + + const data: Parameters[0]['data'] = { + txDetails: { + txType: t('core.ProposalProcedure.governanceAction.treasuryWithdrawals.title'), + deposit: depositPaidWithSymbol(deposit, cardanoCoin), + rewardAccount + }, + procedure: { + anchor: { + url: anchor.url, + hash: anchor.dataHash, + txHashUrl: `${explorerBaseUrl}/${anchor.dataHash}` + } + }, + withdrawals: [...withdrawals].map((withdrawal) => ({ + rewardAccount: withdrawal.rewardAccount.toString(), + lovelace: Wallet.util.getFormattedAmount({ + amount: withdrawal.coin.toString(), + cardanoCoin + }) + })) + }; + + return ( + + ); +}; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/UpdateCommitteeActionContainer.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/UpdateCommitteeActionContainer.tsx new file mode 100644 index 000000000..81cad6e0f --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/UpdateCommitteeActionContainer.tsx @@ -0,0 +1,108 @@ +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Wallet } from '@lace/cardano'; +import { UpdateCommitteeAction } from '@lace/core'; +import { useWalletStore } from '@src/stores'; +import { SignTxData } from '../types'; +import { useCexplorerBaseUrl } from '../hooks'; + +interface Props { + dappInfo: SignTxData['dappInfo']; + governanceAction: Wallet.Cardano.UpdateCommittee; + deposit: Wallet.Cardano.ProposalProcedure['deposit']; + rewardAccount: Wallet.Cardano.ProposalProcedure['rewardAccount']; + anchor: Wallet.Cardano.ProposalProcedure['anchor']; + errorMessage?: string; +} + +export const UpdateCommitteeActionContainer = ({ + dappInfo, + governanceAction, + deposit, + rewardAccount, + anchor, + errorMessage +}: Props): React.ReactElement => { + const { t } = useTranslation(); + const { + walletUI: { cardanoCoin } + } = useWalletStore(); + + const explorerBaseUrl = useCexplorerBaseUrl(); + + const translations = useMemo[0]['translations']>( + () => ({ + txDetails: { + title: t('core.ProposalProcedure.txDetails.title'), + txType: t('core.ProposalProcedure.txDetails.txType'), + deposit: t('core.ProposalProcedure.txDetails.deposit'), + rewardAccount: t('core.ProposalProcedure.txDetails.rewardAccount') + }, + procedure: { + title: t('core.ProposalProcedure.procedure.title'), + anchor: { + url: t('core.ProposalProcedure.procedure.anchor.url'), + hash: t('core.ProposalProcedure.procedure.anchor.hash') + } + }, + actionId: { + title: t('core.ProposalProcedure.governanceAction.actionId.title'), + index: t('core.ProposalProcedure.governanceAction.actionId.index'), + txId: t('core.ProposalProcedure.governanceAction.actionId.txId') + }, + membersToBeAdded: { + title: t('core.ProposalProcedure.governanceAction.updateCommitteeAction.membersToBeAdded.title'), + coldCredential: { + hash: t('core.ProposalProcedure.governanceAction.updateCommitteeAction.membersToBeAdded.coldCredential.hash'), + epoch: t( + 'core.ProposalProcedure.governanceAction.updateCommitteeAction.membersToBeAdded.coldCredential.epoch' + ) + } + }, + membersToBeRemoved: { + title: t('core.ProposalProcedure.governanceAction.updateCommitteeAction.membersToBeRemoved.title'), + hash: t('core.ProposalProcedure.governanceAction.updateCommitteeAction.membersToBeRemoved.hash') + } + }), + [t] + ); + + const { membersToBeAdded, membersToBeRemoved, governanceActionId } = governanceAction; + + const data: Parameters[0]['data'] = { + txDetails: { + txType: t('core.ProposalProcedure.governanceAction.updateCommitteeAction.title'), + deposit: Wallet.util.getFormattedAmount({ + amount: deposit.toString(), + cardanoCoin + }), + rewardAccount + }, + procedure: { + anchor: { + url: anchor.url, + hash: anchor.dataHash, + txHashUrl: `${explorerBaseUrl}/${anchor.dataHash}` + } + }, + ...(governanceActionId && { + actionId: { + index: governanceActionId.actionIndex.toString(), + id: governanceActionId.id || '' + } + }), + membersToBeAdded: [...membersToBeAdded].map(({ coldCredential: { hash }, epoch }) => ({ + coldCredential: { + hash: hash.toString() + }, + epoch: epoch.toString() + })), + membersToBeRemoved: [...membersToBeRemoved].map(({ hash }) => ({ + hash: hash.toString() + })) + }; + + return ( + + ); +}; diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/HardForkInitiationActionContainer.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/HardForkInitiationActionContainer.test.tsx new file mode 100644 index 000000000..d8ca77749 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/HardForkInitiationActionContainer.test.tsx @@ -0,0 +1,177 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-magic-numbers */ +/* eslint-disable import/imports-first */ +const cardanoCoinMock = { + symbol: 'cardanoCoinMockSymbol', + name: 'Cardano' +}; +const mockUseWalletStore = jest.fn(() => ({ + walletUI: { cardanoCoin: cardanoCoinMock }, + walletInfo: {} +})); +const t = jest.fn().mockImplementation((res) => res); +const mockUseTranslation = jest.fn(() => ({ t })); +const mockHardForkInitiationAction = jest.fn(() => ); +const mockLovelacesToAdaString = jest.fn((val) => val); +const mockedCExpolorerBaseUrl = 'mockedCExpolorerBaseUrl'; +const mockuseCexplorerBaseUrl = jest.fn(() => mockedCExpolorerBaseUrl); +import { Wallet } from '@lace/cardano'; +import * as React from 'react'; +import { cleanup, render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { act } from 'react-dom/test-utils'; +import { HardForkInitiationActionContainer } from '../HardForkInitiationActionContainer'; +import { getWrapper } from '../../testing.utils'; +import { depositPaidWithSymbol } from '../../utils'; + +jest.mock('react-i18next', () => { + const original = jest.requireActual('react-i18next'); + return { + __esModule: true, + ...original, + useTranslation: mockUseTranslation + }; +}); + +jest.mock('@lace/core', () => { + const original = jest.requireActual('@lace/core'); + return { + __esModule: true, + ...original, + HardForkInitiationAction: mockHardForkInitiationAction + }; +}); + +jest.mock('@src/stores', () => ({ + ...jest.requireActual('@src/stores'), + useWalletStore: mockUseWalletStore +})); + +jest.mock('../../hooks', () => { + const original = jest.requireActual('../../hooks'); + return { + __esModule: true, + ...original, + useCexplorerBaseUrl: mockuseCexplorerBaseUrl + }; +}); + +jest.mock('@lace/cardano', () => { + const actual = jest.requireActual('@lace/cardano'); + return { + __esModule: true, + ...actual, + Wallet: { + ...actual.Wallet, + util: { + ...actual.Wallet.util, + lovelacesToAdaString: mockLovelacesToAdaString + } + } + }; +}); + +const dappInfo = { + name: 'dappName', + logo: 'dappLogo', + url: 'dappUrl' +}; +const errorMessage = 'errorMessage'; +const deposit = BigInt('10000'); +const rewardAccount = Wallet.Cardano.RewardAccount('stake_test1uqrw9tjymlm8wrwq7jk68n6v7fs9qz8z0tkdkve26dylmfc2ux2hj'); +const anchor = { + url: 'anchorUrl', + dataHash: Wallet.Crypto.Hash32ByteBase16(Buffer.from('anchorDataHashanchorDataHashanch').toString('hex')) +}; + +const hardForkInitiationAction = { + protocolVersion: { + major: 123, + minor: 234, + patch: 456 + }, + governanceActionId: { + actionIndex: 123, + id: Wallet.Cardano.TransactionId('724a0a88b9470a714fc5bf84daf5851fa259a9b89e1a5453f6f5cd6595ad9821') + }, + __typename: Wallet.Cardano.GovernanceActionType.hard_fork_initiation_action +} as Wallet.Cardano.HardForkInitiationAction; + +describe('Testing ProposalProceduresContainer component', () => { + afterEach(() => { + jest.clearAllMocks(); + cleanup(); + }); + + test('should render HardForkInitiationAction component with proper props', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render( + , + { + wrapper: getWrapper() + } + )); + }); + + expect(queryByTestId('HardForkInitiationAction')).toBeInTheDocument(); + expect(mockHardForkInitiationAction).toHaveBeenLastCalledWith( + { + dappInfo, + data: { + txDetails: { + txType: t('core.ProposalProcedure.governanceAction.hardForkInitiation.title'), + deposit: depositPaidWithSymbol(deposit, cardanoCoinMock as Wallet.CoinId), + rewardAccount + }, + procedure: { + anchor: { + url: anchor.url, + hash: anchor.dataHash, + txHashUrl: `${mockedCExpolorerBaseUrl}/${anchor.dataHash}` + } + }, + protocolVersion: { + major: hardForkInitiationAction.protocolVersion.major.toString(), + minor: hardForkInitiationAction.protocolVersion.minor.toString(), + patch: hardForkInitiationAction.protocolVersion.patch?.toString() + }, + actionId: { + index: hardForkInitiationAction.governanceActionId.actionIndex.toString(), + id: hardForkInitiationAction.governanceActionId.id || '' + } + }, + translations: { + txDetails: { + title: t('core.ProposalProcedure.txDetails.title'), + txType: t('core.ProposalProcedure.txDetails.txType'), + deposit: t('core.ProposalProcedure.txDetails.deposit'), + rewardAccount: t('core.ProposalProcedure.txDetails.rewardAccount') + }, + procedure: { + title: t('core.ProposalProcedure.procedure.title'), + anchor: { + url: t('core.ProposalProcedure.procedure.anchor.url'), + hash: t('core.ProposalProcedure.procedure.anchor.hash') + } + }, + protocolVersion: { + major: t('core.ProposalProcedure.governanceAction.hardForkInitiation.protocolVersion.major'), + minor: t('core.ProposalProcedure.governanceAction.hardForkInitiation.protocolVersion.minor'), + patch: t('core.ProposalProcedure.governanceAction.hardForkInitiation.protocolVersion.patch') + }, + actionId: { + title: t('core.ProposalProcedure.governanceAction.actionId.title'), + index: t('core.ProposalProcedure.governanceAction.actionId.index'), + txId: t('core.ProposalProcedure.governanceAction.actionId.txId') + } + }, + errorMessage + }, + {} + ); + }); +}); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/InfoActionContainer.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/InfoActionContainer.test.tsx new file mode 100644 index 000000000..f6b6eb5fc --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/InfoActionContainer.test.tsx @@ -0,0 +1,115 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-magic-numbers */ +/* eslint-disable import/imports-first */ +const t = jest.fn().mockImplementation((res) => res); +const mockUseTranslation = jest.fn(() => ({ t })); +const mockInfoAction = jest.fn(() => ); +const mockedCExpolorerBaseUrl = 'mockedCExpolorerBaseUrl'; +const mockuseCexplorerBaseUrl = jest.fn(() => mockedCExpolorerBaseUrl); +import { Wallet } from '@lace/cardano'; +import * as React from 'react'; +import { cleanup, render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { act } from 'react-dom/test-utils'; +import { InfoActionContainer } from '../InfoActionContainer'; +import { getWrapper } from '../../testing.utils'; + +jest.mock('react-i18next', () => { + const original = jest.requireActual('react-i18next'); + return { + __esModule: true, + ...original, + useTranslation: mockUseTranslation + }; +}); + +jest.mock('@lace/core', () => { + const original = jest.requireActual('@lace/core'); + return { + __esModule: true, + ...original, + InfoAction: mockInfoAction + }; +}); + +jest.mock('../../hooks', () => { + const original = jest.requireActual('../../hooks'); + return { + __esModule: true, + ...original, + useCexplorerBaseUrl: mockuseCexplorerBaseUrl + }; +}); + +const dappInfo = { + name: 'dappName', + logo: 'dappLogo', + url: 'dappUrl' +}; +const errorMessage = 'errorMessage'; +const deposit = BigInt('10000'); +const rewardAccount = Wallet.Cardano.RewardAccount('stake_test1uqrw9tjymlm8wrwq7jk68n6v7fs9qz8z0tkdkve26dylmfc2ux2hj'); +const anchor = { + url: 'anchorUrl', + dataHash: Wallet.Crypto.Hash32ByteBase16(Buffer.from('anchorDataHashanchorDataHashanch').toString('hex')) +}; + +const infoAction = { + __typename: Wallet.Cardano.GovernanceActionType.info_action +} as Wallet.Cardano.InfoAction; + +describe('Testing ProposalProceduresContainer component', () => { + afterEach(() => { + jest.clearAllMocks(); + cleanup(); + }); + + test('should render InfoAction component with proper props', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render( + , + { + wrapper: getWrapper() + } + )); + }); + + expect(queryByTestId('InfoAction')).toBeInTheDocument(); + expect(mockInfoAction).toHaveBeenLastCalledWith( + { + dappInfo, + data: { + txDetails: { + txType: t('core.ProposalProcedure.governanceAction.infoAction.title') + }, + procedure: { + anchor: { + url: anchor.url, + hash: anchor.dataHash, + txHashUrl: `${mockedCExpolorerBaseUrl}/${anchor.dataHash}` + } + } + }, + translations: { + txDetails: { + title: t('core.ProposalProcedure.txDetails.title'), + txType: t('core.ProposalProcedure.txDetails.txType') + }, + procedure: { + title: t('core.ProposalProcedure.procedure.title'), + anchor: { + url: t('core.ProposalProcedure.procedure.anchor.url'), + hash: t('core.ProposalProcedure.procedure.anchor.hash') + } + } + }, + errorMessage + }, + {} + ); + }); +}); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/NewConstitutionActionContainer.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/NewConstitutionActionContainer.test.tsx new file mode 100644 index 000000000..82e3f9dd4 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/NewConstitutionActionContainer.test.tsx @@ -0,0 +1,181 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-magic-numbers */ +/* eslint-disable import/imports-first */ +const cardanoCoinMock = { + symbol: 'cardanoCoinMockSymbol', + name: 'Cardano' +}; +const mockUseWalletStore = jest.fn(() => ({ + walletUI: { cardanoCoin: cardanoCoinMock }, + walletInfo: {} +})); +const t = jest.fn().mockImplementation((res) => res); +const mockUseTranslation = jest.fn(() => ({ t })); +const mockNewConstitutionAction = jest.fn(() => ); +const mockLovelacesToAdaString = jest.fn((val) => val); +const mockedCExpolorerBaseUrl = 'mockedCExpolorerBaseUrl'; +const mockuseCexplorerBaseUrl = jest.fn(() => mockedCExpolorerBaseUrl); +import { Wallet } from '@lace/cardano'; +import * as React from 'react'; +import { cleanup, render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { act } from 'react-dom/test-utils'; +import { NewConstitutionActionContainer } from '../NewConstitutionActionContainer'; +import { getWrapper } from '../../testing.utils'; +import { depositPaidWithSymbol } from '../../utils'; + +jest.mock('react-i18next', () => { + const original = jest.requireActual('react-i18next'); + return { + __esModule: true, + ...original, + useTranslation: mockUseTranslation + }; +}); + +jest.mock('@lace/core', () => { + const original = jest.requireActual('@lace/core'); + return { + __esModule: true, + ...original, + NewConstitutionAction: mockNewConstitutionAction + }; +}); + +jest.mock('@src/stores', () => ({ + ...jest.requireActual('@src/stores'), + useWalletStore: mockUseWalletStore +})); + +jest.mock('../../hooks', () => { + const original = jest.requireActual('../../hooks'); + return { + __esModule: true, + ...original, + useCexplorerBaseUrl: mockuseCexplorerBaseUrl + }; +}); + +jest.mock('@lace/cardano', () => { + const actual = jest.requireActual('@lace/cardano'); + return { + __esModule: true, + ...actual, + Wallet: { + ...actual.Wallet, + util: { + ...actual.Wallet.util, + lovelacesToAdaString: mockLovelacesToAdaString + } + } + }; +}); + +const dappInfo = { + name: 'dappName', + logo: 'dappLogo', + url: 'dappUrl' +}; +const errorMessage = 'errorMessage'; +const deposit = BigInt('10000'); +const rewardAccount = Wallet.Cardano.RewardAccount('stake_test1uqrw9tjymlm8wrwq7jk68n6v7fs9qz8z0tkdkve26dylmfc2ux2hj'); +const anchor = { + url: 'anchorUrl', + dataHash: Wallet.Crypto.Hash32ByteBase16(Buffer.from('anchorDataHashanchorDataHashanch').toString('hex')) +}; + +const newConstitution = { + __typename: Wallet.Cardano.GovernanceActionType.new_constitution, + constitution: { + anchor, + scriptHash: Wallet.Crypto.Hash28ByteBase16(Buffer.from('newConstitutionscriptHashnew').toString('hex')) + }, + governanceActionId: { + actionIndex: 123, + id: Wallet.Cardano.TransactionId('724a0a88b9470a714fc5bf84daf5851fa259a9b89e1a5453f6f5cd6595ad9821') + } +} as Wallet.Cardano.NewConstitution; + +describe('Testing ProposalProceduresContainer component', () => { + afterEach(() => { + jest.clearAllMocks(); + cleanup(); + }); + + test('should render NewConstitutionAction component with proper props', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render( + , + { + wrapper: getWrapper() + } + )); + }); + + expect(queryByTestId('NewConstitutionAction')).toBeInTheDocument(); + expect(mockNewConstitutionAction).toHaveBeenLastCalledWith( + { + dappInfo, + data: { + txDetails: { + txType: t('core.ProposalProcedure.governanceAction.newConstitutionAction.title'), + deposit: depositPaidWithSymbol(deposit, cardanoCoinMock as Wallet.CoinId), + rewardAccount + }, + procedure: { + anchor: { + url: anchor.url, + hash: anchor.dataHash, + txHashUrl: `${mockedCExpolorerBaseUrl}/${anchor.dataHash}` + } + }, + actionId: { + index: newConstitution.governanceActionId.actionIndex.toString(), + id: newConstitution.governanceActionId.id || '' + }, + constitution: { + anchor: { + dataHash: newConstitution.constitution.anchor.dataHash.toString(), + url: newConstitution.constitution.anchor.url.toString() + }, + scriptHash: newConstitution.constitution.scriptHash.toString() + } + }, + translations: { + txDetails: { + title: t('core.ProposalProcedure.txDetails.title'), + txType: t('core.ProposalProcedure.txDetails.txType'), + deposit: t('core.ProposalProcedure.txDetails.deposit'), + rewardAccount: t('core.ProposalProcedure.txDetails.rewardAccount') + }, + procedure: { + title: t('core.ProposalProcedure.procedure.title'), + anchor: { + url: t('core.ProposalProcedure.procedure.anchor.url'), + hash: t('core.ProposalProcedure.procedure.anchor.hash') + } + }, + constitution: { + title: t('core.ProposalProcedure.governanceAction.newConstitutionAction.constitution.title'), + anchor: { + dataHash: t('core.ProposalProcedure.governanceAction.newConstitutionAction.constitution.anchor.dataHash'), + url: t('core.ProposalProcedure.governanceAction.newConstitutionAction.constitution.anchor.url') + }, + scriptHash: t('core.ProposalProcedure.governanceAction.newConstitutionAction.constitution.scriptHash') + }, + actionId: { + title: t('core.ProposalProcedure.governanceAction.actionId.title'), + index: t('core.ProposalProcedure.governanceAction.actionId.index'), + txId: t('core.ProposalProcedure.governanceAction.actionId.txId') + } + }, + errorMessage + }, + {} + ); + }); +}); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/NoConfidenceActionContainer.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/NoConfidenceActionContainer.test.tsx new file mode 100644 index 000000000..2327db288 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/NoConfidenceActionContainer.test.tsx @@ -0,0 +1,162 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-magic-numbers */ +/* eslint-disable import/imports-first */ +const cardanoCoinMock = { + symbol: 'cardanoCoinMockSymbol', + name: 'Cardano' +}; +const mockUseWalletStore = jest.fn(() => ({ + walletUI: { cardanoCoin: cardanoCoinMock }, + walletInfo: {} +})); +const t = jest.fn().mockImplementation((res) => res); +const mockUseTranslation = jest.fn(() => ({ t })); +const mockNoConfidenceAction = jest.fn(() => ); +const mockLovelacesToAdaString = jest.fn((val) => val); +const mockedCExpolorerBaseUrl = 'mockedCExpolorerBaseUrl'; +const mockuseCexplorerBaseUrl = jest.fn(() => mockedCExpolorerBaseUrl); +import { Wallet } from '@lace/cardano'; +import * as React from 'react'; +import { cleanup, render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { act } from 'react-dom/test-utils'; +import { NoConfidenceActionContainer } from '../NoConfidenceActionContainer'; +import { getWrapper } from '../../testing.utils'; +import { depositPaidWithSymbol } from '../../utils'; + +jest.mock('react-i18next', () => { + const original = jest.requireActual('react-i18next'); + return { + __esModule: true, + ...original, + useTranslation: mockUseTranslation + }; +}); + +jest.mock('@lace/core', () => { + const original = jest.requireActual('@lace/core'); + return { + __esModule: true, + ...original, + NoConfidenceAction: mockNoConfidenceAction + }; +}); + +jest.mock('@src/stores', () => ({ + ...jest.requireActual('@src/stores'), + useWalletStore: mockUseWalletStore +})); + +jest.mock('../../hooks', () => { + const original = jest.requireActual('../../hooks'); + return { + __esModule: true, + ...original, + useCexplorerBaseUrl: mockuseCexplorerBaseUrl + }; +}); + +jest.mock('@lace/cardano', () => { + const actual = jest.requireActual('@lace/cardano'); + return { + __esModule: true, + ...actual, + Wallet: { + ...actual.Wallet, + util: { + ...actual.Wallet.util, + lovelacesToAdaString: mockLovelacesToAdaString + } + } + }; +}); + +const dappInfo = { + name: 'dappName', + logo: 'dappLogo', + url: 'dappUrl' +}; +const errorMessage = 'errorMessage'; +const deposit = BigInt('10000'); +const rewardAccount = Wallet.Cardano.RewardAccount('stake_test1uqrw9tjymlm8wrwq7jk68n6v7fs9qz8z0tkdkve26dylmfc2ux2hj'); +const anchor = { + url: 'anchorUrl', + dataHash: Wallet.Crypto.Hash32ByteBase16(Buffer.from('anchorDataHashanchorDataHashanch').toString('hex')) +}; + +const noConfidence = { + __typename: Wallet.Cardano.GovernanceActionType.no_confidence, + governanceActionId: { + actionIndex: 123, + id: Wallet.Cardano.TransactionId('724a0a88b9470a714fc5bf84daf5851fa259a9b89e1a5453f6f5cd6595ad9821') + } +} as Wallet.Cardano.NoConfidence; + +describe('Testing ProposalProceduresContainer component', () => { + afterEach(() => { + jest.clearAllMocks(); + cleanup(); + }); + + test('should render NoConfidenceAction component with proper props', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render( + , + { + wrapper: getWrapper() + } + )); + }); + + expect(queryByTestId('NoConfidenceAction')).toBeInTheDocument(); + expect(mockNoConfidenceAction).toHaveBeenLastCalledWith( + { + dappInfo, + data: { + txDetails: { + txType: t('core.ProposalProcedure.governanceAction.noConfidenceAction.title'), + deposit: depositPaidWithSymbol(deposit, cardanoCoinMock as Wallet.CoinId), + rewardAccount + }, + procedure: { + anchor: { + url: anchor.url, + hash: anchor.dataHash, + txHashUrl: `${mockedCExpolorerBaseUrl}/${anchor.dataHash}` + } + }, + actionId: { + index: noConfidence.governanceActionId.actionIndex.toString(), + id: noConfidence.governanceActionId.id || '' + } + }, + translations: { + txDetails: { + title: t('core.ProposalProcedure.txDetails.title'), + txType: t('core.ProposalProcedure.txDetails.txType'), + deposit: t('core.ProposalProcedure.txDetails.deposit'), + rewardAccount: t('core.ProposalProcedure.txDetails.rewardAccount') + }, + procedure: { + title: t('core.ProposalProcedure.procedure.title'), + anchor: { + url: t('core.ProposalProcedure.procedure.anchor.url'), + hash: t('core.ProposalProcedure.procedure.anchor.hash') + } + }, + actionId: { + title: t('core.ProposalProcedure.governanceAction.actionId.title'), + index: t('core.ProposalProcedure.governanceAction.actionId.index'), + txId: t('core.ProposalProcedure.governanceAction.actionId.txId') + } + }, + errorMessage + }, + {} + ); + }); +}); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/ParameterChangeActionContainer.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/ParameterChangeActionContainer.test.tsx new file mode 100644 index 000000000..ab149bddc --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/ParameterChangeActionContainer.test.tsx @@ -0,0 +1,506 @@ +/* eslint-disable unicorn/no-array-reduce */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-magic-numbers */ +/* eslint-disable import/imports-first */ +const cardanoCoinMock = { + symbol: 'cardanoCoinMockSymbol', + name: 'Cardano' +}; +const mockUseWalletStore = jest.fn(() => ({ + walletUI: { cardanoCoin: cardanoCoinMock }, + walletInfo: {} +})); +const t = jest.fn().mockImplementation((res) => res); +const mockUseTranslation = jest.fn(() => ({ t })); +const mockParameterChangeAction = jest.fn(() => ); +const mockLovelacesToAdaString = jest.fn((val) => val); +const mockedCExpolorerBaseUrl = 'mockedCExpolorerBaseUrl'; +const mockuseCexplorerBaseUrl = jest.fn(() => mockedCExpolorerBaseUrl); +import { Wallet } from '@lace/cardano'; +import * as React from 'react'; +import { cleanup, render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { act } from 'react-dom/test-utils'; +import { ParameterChangeActionContainer } from '../ParameterChangeActionContainer'; +import { formatPercentages } from '@lace/common'; +import { getWrapper } from '../../testing.utils'; +import { depositPaidWithSymbol } from '../../utils'; + +jest.mock('react-i18next', () => { + const original = jest.requireActual('react-i18next'); + return { + __esModule: true, + ...original, + useTranslation: mockUseTranslation + }; +}); + +jest.mock('@lace/core', () => { + const original = jest.requireActual('@lace/core'); + return { + __esModule: true, + ...original, + ParameterChangeAction: mockParameterChangeAction + }; +}); + +jest.mock('@src/stores', () => ({ + ...jest.requireActual('@src/stores'), + useWalletStore: mockUseWalletStore +})); + +jest.mock('../../hooks', () => { + const original = jest.requireActual('../../hooks'); + return { + __esModule: true, + ...original, + useCexplorerBaseUrl: mockuseCexplorerBaseUrl + }; +}); + +jest.mock('@lace/cardano', () => { + const actual = jest.requireActual('@lace/cardano'); + return { + __esModule: true, + ...actual, + Wallet: { + ...actual.Wallet, + util: { + ...actual.Wallet.util, + lovelacesToAdaString: mockLovelacesToAdaString + } + } + }; +}); + +const dappInfo = { + name: 'dappName', + logo: 'dappLogo', + url: 'dappUrl' +}; +const errorMessage = 'errorMessage'; +const deposit = BigInt('10000'); +const rewardAccount = Wallet.Cardano.RewardAccount('stake_test1uqrw9tjymlm8wrwq7jk68n6v7fs9qz8z0tkdkve26dylmfc2ux2hj'); +const anchor = { + url: 'anchorUrl', + dataHash: Wallet.Crypto.Hash32ByteBase16(Buffer.from('anchorDataHashanchorDataHashanch').toString('hex')) +}; + +const parameterChangeAction = { + protocolParamUpdate: { + maxBlockBodySize: 1, + maxTxSize: 2, + maxBlockHeaderSize: 3, + maxValueSize: 4, + maxExecutionUnitsPerTransaction: { + memory: 5, + steps: 6 + }, + maxExecutionUnitsPerBlock: { + memory: 7, + steps: 8 + }, + maxCollateralInputs: 9, + stakeKeyDeposit: 10, + poolDeposit: 11, + minFeeCoefficient: 12, + minFeeConstant: 13, + treasuryExpansion: '14', + monetaryExpansion: '15', + minPoolCost: 16, + coinsPerUtxoByte: 17, + prices: { + memory: 18, + steps: 19 + }, + poolInfluence: '20', + poolRetirementEpochBound: 21, + desiredNumberOfPools: 22, + costModels: new Map([ + [0, [23, 24]], + [1, [25, 26]] + ]), + collateralPercentage: 27, + governanceActionDeposit: 28, + dRepDeposit: 29, + governanceActionValidityPeriod: 30, + dRepInactivityPeriod: 31, + minCommitteeSize: 32, + committeeTermLimit: 33, + dRepVotingThresholds: { + motionNoConfidence: { + numerator: 34, + denominator: 35 + }, + committeeNormal: { + numerator: 36, + denominator: 37 + }, + commiteeNoConfidence: { + numerator: 38, + denominator: 39 + }, + updateConstitution: { + numerator: 40, + denominator: 41 + }, + hardForkInitiation: { + numerator: 42, + denominator: 43 + }, + ppNetworkGroup: { + numerator: 44, + denominator: 45 + }, + ppEconomicGroup: { + numerator: 46, + denominator: 47 + }, + ppTechnicalGroup: { + numerator: 48, + denominator: 49 + }, + ppGovernanceGroup: { + numerator: 50, + denominator: 51 + }, + treasuryWithdrawal: { + numerator: 55, + denominator: 56 + } + } + }, + governanceActionId: { + actionIndex: 123, + id: Wallet.Cardano.TransactionId('724a0a88b9470a714fc5bf84daf5851fa259a9b89e1a5453f6f5cd6595ad9821') + }, + __typename: Wallet.Cardano.GovernanceActionType.parameter_change_action +} as Wallet.Cardano.ParameterChangeAction; + +describe('Testing ProposalProceduresContainer component', () => { + afterEach(() => { + jest.clearAllMocks(); + cleanup(); + }); + + test('should render ParameterChangeAction component with proper props', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render( + , + { + wrapper: getWrapper() + } + )); + }); + + expect(queryByTestId('ParameterChangeAction')).toBeInTheDocument(); + expect(mockParameterChangeAction).toHaveBeenLastCalledWith( + { + dappInfo, + data: { + txDetails: { + txType: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.title'), + deposit: depositPaidWithSymbol(deposit, cardanoCoinMock as Wallet.CoinId), + rewardAccount + }, + anchor: { + url: anchor.url, + hash: anchor.dataHash, + txHashUrl: `${mockedCExpolorerBaseUrl}/${anchor.dataHash}` + }, + protocolParamUpdate: { + maxTxExUnits: { + memory: parameterChangeAction.protocolParamUpdate.maxExecutionUnitsPerTransaction.memory.toString(), + step: parameterChangeAction.protocolParamUpdate.maxExecutionUnitsPerTransaction.steps.toString() + }, + maxBlockExUnits: { + memory: parameterChangeAction.protocolParamUpdate.maxExecutionUnitsPerBlock.memory.toString(), + step: parameterChangeAction.protocolParamUpdate.maxExecutionUnitsPerBlock.steps.toString() + }, + networkGroup: { + maxBBSize: parameterChangeAction.protocolParamUpdate.maxBlockBodySize.toString(), + maxTxSize: parameterChangeAction.protocolParamUpdate.maxTxSize.toString(), + maxBHSize: parameterChangeAction.protocolParamUpdate.maxBlockHeaderSize.toString(), + maxValSize: parameterChangeAction.protocolParamUpdate.maxValueSize.toString(), + maxCollateralInputs: parameterChangeAction.protocolParamUpdate.maxCollateralInputs.toString() + }, + economicGroup: { + minFeeA: parameterChangeAction.protocolParamUpdate.minFeeCoefficient.toString(), + minFeeB: parameterChangeAction.protocolParamUpdate.minFeeConstant.toString(), + keyDeposit: parameterChangeAction.protocolParamUpdate.stakeKeyDeposit.toString(), + poolDeposit: parameterChangeAction.protocolParamUpdate.poolDeposit.toString(), + rho: parameterChangeAction.protocolParamUpdate.monetaryExpansion, + tau: parameterChangeAction.protocolParamUpdate.treasuryExpansion, + minPoolCost: parameterChangeAction.protocolParamUpdate.minPoolCost.toString(), + coinsPerUTxOByte: parameterChangeAction.protocolParamUpdate.coinsPerUtxoByte.toString(), + price: { + memory: parameterChangeAction.protocolParamUpdate.prices.memory.toString(), + step: parameterChangeAction.protocolParamUpdate.prices.steps.toString() + } + }, + technicalGroup: { + a0: parameterChangeAction.protocolParamUpdate.poolInfluence, + eMax: parameterChangeAction.protocolParamUpdate.poolRetirementEpochBound.toString(), + nOpt: parameterChangeAction.protocolParamUpdate.desiredNumberOfPools.toString(), + costModels: { + PlutusV1: Object.entries( + parameterChangeAction.protocolParamUpdate.costModels.get(Wallet.Cardano.PlutusLanguageVersion.V1) + ).reduce((acc, cur) => ({ ...acc, [cur[0]]: cur[1] }), {}), + PlutusV2: Object.entries( + parameterChangeAction.protocolParamUpdate.costModels.get(Wallet.Cardano.PlutusLanguageVersion.V2) + ).reduce((acc, cur) => ({ ...acc, [cur[0]]: cur[1] }), {}) + }, + collateralPercentage: parameterChangeAction.protocolParamUpdate.collateralPercentage.toString() + }, + governanceGroup: { + govActionLifetime: parameterChangeAction.protocolParamUpdate.governanceActionValidityPeriod.toString(), + govActionDeposit: parameterChangeAction.protocolParamUpdate.governanceActionDeposit.toString(), + drepDeposit: parameterChangeAction.protocolParamUpdate.dRepDeposit.toString(), + drepActivity: parameterChangeAction.protocolParamUpdate.dRepInactivityPeriod.toString(), + ccMinSize: parameterChangeAction.protocolParamUpdate.minCommitteeSize.toString(), + ccMaxTermLength: parameterChangeAction.protocolParamUpdate.committeeTermLimit.toString(), + dRepVotingThresholds: { + motionNoConfidence: formatPercentages( + parameterChangeAction.protocolParamUpdate.dRepVotingThresholds.motionNoConfidence.numerator / + parameterChangeAction.protocolParamUpdate.dRepVotingThresholds.motionNoConfidence.denominator + ), + committeeNormal: formatPercentages( + parameterChangeAction.protocolParamUpdate.dRepVotingThresholds.committeeNormal.numerator / + parameterChangeAction.protocolParamUpdate.dRepVotingThresholds.committeeNormal.denominator + ), + committeeNoConfidence: formatPercentages( + parameterChangeAction.protocolParamUpdate.dRepVotingThresholds.commiteeNoConfidence.numerator / + parameterChangeAction.protocolParamUpdate.dRepVotingThresholds.commiteeNoConfidence.denominator + ), + updateToConstitution: formatPercentages( + parameterChangeAction.protocolParamUpdate.dRepVotingThresholds.updateConstitution.numerator / + parameterChangeAction.protocolParamUpdate.dRepVotingThresholds.updateConstitution.denominator + ), + hardForkInitiation: formatPercentages( + parameterChangeAction.protocolParamUpdate.dRepVotingThresholds.hardForkInitiation.numerator / + parameterChangeAction.protocolParamUpdate.dRepVotingThresholds.hardForkInitiation.denominator + ), + ppNetworkGroup: formatPercentages( + parameterChangeAction.protocolParamUpdate.dRepVotingThresholds.ppNetworkGroup.numerator / + parameterChangeAction.protocolParamUpdate.dRepVotingThresholds.ppNetworkGroup.denominator + ), + ppEconomicGroup: formatPercentages( + parameterChangeAction.protocolParamUpdate.dRepVotingThresholds.ppEconomicGroup.numerator / + parameterChangeAction.protocolParamUpdate.dRepVotingThresholds.ppEconomicGroup.denominator + ), + ppTechnicalGroup: formatPercentages( + parameterChangeAction.protocolParamUpdate.dRepVotingThresholds.ppTechnicalGroup.numerator / + parameterChangeAction.protocolParamUpdate.dRepVotingThresholds.ppTechnicalGroup.denominator + ), + ppGovGroup: formatPercentages( + parameterChangeAction.protocolParamUpdate.dRepVotingThresholds.ppGovernanceGroup.numerator / + parameterChangeAction.protocolParamUpdate.dRepVotingThresholds.ppGovernanceGroup.denominator + ), + treasuryWithdrawal: formatPercentages( + parameterChangeAction.protocolParamUpdate.dRepVotingThresholds.treasuryWithdrawal.numerator / + parameterChangeAction.protocolParamUpdate.dRepVotingThresholds.treasuryWithdrawal.denominator + ) + } + } + } + }, + translations: { + txDetails: { + title: t('core.ProposalProcedure.txDetails.title'), + txType: t('core.ProposalProcedure.txDetails.txType'), + deposit: t('core.ProposalProcedure.txDetails.deposit'), + rewardAccount: t('core.ProposalProcedure.txDetails.rewardAccount') + }, + anchor: { + url: t('core.ProposalProcedure.procedure.anchor.url'), + hash: t('core.ProposalProcedure.procedure.anchor.hash') + }, + memory: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.memory'), + step: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.step'), + networkGroup: { + title: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.title'), + maxBBSize: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.maxBBSize'), + maxTxSize: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.maxTxSize'), + maxBHSize: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.maxBHSize'), + maxValSize: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.maxValSize'), + maxTxExUnits: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.maxTxExUnits'), + maxBlockExUnits: t( + 'core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.maxBlockExUnits' + ), + maxCollateralInputs: t( + 'core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.maxCollateralInputs' + ), + coinsByUTXOByte: t( + 'core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.coinsByUTXOByte' + ), + tooltip: { + maxBBSize: t( + 'core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.tooltip.maxBBSize' + ), + maxTxSize: t( + 'core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.tooltip.maxTxSize' + ), + maxBHSize: t( + 'core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.tooltip.maxBHSize' + ), + maxValSize: t( + 'core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.tooltip.maxValSize' + ), + maxTxExUnits: t( + 'core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.tooltip.maxTxExUnits' + ), + maxBlockExUnits: t( + 'core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.tooltip.maxBlockExUnits' + ), + maxCollateralInputs: t( + 'core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.tooltip.maxCollateralInputs' + ), + coinsByUTXOByte: t( + 'core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.tooltip.coinsByUTXOByte' + ) + } + }, + economicGroup: { + title: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.title'), + minFeeA: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.minFeeA'), + minFeeB: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.minFeeB'), + keyDeposit: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.keyDeposit'), + poolDeposit: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.poolDeposit'), + rho: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.rho'), + tau: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.tau'), + minPoolCost: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.minPoolCost'), + coinsPerUTxOByte: t( + 'core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.coinsPerUTxOByte' + ), + prices: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.prices'), + tooltip: { + minFeeA: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.tooltip.minFeeA'), + minFeeB: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.tooltip.minFeeB'), + keyDeposit: t( + 'core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.tooltip.keyDeposit' + ), + poolDeposit: t( + 'core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.tooltip.poolDeposit' + ), + rho: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.tooltip.rho'), + tau: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.tooltip.tau'), + minPoolCost: t( + 'core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.tooltip.minPoolCost' + ), + coinsPerUTxOByte: t( + 'core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.tooltip.coinsPerUTxOByte' + ), + prices: t('core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.tooltip.prices') + } + }, + technicalGroup: { + title: t('core.ProposalProcedure.governanceAction.technicalGroup.title'), + a0: t('core.ProposalProcedure.governanceAction.technicalGroup.a0'), + eMax: t('core.ProposalProcedure.governanceAction.technicalGroup.eMax'), + nOpt: t('core.ProposalProcedure.governanceAction.technicalGroup.nOpt'), + costModels: t('core.ProposalProcedure.governanceAction.technicalGroup.costModels'), + collateralPercentage: t('core.ProposalProcedure.governanceAction.technicalGroup.collateralPercentage'), + tooltip: { + a0: t('core.ProposalProcedure.governanceAction.technicalGroup.tooltip.a0'), + eMax: t('core.ProposalProcedure.governanceAction.technicalGroup.tooltip.eMax'), + nOpt: t('core.ProposalProcedure.governanceAction.technicalGroup.tooltip.nOpt'), + costModels: t('core.ProposalProcedure.governanceAction.technicalGroup.tooltip.costModels'), + collateralPercentage: t( + 'core.ProposalProcedure.governanceAction.technicalGroup.tooltip.collateralPercentage' + ) + } + }, + governanceGroup: { + title: t('core.ProposalProcedure.governanceAction.governanceGroup.title'), + govActionLifetime: t('core.ProposalProcedure.governanceAction.governanceGroup.govActionLifetime'), + govActionDeposit: t('core.ProposalProcedure.governanceAction.governanceGroup.govActionDeposit'), + drepDeposit: t('core.ProposalProcedure.governanceAction.governanceGroup.drepDeposit'), + drepActivity: t('core.ProposalProcedure.governanceAction.governanceGroup.drepActivity'), + ccMinSize: t('core.ProposalProcedure.governanceAction.governanceGroup.ccMinSize'), + ccMaxTermLength: t('core.ProposalProcedure.governanceAction.governanceGroup.ccMaxTermLength'), + dRepVotingThresholds: { + title: t('core.ProposalProcedure.governanceAction.governanceGroup.dRepVotingThresholds.title'), + motionNoConfidence: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.dRepVotingThresholds.motionNoConfidence' + ), + committeeNormal: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.dRepVotingThresholds.committeeNormal' + ), + committeeNoConfidence: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.dRepVotingThresholds.committeeNoConfidence' + ), + updateConstitution: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.dRepVotingThresholds.updateConstitution' + ), + hardForkInitiation: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.dRepVotingThresholds.hardForkInitiation' + ), + ppNetworkGroup: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.dRepVotingThresholds.ppNetworkGroup' + ), + ppEconomicGroup: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.dRepVotingThresholds.ppEconomicGroup' + ), + ppTechnicalGroup: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.dRepVotingThresholds.ppTechnicalGroup' + ), + ppGovernanceGroup: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.dRepVotingThresholds.ppGovernanceGroup' + ), + treasuryWithdrawal: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.dRepVotingThresholds.treasuryWithdrawal' + ) + }, + tooltip: { + govActionLifetime: t('core.ProposalProcedure.governanceAction.governanceGroup.tooltip.govActionLifetime'), + govActionDeposit: t('core.ProposalProcedure.governanceAction.governanceGroup.tooltip.govActionDeposit'), + drepDeposit: t('core.ProposalProcedure.governanceAction.governanceGroup.tooltip.drepDeposit'), + drepActivity: t('core.ProposalProcedure.governanceAction.governanceGroup.tooltip.drepActivity'), + ccMinSize: t('core.ProposalProcedure.governanceAction.governanceGroup.tooltip.ccMinSize'), + ccMaxTermLength: t('core.ProposalProcedure.governanceAction.governanceGroup.tooltip.ccMaxTermLength'), + dRepVotingThresholds: { + title: t('core.ProposalProcedure.governanceAction.governanceGroup.tooltip.dRepVotingThresholds.title'), + motionNoConfidence: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.tooltip.dRepVotingThresholds.motionNoConfidence' + ), + committeeNormal: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.tooltip.dRepVotingThresholds.committeeNormal' + ), + committeeNoConfidence: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.tooltip.dRepVotingThresholds.committeeNoConfidence' + ), + updateConstitution: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.tooltip.dRepVotingThresholds.updateConstitution' + ), + hardForkInitiation: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.tooltip.dRepVotingThresholds.hardForkInitiation' + ), + ppNetworkGroup: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.tooltip.dRepVotingThresholds.ppNetworkGroup' + ), + ppEconomicGroup: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.tooltip.dRepVotingThresholds.ppEconomicGroup' + ), + ppTechnicalGroup: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.tooltip.dRepVotingThresholds.ppTechnicalGroup' + ), + ppGovernanceGroup: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.tooltip.dRepVotingThresholds.ppGovernanceGroup' + ), + treasuryWithdrawal: t( + 'core.ProposalProcedure.governanceAction.governanceGroup.tooltip.dRepVotingThresholds.treasuryWithdrawal' + ) + } + } + } + }, + errorMessage + }, + {} + ); + }); +}); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/TreasuryWithdrawalsActionContainer.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/TreasuryWithdrawalsActionContainer.test.tsx new file mode 100644 index 000000000..88d93774d --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/TreasuryWithdrawalsActionContainer.test.tsx @@ -0,0 +1,170 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-magic-numbers */ +/* eslint-disable import/imports-first */ +const cardanoCoinMock = { + symbol: 'cardanoCoinMockSymbol', + name: 'Cardano' +}; +const mockUseWalletStore = jest.fn(() => ({ + walletUI: { cardanoCoin: cardanoCoinMock }, + walletInfo: {} +})); +const t = jest.fn().mockImplementation((res) => res); +const mockUseTranslation = jest.fn(() => ({ t })); +const mockTreasuryWithdrawalsAction = jest.fn(() => ); +const mockLovelacesToAdaString = jest.fn((val) => val); +const mockedCExpolorerBaseUrl = 'mockedCExpolorerBaseUrl'; +const mockuseCexplorerBaseUrl = jest.fn(() => mockedCExpolorerBaseUrl); +import { Wallet } from '@lace/cardano'; +import * as React from 'react'; +import { cleanup, render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { act } from 'react-dom/test-utils'; +import { TreasuryWithdrawalsActionContainer } from '../TreasuryWithdrawalsActionContainer'; +import { getWrapper } from '../../testing.utils'; +import { depositPaidWithSymbol } from '../../utils'; + +jest.mock('react-i18next', () => { + const original = jest.requireActual('react-i18next'); + return { + __esModule: true, + ...original, + useTranslation: mockUseTranslation + }; +}); + +jest.mock('@lace/core', () => { + const original = jest.requireActual('@lace/core'); + return { + __esModule: true, + ...original, + TreasuryWithdrawalsAction: mockTreasuryWithdrawalsAction + }; +}); + +jest.mock('@src/stores', () => ({ + ...jest.requireActual('@src/stores'), + useWalletStore: mockUseWalletStore +})); + +jest.mock('../../hooks', () => { + const original = jest.requireActual('../../hooks'); + return { + __esModule: true, + ...original, + useCexplorerBaseUrl: mockuseCexplorerBaseUrl + }; +}); + +jest.mock('@lace/cardano', () => { + const actual = jest.requireActual('@lace/cardano'); + return { + __esModule: true, + ...actual, + Wallet: { + ...actual.Wallet, + util: { + ...actual.Wallet.util, + lovelacesToAdaString: mockLovelacesToAdaString + } + } + }; +}); + +const dappInfo = { + name: 'dappName', + logo: 'dappLogo', + url: 'dappUrl' +}; +const errorMessage = 'errorMessage'; +const deposit = BigInt('10000'); +const rewardAccount = Wallet.Cardano.RewardAccount('stake_test1uqrw9tjymlm8wrwq7jk68n6v7fs9qz8z0tkdkve26dylmfc2ux2hj'); +const anchor = { + url: 'anchorUrl', + dataHash: Wallet.Crypto.Hash32ByteBase16(Buffer.from('anchorDataHashanchorDataHashanch').toString('hex')) +}; + +const treasuryWithdrawalsAction = { + withdrawals: new Set([ + { rewardAccount, coin: BigInt('10000000') }, + { rewardAccount, coin: BigInt('10000001') } + ]), + __typename: Wallet.Cardano.GovernanceActionType.treasury_withdrawals_action +} as Wallet.Cardano.TreasuryWithdrawalsAction; + +describe('Testing ProposalProceduresContainer component', () => { + afterEach(() => { + jest.clearAllMocks(); + cleanup(); + }); + + test('should render TreasuryWithdrawalsAction component with proper props', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render( + , + { + wrapper: getWrapper() + } + )); + }); + + expect(queryByTestId('TreasuryWithdrawalsAction')).toBeInTheDocument(); + expect(mockTreasuryWithdrawalsAction).toHaveBeenLastCalledWith( + { + dappInfo, + data: { + txDetails: { + txType: t('core.ProposalProcedure.governanceAction.treasuryWithdrawals.title'), + deposit: depositPaidWithSymbol(deposit, cardanoCoinMock as Wallet.CoinId), + rewardAccount + }, + procedure: { + anchor: { + url: anchor.url, + hash: anchor.dataHash, + txHashUrl: `${mockedCExpolorerBaseUrl}/${anchor.dataHash}` + } + }, + withdrawals: [...treasuryWithdrawalsAction.withdrawals].map((withdrawal) => ({ + rewardAccount: withdrawal.rewardAccount.toString(), + lovelace: Wallet.util.getFormattedAmount({ + amount: withdrawal.coin.toString(), + cardanoCoin: cardanoCoinMock as Wallet.CoinId + }) + })) + }, + translations: { + txDetails: { + title: t('core.ProposalProcedure.txDetails.title'), + txType: t('core.ProposalProcedure.txDetails.txType'), + deposit: t('core.ProposalProcedure.txDetails.deposit'), + rewardAccount: t('core.ProposalProcedure.txDetails.rewardAccount') + }, + procedure: { + title: t('core.ProposalProcedure.procedure.title'), + anchor: { + url: t('core.ProposalProcedure.procedure.anchor.url'), + hash: t('core.ProposalProcedure.procedure.anchor.hash') + } + }, + actionId: { + title: t('core.ProposalProcedure.governanceAction.actionId.title'), + index: t('core.ProposalProcedure.governanceAction.actionId.index'), + txId: t('core.ProposalProcedure.governanceAction.actionId.txId') + }, + withdrawals: { + title: t('core.ProposalProcedure.governanceAction.treasuryWithdrawals.title'), + rewardAccount: t('core.ProposalProcedure.governanceAction.treasuryWithdrawals.withdrawals.rewardAccount'), + lovelace: t('core.ProposalProcedure.governanceAction.treasuryWithdrawals.withdrawals.lovelace') + } + }, + errorMessage + }, + {} + ); + }); +}); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/UpdateCommitteeActionContainer.test.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/UpdateCommitteeActionContainer.test.tsx new file mode 100644 index 000000000..39bc1874d --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/proposal-procedures/__tests__/UpdateCommitteeActionContainer.test.tsx @@ -0,0 +1,210 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-magic-numbers */ +/* eslint-disable import/imports-first */ +const cardanoCoinMock = { + symbol: 'cardanoCoinMockSymbol', + name: 'Cardano' +}; +const mockUseWalletStore = jest.fn(() => ({ + walletUI: { cardanoCoin: cardanoCoinMock }, + walletInfo: {} +})); +const t = jest.fn().mockImplementation((res) => res); +const mockUseTranslation = jest.fn(() => ({ t })); +const mockUpdateCommitteeAction = jest.fn(() => ); +const mockLovelacesToAdaString = jest.fn((val) => val); +const mockedCExpolorerBaseUrl = 'mockedCExpolorerBaseUrl'; +const mockuseCexplorerBaseUrl = jest.fn(() => mockedCExpolorerBaseUrl); +import { Wallet } from '@lace/cardano'; +import * as React from 'react'; +import { cleanup, render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { act } from 'react-dom/test-utils'; +import { UpdateCommitteeActionContainer } from '../UpdateCommitteeActionContainer'; +import { getWrapper } from '../../testing.utils'; +import { depositPaidWithSymbol } from '../../utils'; + +jest.mock('react-i18next', () => { + const original = jest.requireActual('react-i18next'); + return { + __esModule: true, + ...original, + useTranslation: mockUseTranslation + }; +}); + +jest.mock('@lace/core', () => { + const original = jest.requireActual('@lace/core'); + return { + __esModule: true, + ...original, + UpdateCommitteeAction: mockUpdateCommitteeAction + }; +}); + +jest.mock('@src/stores', () => ({ + ...jest.requireActual('@src/stores'), + useWalletStore: mockUseWalletStore +})); + +jest.mock('../../hooks', () => { + const original = jest.requireActual('../../hooks'); + return { + __esModule: true, + ...original, + useCexplorerBaseUrl: mockuseCexplorerBaseUrl + }; +}); + +jest.mock('@lace/cardano', () => { + const actual = jest.requireActual('@lace/cardano'); + return { + __esModule: true, + ...actual, + Wallet: { + ...actual.Wallet, + util: { + ...actual.Wallet.util, + lovelacesToAdaString: mockLovelacesToAdaString + } + } + }; +}); + +const dappInfo = { + name: 'dappName', + logo: 'dappLogo', + url: 'dappUrl' +}; +const errorMessage = 'errorMessage'; +const deposit = BigInt('10000'); +const rewardAccount = Wallet.Cardano.RewardAccount('stake_test1uqrw9tjymlm8wrwq7jk68n6v7fs9qz8z0tkdkve26dylmfc2ux2hj'); +const anchor = { + url: 'anchorUrl', + dataHash: Wallet.Crypto.Hash32ByteBase16(Buffer.from('anchorDataHashanchorDataHashanch').toString('hex')) +}; + +const updateCommittee = { + membersToBeAdded: new Set([ + { + coldCredential: { + type: 0, + hash: Wallet.Crypto.Hash28ByteBase16(Buffer.from('updateCommitteecoldCredenti1').toString('hex')) + }, + epoch: 1 + }, + { + coldCredential: { + type: 1, + hash: Wallet.Crypto.Hash28ByteBase16(Buffer.from('updateCommitteecoldCredenti2').toString('hex')) + }, + epoch: 2 + } + ]), + membersToBeRemoved: new Set([ + { + type: 0, + hash: Wallet.Crypto.Hash28ByteBase16(Buffer.from('updateCommitteecoldCredenti2').toString('hex')) + }, + { + type: 1, + hash: Wallet.Crypto.Hash28ByteBase16(Buffer.from('updateCommitteecoldCredenti3').toString('hex')) + } + ]), + governanceActionId: { + actionIndex: 123, + id: Wallet.Cardano.TransactionId('724a0a88b9470a714fc5bf84daf5851fa259a9b89e1a5453f6f5cd6595ad9821') + }, + __typename: Wallet.Cardano.GovernanceActionType.update_committee +} as Wallet.Cardano.UpdateCommittee; + +describe('Testing ProposalProceduresContainer component', () => { + afterEach(() => { + jest.clearAllMocks(); + cleanup(); + }); + + test('should render UpdateCommitteeAction component with proper props', async () => { + let queryByTestId: any; + + await act(async () => { + ({ queryByTestId } = render( + , + { + wrapper: getWrapper() + } + )); + }); + + expect(queryByTestId('UpdateCommitteeAction')).toBeInTheDocument(); + expect(mockUpdateCommitteeAction).toHaveBeenLastCalledWith( + { + dappInfo, + data: { + txDetails: { + txType: t('core.ProposalProcedure.governanceAction.updateCommitteeAction.title'), + deposit: depositPaidWithSymbol(deposit, cardanoCoinMock as Wallet.CoinId), + rewardAccount + }, + procedure: { + anchor: { + url: anchor.url, + hash: anchor.dataHash, + txHashUrl: `${mockedCExpolorerBaseUrl}/${anchor.dataHash}` + } + }, + actionId: { + index: updateCommittee.governanceActionId.actionIndex.toString(), + id: updateCommittee.governanceActionId.id || '' + }, + membersToBeAdded: [...updateCommittee.membersToBeAdded].map(({ coldCredential: { hash }, epoch }) => ({ + coldCredential: { + hash: hash.toString() + }, + epoch: epoch.toString() + })), + membersToBeRemoved: [...updateCommittee.membersToBeRemoved].map(({ hash }) => ({ hash: hash.toString() })) + }, + translations: { + txDetails: { + title: t('core.ProposalProcedure.txDetails.title'), + txType: t('core.ProposalProcedure.txDetails.txType'), + deposit: t('core.ProposalProcedure.txDetails.deposit'), + rewardAccount: t('core.ProposalProcedure.txDetails.rewardAccount') + }, + procedure: { + title: t('core.ProposalProcedure.procedure.title'), + anchor: { + url: t('core.ProposalProcedure.procedure.anchor.url'), + hash: t('core.ProposalProcedure.procedure.anchor.hash') + } + }, + actionId: { + title: t('core.ProposalProcedure.governanceAction.actionId.title'), + index: t('core.ProposalProcedure.governanceAction.actionId.index'), + txId: t('core.ProposalProcedure.governanceAction.actionId.txId') + }, + membersToBeAdded: { + title: t('core.ProposalProcedure.governanceAction.updateCommitteeAction.membersToBeAdded.title'), + coldCredential: { + hash: t( + 'core.ProposalProcedure.governanceAction.updateCommitteeAction.membersToBeAdded.coldCredential.hash' + ), + epoch: t( + 'core.ProposalProcedure.governanceAction.updateCommitteeAction.membersToBeAdded.coldCredential.epoch' + ) + } + }, + membersToBeRemoved: { + title: t('core.ProposalProcedure.governanceAction.updateCommitteeAction.membersToBeRemoved.title'), + hash: t('core.ProposalProcedure.governanceAction.updateCommitteeAction.membersToBeRemoved.hash') + } + }, + errorMessage + }, + {} + ); + }); +}); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/testing.utils.tsx b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/testing.utils.tsx new file mode 100644 index 000000000..3d36e6ec4 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/testing.utils.tsx @@ -0,0 +1,43 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { I18nextProvider } from 'react-i18next'; +import { StoreProvider } from '@src/stores'; +import { + AnalyticsProvider, + AppSettingsProvider, + BackgroundServiceAPIProvider, + BackgroundServiceAPIProviderProps, + DatabaseProvider, + ViewFlowProvider +} from '@src/providers'; +import { APP_MODE_BROWSER } from '@src/utils/constants'; +import i18n from '@lib/i18n'; +import { PostHogClientProvider } from '@providers/PostHogClientProvider'; +import { postHogClientMocks } from '@src/utils/mocks/test-helpers'; +import React from 'react'; +import { sendViewsFlowState } from '../../config'; + +const backgroundService = { + getBackgroundStorage: jest.fn(), + setBackgroundStorage: jest.fn() +} as unknown as BackgroundServiceAPIProviderProps['value']; + +export const getWrapper = + () => + ({ children }: { children: React.ReactNode }): React.ReactElement => + ( + + + + + + + + {children} + + + + + + + + ); diff --git a/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/types.ts b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/types.ts new file mode 100644 index 000000000..9d04096c0 --- /dev/null +++ b/apps/browser-extension-wallet/src/features/dapp/components/confirm-transaction/types.ts @@ -0,0 +1,2 @@ +import { Wallet } from '@lace/cardano'; +export type SignTxData = { dappInfo: Wallet.DappInfo; tx: Wallet.Cardano.Tx }; 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 ad1ffe650..aa50a42c2 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 @@ -1,53 +1,18 @@ +/* eslint-disable sonarjs/no-small-switch */ +/* eslint-disable complexity */ import { Wallet } from '@lace/cardano'; import { assetsBurnedInspector, assetsMintedInspector, createTxInspector } from '@cardano-sdk/core'; -import { CardanoTxOut } from '@src/types'; -import { RemoteApiPropertyType, exposeApi } from '@cardano-sdk/web-extension'; +import { RemoteApiPropertyType, TransactionWitnessRequest, WalletType, exposeApi } from '@cardano-sdk/web-extension'; import type { UserPromptService } from '@lib/scripts/background/services'; -import { DAPP_CHANNELS } from '@src/utils/constants'; +import { DAPP_CHANNELS, cardanoCoin } from '@src/utils/constants'; import { runtime } from 'webextension-polyfill'; import { of } from 'rxjs'; -import { sectionTitle, DAPP_VIEWS } from '../../config'; +import { VoterTypeEnum, getVoterType } from '@src/utils/tx-inspection'; const { CertificateType } = Wallet.Cardano; -export enum TxType { - Send = 'Send', - Mint = 'Mint', - Burn = 'Burn', - DRepRegistration = 'DRepRegistration', - DRepRetirement = 'DRepRetirement', - VoteDelegation = 'VoteDelegation', - VotingProcedures = 'VotingProcedures' -} - -export const getTitleKey = (txType: TxType): string => { - if (txType === TxType.DRepRegistration) { - return 'core.drepRegistration.title'; - } - - if (txType === TxType.DRepRetirement) { - return 'core.drepRetirement.title'; - } - - if (txType === TxType.VoteDelegation) { - return 'core.voteDelegation.title'; - } - - if (txType === TxType.VotingProcedures) { - return 'core.votingProcedures.title'; - } - - return sectionTitle[DAPP_VIEWS.CONFIRM_TX]; -}; +const DAPP_TOAST_DURATION = 50; -/** - * Signing with dapp connector is a 2 step process: - * 1. Open UI window and expose a SigningCoordinator - * 2. Sign - * - * This function exposes an `allowSignTx` observable, which emits a single `true` value to - * the service worker to indicate that the UI has loaded and `SigningCoordinator` is ready to sign - */ export const readyToSign = (): void => { exposeApi>( { @@ -63,17 +28,23 @@ export const readyToSign = (): void => { ); }; -export const getTransactionAssetsId = (outputs: CardanoTxOut[]): Wallet.Cardano.AssetId[] => { - const assetIds: Wallet.Cardano.AssetId[] = []; - const assetMaps = outputs.map((output) => output.value.assets); - for (const asset of assetMaps) { - if (asset) { - for (const id of asset.keys()) { - !assetIds.includes(id) && assetIds.push(id); - } - } +export const disallowSignTx = async ( + req: TransactionWitnessRequest, + close = false +): Promise => { + await req.reject('User declined to sign'); + close && setTimeout(() => window.close(), DAPP_TOAST_DURATION); +}; + +export const allowSignTx = async ( + req: TransactionWitnessRequest, + callback?: () => void +): Promise => { + if (req.walletType !== WalletType.Ledger && req.walletType !== WalletType.Trezor) { + throw new Error('Invalid state: expected hw wallet'); } - return assetIds; + await req.sign(); + callback && callback(); }; export const certificateInspectorFactory = @@ -85,47 +56,93 @@ export const votingProceduresInspector = async ( tx: Wallet.Cardano.Tx ): Promise => tx?.body?.votingProcedures; -export const getTxType = async (tx: Wallet.Cardano.Tx): Promise => { +// eslint-disable-next-line complexity +export const proposalProceduresInspector = async ( + tx: Wallet.Cardano.Tx +): Promise => tx?.body?.proposalProcedures; + +export const getTxType = async (tx: Wallet.Cardano.Tx): Promise => { const inspector = createTxInspector({ minted: assetsMintedInspector, burned: assetsBurnedInspector, votingProcedures: votingProceduresInspector, + proposalProcedures: proposalProceduresInspector, dRepRegistration: certificateInspectorFactory(CertificateType.RegisterDelegateRepresentative), dRepRetirement: certificateInspectorFactory(CertificateType.UnregisterDelegateRepresentative), - voteDelegation: certificateInspectorFactory(CertificateType.VoteDelegation) + dRepUpdate: certificateInspectorFactory(CertificateType.UpdateDelegateRepresentative), + voteDelegation: certificateInspectorFactory(CertificateType.VoteDelegation), + voteRegistrationDelegation: certificateInspectorFactory(CertificateType.VoteRegistrationDelegation), + stakeVoteDelegation: certificateInspectorFactory(CertificateType.StakeVoteDelegation), + stakeRegistrationDelegation: certificateInspectorFactory(CertificateType.StakeRegistrationDelegation), + stakeVoteDelegationRegistration: certificateInspectorFactory(CertificateType.StakeVoteRegistrationDelegation) }); - const { minted, burned, dRepRegistration, dRepRetirement, voteDelegation, votingProcedures } = await inspector( - tx as Wallet.Cardano.HydratedTx - ); + const { + minted, + burned, + votingProcedures, + dRepRegistration, + dRepRetirement, + dRepUpdate, + voteDelegation, + stakeVoteDelegation, + voteRegistrationDelegation, + stakeRegistrationDelegation, + stakeVoteDelegationRegistration, + proposalProcedures + } = await inspector(tx as Wallet.Cardano.HydratedTx); const isMintTransaction = minted.length > 0; const isBurnTransaction = burned.length > 0; + if (proposalProcedures) { + return Wallet.Cip30TxType.ProposalProcedures; + } + if (votingProcedures) { - return TxType.VotingProcedures; + return Wallet.Cip30TxType.VotingProcedures; } if (isMintTransaction) { - return TxType.Mint; + return Wallet.Cip30TxType.Mint; } if (isBurnTransaction) { - return TxType.Burn; + return Wallet.Cip30TxType.Burn; } if (dRepRegistration) { - return TxType.DRepRegistration; + return Wallet.Cip30TxType.DRepRegistration; } if (dRepRetirement) { - return TxType.DRepRetirement; + return Wallet.Cip30TxType.DRepRetirement; } if (voteDelegation) { - return TxType.VoteDelegation; + return Wallet.Cip30TxType.VoteDelegation; } - return TxType.Send; + if (stakeVoteDelegation) { + return Wallet.Cip30TxType.StakeVoteDelegation; + } + + if (voteRegistrationDelegation) { + return Wallet.Cip30TxType.VoteRegistrationDelegation; + } + + if (stakeRegistrationDelegation) { + return Wallet.Cip30TxType.StakeRegistrationDelegation; + } + + if (stakeVoteDelegationRegistration) { + return Wallet.Cip30TxType.StakeVoteDelegationRegistration; + } + + if (dRepUpdate) { + return Wallet.Cip30TxType.DRepUpdate; + } + + return Wallet.Cip30TxType.Send; }; export const drepIDasBech32FromHash = (value: Wallet.Crypto.Hash28ByteBase16): Wallet.Cardano.DRepID => @@ -139,9 +156,34 @@ export const pubDRepKeyToHash = async ( return Wallet.Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(drepKeyHex); }; -export const getOwnRetirementMessageKey = (isOwnRetirement: boolean | undefined): string => { - if (isOwnRetirement === undefined) { - return ''; +export const depositPaidWithSymbol = (deposit: bigint, coinId: Wallet.CoinId): string => { + switch (coinId.name) { + case cardanoCoin.name: + return Wallet.util.getFormattedAmount({ + amount: deposit.toString(), + cardanoCoin: coinId + }); + default: + 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 isOwnRetirement ? 'core.drepRetirement.isOwnRetirement' : 'core.drepRetirement.isNotOwnRetirement'; + 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/features/dapp/components/index.ts b/apps/browser-extension-wallet/src/features/dapp/components/index.ts index 4d3125556..c96197a71 100644 --- a/apps/browser-extension-wallet/src/features/dapp/components/index.ts +++ b/apps/browser-extension-wallet/src/features/dapp/components/index.ts @@ -2,7 +2,6 @@ export * from './Connect'; export * from './SignTxFlowContainer'; export * from './DappTransactionSuccess'; export * from './DappTransactionFail'; -export * from './NoWallet'; export * from './ConfirmData'; export * from './BetaPill'; export * from './SignDataFlowContainer'; diff --git a/apps/browser-extension-wallet/src/features/dapp/config/ViewsConfig.tsx b/apps/browser-extension-wallet/src/features/dapp/config/ViewsConfig.tsx index 34624df13..dbb0144ef 100644 --- a/apps/browser-extension-wallet/src/features/dapp/config/ViewsConfig.tsx +++ b/apps/browser-extension-wallet/src/features/dapp/config/ViewsConfig.tsx @@ -1,5 +1,5 @@ import { IViewsList } from '../../../types'; -import { ConfirmTransaction } from '../components/ConfirmTransaction'; +import { ConfirmTransaction } from '../components/confirm-transaction/ConfirmTransaction'; import { SignTransaction } from '../components/SignTransaction'; import { DappTransactionFail } from '../components/DappTransactionFail'; import { IViewAction, IViewState } from '../../../providers'; diff --git a/apps/browser-extension-wallet/src/hooks/__tests__/useActionExecution.test.ts b/apps/browser-extension-wallet/src/hooks/__tests__/useActionExecution.test.ts index 45b16018f..9a4f74627 100644 --- a/apps/browser-extension-wallet/src/hooks/__tests__/useActionExecution.test.ts +++ b/apps/browser-extension-wallet/src/hooks/__tests__/useActionExecution.test.ts @@ -9,7 +9,7 @@ import { ActionExecutionArgs, useActionExecution } from '@hooks/useActionExecuti import { ToastProps } from '@lace/common'; jest.mock('react-i18next', () => { - const original = jest.requireActual('@lace/common'); + const original = jest.requireActual('react-i18next'); return { __esModule: true, ...original, diff --git a/apps/browser-extension-wallet/src/hooks/__tests__/useDelegationDetails.test.ts b/apps/browser-extension-wallet/src/hooks/__tests__/useDelegationDetails.test.ts index 2a20ab0bd..d6e507db6 100644 --- a/apps/browser-extension-wallet/src/hooks/__tests__/useDelegationDetails.test.ts +++ b/apps/browser-extension-wallet/src/hooks/__tests__/useDelegationDetails.test.ts @@ -29,7 +29,7 @@ jest.mock('../../stores', () => ({ })); describe('Testing useBuildDelegation hook', () => { - process.env.AVAILABLE_CHAINS = process.env.AVAILABLE_CHAINS || 'Mainnet,Preprod,Preview'; + process.env.AVAILABLE_CHAINS = process.env.AVAILABLE_CHAINS || 'Mainnet,Preprod,Preview,Sanchonet'; process.env.DEFAULT_CHAIN = process.env.DEFAULT_CHAIN || 'Preprod'; test('should return use delegation details function', () => { diff --git a/apps/browser-extension-wallet/src/lib/scripts/background/wallet.ts b/apps/browser-extension-wallet/src/lib/scripts/background/wallet.ts index 4e74fa959..34f1da36f 100644 --- a/apps/browser-extension-wallet/src/lib/scripts/background/wallet.ts +++ b/apps/browser-extension-wallet/src/lib/scripts/background/wallet.ts @@ -46,7 +46,7 @@ const chainIdToChainName = (chainId: Cardano.ChainId): Wallet.ChainName => { case Wallet.Cardano.ChainIds.Preview.networkMagic: return 'Preview'; case Wallet.Cardano.ChainIds.Sanchonet.networkMagic: - throw new Error('TODO: add sanchonet option'); + return 'Sanchonet'; default: throw new Error(`Unknown network magic: ${chainId.networkMagic}`); } diff --git a/apps/browser-extension-wallet/src/lib/translations/en.json b/apps/browser-extension-wallet/src/lib/translations/en.json index 553b6865b..ac34de5a9 100644 --- a/apps/browser-extension-wallet/src/lib/translations/en.json +++ b/apps/browser-extension-wallet/src/lib/translations/en.json @@ -23,7 +23,38 @@ "incoming": "Received", "outgoing": "Sent", "sending": "Sending", - "self": "Self Transaction" + "self": "Self Transaction", + "vote": "Vote Signing", + "HardForkInitiationAction": "Hard Fork Initiation Action", + "NewConstitution": "New Constitution Action", + "NoConfidence": "No Confidence Action", + "ParameterChangeAction": "Parameter Change Action", + "TreasuryWithdrawalsAction": "Treasury Withdrawals Action", + "UpdateCommittee": "Update Committee Action", + "InfoAction": "Info Action", + "UpdateDelegateRepresentativeCertificate": "Update DRep", + "StakeVoteDelegationCertificate": "Stake Vote Delegation Certificate", + "StakeRegistrationDelegationCertificate": "Stake Registration Delegation Certificate", + "VoteRegistrationDelegationCertificate": "Vote Registration Delegation Certificate", + "StakeVoteRegistrationDelegationCertificate": "Stake Vote Registration Delegation Certificate", + "ResignCommitteeColdCertificate": "Resign Committee", + "AuthorizeCommitteeHotCertificate": "Authorize Committee", + "RegisterDelegateRepresentativeCertificate": "DRep Registration", + "UnregisterDelegateRepresentativeCertificate": "DRep De-Registration", + "VoteDelegationCertificate": "Vote Delegation", + "StakeRegistrationDelegateCertificate": "Stake Registration Delegation Certificate", + "StakeVoteRegistrationDelegateCertificate": "Stake Vote Registration Delegation Certificate", + "VoteRegistrationDelegateCertificate": "Vote Registration Delegation Certificate" + }, + "certificates": { + "headings": { + "typename": "Certificate Type", + "anchor": "Anchor", + "drep": "DRep", + "deposit": "Deposit paid", + "coldCredential": "Cold credential", + "hotCredential": "Hot credential" + } } } }, @@ -32,7 +63,7 @@ "sent": "Sent", "sending": "Sending", "header": "Summary of your activity", - "transactionHash": "Transaction Hash", + "transactionID": "Transaction ID", "status": "Status", "timestamp": "Timestamp", "inputs": "Inputs", @@ -135,7 +166,7 @@ "mainnet": "Mainnet", "preprod": "Preprod", "preview": "Preview", - "legacyTestnet": "Legacy Testnet", + "sanchonet": "Sanchonet", "custom": "Custom", "offline": "Offline", "error": "Your internet connection is not working. You can still navigate the wallet based on the latest connection you had.", @@ -330,7 +361,7 @@ "copyHandle": "Copy handle" }, "dapp": { - "nowallet.btn": "Create or restore a wallet", + "dappErrorPage.closeButton": "Cancel", "connect.header": "Authorize DApp", "connect.btn.accept": "Authorize", "connect.btn.cancel": "Cancel", @@ -369,6 +400,7 @@ "delete.confirm": "Disconnect DApp", "noWallet.heading": "You don't have a wallet right now", "noWallet.description": "You'll need to create or restore a wallet to connect to a dApp or make a transaction.", + "noWallet.closeButton": "Create or restore a wallet", "educationBanner.title": "DApp Guide", "betaModal": { "header": "DApp connector is now in Beta", @@ -1189,6 +1221,306 @@ "tryingToUseAssetNotInWallet": "This DApp is trying to use token not held in your wallet.", "noCollateral": "Wallet should not be able to sign dapp txs without collateral." }, + "ProposalProcedures": { + "title": "Confirm Governance Action" + }, + "ProposalProcedure": { + "dRepId": "DRep ID", + "txDetails": { + "deposit": "Deposit", + "rewardAccount": "Reward Account", + "title": "Transaction Details", + "txType": "Transaction Type" + }, + "procedure": { + "anchor": { + "hash": "Anchor Hash", + "url": "Anchor URL" + }, + "title": "Procedure", + "dRepId": "DRep ID" + }, + "governanceAction": { + "actionId": { + "title": "Action ID", + "index": "Index", + "txId": "TX ID" + }, + "hardForkInitiation": { + "title": "Hard Fork Initiation", + "protocolVersion": { + "major": "Protocol Version Major", + "minor": "Protocol Version Minor", + "patch": "Protocol Version Patch" + } + }, + "newConstitutionAction": { + "title": "New Constitution Action", + "constitution": { + "title": "Constitution Details", + "anchor": { + "dataHash": "Anchor Data Hash", + "url": "Constitution Anchor URL" + }, + "scriptHash": "Constitution Script Hash" + } + }, + "infoAction": { + "title": "Info Action" + }, + "noConfidenceAction": { + "title": "No Confidence" + }, + "protocolParamUpdate": { + "title": "Protocol Parameter Update", + "memory": "Memory", + "step": "Step", + "networkGroup": { + "title": "Network Group", + "maxBBSize": "Max BB Size", + "maxTxSize": "Max Tx Size", + "maxBHSize": "Max BH Size", + "maxValSize": "Max Val Size", + "maxTxExUnits": "Max TX Ex Units", + "maxBlockExUnits": "Max BLK Ex Units", + "maxCollateralInputs": "Max Coll Inputs", + "tooltip": { + "maxBBSize": "Max block body size", + "maxTxSize": "Max transaction size", + "maxBHSize": "Max block header size", + "maxValSize": "Max size of a serialized asset value", + "maxTxExUnits": "Max script execution units in a single transaction", + "maxBlockExUnits": "Max script execution units in a single block", + "maxCollateralInputs": "Max number of collateral inputs" + } + }, + "economicGroup": { + "title": "Economic Group", + "minFeeA": "Min Fee A", + "minFeeB": "Min Fee B", + "keyDeposit": "Key Deposit", + "poolDeposit": "Pool Deposit", + "rho": "Rho", + "tau": "Tau", + "minPoolCost": "Min Pool Cost", + "coinsPerUTxOByte": "Coins/UTxO Byte", + "prices": "Price", + "tooltip": { + "minFeeA": "Min fee coefficient", + "minFeeB": "Min fee constant", + "keyDeposit": "Delegation key Lovelace deposit", + "poolDeposit": "Pool registration Lovelace deposit", + "rho": "Monetary expansion", + "tau": "Treasury expansion", + "minPoolCost": "Min fixed rewards cut for pools", + "coinsPerUTxOByte": "Min Lovelace deposit per byte of serialized UTxO", + "prices": "Prices of Plutus execution units" + } + }, + "technicalGroup": { + "title": "Technical Group", + "a0": "A0", + "eMax": "EMax", + "nOpt": "NOpt", + "costModels": "Cost Models", + "PlutusV1": "PlutusV1", + "PlutusV2": "PlutusV2", + "collateralPercentage": "Coll Percentage", + "tooltip": { + "a0": "Pool pledge influence", + "eMax": "Pool retirement maximum epoch", + "nOpt": "Desired number of pools", + "costModels": "Plutus execution cost models", + "collateralPercentage": "Proportion of collateral needed for scripts" + } + }, + "governanceGroup": { + "title": "Governance Group", + "govActionLifetime": "Gov Act Lifetime", + "govActionDeposit": "Gov Act Deposit", + "drepDeposit": "DRep Deposit", + "drepActivity": "DRep Activity", + "ccMinSize": "CC Min Size", + "ccMaxTermLength": "CC Max Term Length", + "dRepVotingThresholds": { + "title": "Governance Voting Thresholds", + "motionNoConfidence": "Motion No Conf", + "committeeNormal": "Comm Normal", + "committeeNoConfidence": "Comm No Conf", + "updateConstitution": "Update Const", + "hardForkInitiation": "Hard Fork Init", + "ppNetworkGroup": "PP Network Grp", + "ppEconomicGroup": "PP Economic Grp", + "ppTechnicalGroup": "PP Technical Grp", + "ppGovernanceGroup": "PP Governance Grp", + "treasuryWithdrawal": "Treasury Withdraw" + }, + "tooltip": { + "govActionLifetime": "governance action maximum lifetime in epochs", + "govActionDeposit": "governance action deposit", + "drepDeposit": "DRep deposit amount", + "drepActivity": "DRep activity period in epochs", + "ccMinSize": "Min constitutional committee size", + "ccMaxTermLength": "Max term length (in epochs) for the constitutional committee members", + "dRepVotingThresholds": { + "title": "Governance voting thresholds", + "motionNoConfidence": "1. Motion of no-confidence", + "committeeNormal": "2a. New committee/threshold (normal state)", + "committeeNoConfidence": "2b. New committee/threshold (state of no-confidence)", + "updateConstitution": "3. Update to the Constitution or proposal policy", + "hardForkInitiation": "4. Hard-fork initiation", + "ppNetworkGroup": "5a. Protocol parameter changes, network group", + "ppEconomicGroup": "5b. Protocol parameter changes, economic group", + "ppTechnicalGroup": "5c. Protocol parameter changes, technical group", + "ppGovernanceGroup": "5d. Protocol parameter changes, governance group", + "treasuryWithdrawal": "6. Treasury withdrawal" + } + } + } + }, + "treasuryWithdrawals": { + "title": "Treasury Withdrawals", + "withdrawals": { + "lovelace": "Withdrawal Amount", + "rewardAccount": "Withdrawal Reward Account" + } + }, + "updateCommitteeAction": { + "title": "Update Committee Action", + "membersToBeAdded": { + "title": "Members To Be Added", + "coldCredential": { + "hash": "Cold Credential Hash", + "epoch": "Epoch" + } + }, + "membersToBeRemoved": { + "title": "Members To Be Removed", + "hash": "Hash" + }, + "newQuorumThreshold": { + "title": "New Quorum Threshold", + "denominator": "Denominator", + "numerator": "Numerator" + } + } + } + }, + "VotingProcedures": { + "title": "Confirm Vote", + "voterType": "Voter Type", + "procedureTitle": "Procedure", + "actionIdTitle": "Action ID", + "vote": "Vote", + "actionId": { + "index": "Index", + "txHash": "TX Hash" + }, + "anchor": { + "hash": "Anchor Hash", + "url": "Anchor URL" + }, + "dRepId": "DRep ID", + "voterTypes": { + "constitutionalCommittee": "Constitutional Committee", + "spo": "SPO", + "drep": "DRep" + }, + "votes": { + "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": { + "title": "Confirm DRep Registration", + "metadata": "Metadata", + "url": "URL", + "hash": "Hash", + "drepId": "DRep ID", + "depositPaid": "Deposit paid" + }, + "DRepRetirement": { + "title": "Confirm DRep Retirement", + "metadata": "Metadata", + "drepId": "DRep ID", + "depositReturned": "Deposit returned", + "drepIdMismatchScreen": { + "title": "DRep ID mismatch", + "description": "The presented DRepID does not match your wallet's DRepID", + "cancel": "Cancel" + } + }, + "DRepUpdate": { + "title": "Confirm DRep Update", + "metadata": "Metadata", + "drepId": "DRep ID", + "url": "URL", + "hash": "Hash" + }, + "VoteDelegation": { + "title": "Confirm Vote Delegation", + "metadata": "Metadata", + "drepId": "DRep ID", + "alwaysAbstain": "Abstain", + "alwaysNoConfidence": "No Confidence", + "option": "Yes" + }, + "StakeVoteDelegation": { + "title": "Confirm Staking & Vote Delegation", + "metadata": "Metadata", + "drepId": "DRep ID", + "alwaysAbstain": "Abstain", + "alwaysNoConfidence": "No Confidence", + "option": "Yes", + "stakeKeyHash": "Stake Key Hash", + "poolId": "Pool ID" + }, + "StakeVoteDelegationRegistration": { + "title": "Confirm Vote Delegation & Staking", + "metadata": "Metadata", + "drepId": "DRep ID", + "alwaysAbstain": "Abstain", + "alwaysNoConfidence": "No Confidence", + "option": "Yes", + "stakeKeyHash": "Stake Key Hash", + "poolId": "Pool ID", + "depositPaid": "Deposit paid" + }, + "StakeRegistrationDelegation": { + "title": "Confirm Staking", + "metadata": "Metadata", + "stakeKeyHash": "Stake Key Hash", + "poolId": "Pool ID", + "depositPaid": "Deposit paid" + }, + "VoteRegistrationDelegation": { + "title": "Confirm Vote Delegation", + "metadata": "Metadata", + "drepId": "DRep ID", + "alwaysAbstain": "Abstain", + "alwaysNoConfidence": "No Confidence", + "option": "Yes", + "stakeKeyHash": "Stake Key Hash", + "depositPaid": "Deposit paid" + }, + "Mint": { + "title": "Confirm transaction" + }, + "Burn": { + "title": "Confirm transaction" + }, + "Send": { + "title": "Confirm transaction" + }, "destinationAddressInput": { "recipientAddress": "Recipient's address or $handle" }, diff --git a/apps/browser-extension-wallet/src/providers/PostHogClientProvider/client/PostHogClient.ts b/apps/browser-extension-wallet/src/providers/PostHogClientProvider/client/PostHogClient.ts index e00b84089..e1d917d7d 100644 --- a/apps/browser-extension-wallet/src/providers/PostHogClientProvider/client/PostHogClient.ts +++ b/apps/browser-extension-wallet/src/providers/PostHogClientProvider/client/PostHogClient.ts @@ -44,7 +44,7 @@ export class PostHogClient { ) { if (!this.publicPostHogHost) throw new Error('PUBLIC_POSTHOG_HOST url has not been provided'); const token = this.getApiToken(this.chain); - if (!token) throw new Error('posthog token has not been provided'); + if (!token) throw new Error(`posthog token has not been provided for chain: ${this.chain.networkId}`); this.hasPostHogInitialized$ = new BehaviorSubject(false); this.initSuccess = this.userIdService diff --git a/apps/browser-extension-wallet/src/providers/PostHogClientProvider/client/config.ts b/apps/browser-extension-wallet/src/providers/PostHogClientProvider/client/config.ts index 29831d31d..8fb2af712 100644 --- a/apps/browser-extension-wallet/src/providers/PostHogClientProvider/client/config.ts +++ b/apps/browser-extension-wallet/src/providers/PostHogClientProvider/client/config.ts @@ -9,23 +9,27 @@ export const POSTHOG_EXCLUDED_EVENTS = process.env.POSTHOG_EXCLUDED_EVENTS ?? '' export const DEV_NETWORK_ID_TO_POSTHOG_TOKEN_MAP: Record = { [Wallet.Cardano.NetworkMagics.Mainnet]: process.env.POSTHOG_DEV_TOKEN_MAINNET, [Wallet.Cardano.NetworkMagics.Preprod]: process.env.POSTHOG_DEV_TOKEN_PREPROD, - [Wallet.Cardano.NetworkMagics.Preview]: process.env.POSTHOG_DEV_TOKEN_PREVIEW + [Wallet.Cardano.NetworkMagics.Preview]: process.env.POSTHOG_DEV_TOKEN_PREVIEW, + [Wallet.Cardano.NetworkMagics.Sanchonet]: process.env.POSTHOG_DEV_TOKEN_SANCHONET }; export const PRODUCTION_NETWORK_ID_TO_POSTHOG_TOKEN_MAP: Record = { [Wallet.Cardano.NetworkMagics.Mainnet]: process.env.POSTHOG_PRODUCTION_TOKEN_MAINNET, [Wallet.Cardano.NetworkMagics.Preprod]: process.env.POSTHOG_PRODUCTION_TOKEN_PREPROD, - [Wallet.Cardano.NetworkMagics.Preview]: process.env.POSTHOG_PRODUCTION_TOKEN_PREVIEW + [Wallet.Cardano.NetworkMagics.Preview]: process.env.POSTHOG_PRODUCTION_TOKEN_PREVIEW, + [Wallet.Cardano.NetworkMagics.Sanchonet]: process.env.POSTHOG_PRODUCTION_TOKEN_SANCHONET }; export const DEV_NETWORK_ID_TO_POSTHOG_PROJECT_ID_MAP: Record = { [Wallet.Cardano.NetworkMagics.Mainnet]: 6315, [Wallet.Cardano.NetworkMagics.Preprod]: 6316, - [Wallet.Cardano.NetworkMagics.Preview]: 4874 + [Wallet.Cardano.NetworkMagics.Preview]: 4874, + [Wallet.Cardano.NetworkMagics.Sanchonet]: 11_178 }; export const PRODUCTION_NETWORK_ID_TO_POSTHOG_PROJECT_ID_MAP: Record = { [Wallet.Cardano.NetworkMagics.Mainnet]: 6621, [Wallet.Cardano.NetworkMagics.Preprod]: 6620, - [Wallet.Cardano.NetworkMagics.Preview]: 6619 + [Wallet.Cardano.NetworkMagics.Preview]: 6619, + [Wallet.Cardano.NetworkMagics.Sanchonet]: 11_179 }; diff --git a/apps/browser-extension-wallet/src/providers/ViewFlowProvider/context.tsx b/apps/browser-extension-wallet/src/providers/ViewFlowProvider/context.tsx index a4ea0c089..26d9de5c2 100644 --- a/apps/browser-extension-wallet/src/providers/ViewFlowProvider/context.tsx +++ b/apps/browser-extension-wallet/src/providers/ViewFlowProvider/context.tsx @@ -1,6 +1,5 @@ import { DAPP_VIEWS } from '@src/features/dapp/config'; import React, { useState, createContext, useContext } from 'react'; -import { useSearchParams } from '@lace/common'; import { SignDataRequest, TransactionWitnessRequest } from '@cardano-sdk/web-extension'; import { Wallet } from '@lace/cardano'; @@ -22,6 +21,7 @@ export interface IViewState { const useViewsFlowState = (view: IViewState) => { const [currentView, setCurrentView] = useState(view.initial); + const [dappInfo, setDappInfo] = useState(); const [signTxRequest, setSignTxRequest] = useState< TransactionWitnessRequest | undefined >(); @@ -40,17 +40,11 @@ const useViewsFlowState = (view: IViewState) => { const nextView = view.states[currentView].next; setCurrentView(nextView); }; - - const { logo, url, name } = useSearchParams(['logo', 'url', 'name']); - return { currentView, utils: { renderCurrentView, setPreviousView, setNextView }, - dappInfo: { - logo, - url, - name - }, + dappInfo, + setDappInfo, signTxRequest: { set: setSignTxRequest, request: signTxRequest diff --git a/apps/browser-extension-wallet/src/routes/DappConnectorView.tsx b/apps/browser-extension-wallet/src/routes/DappConnectorView.tsx index e3b62bab3..556936265 100644 --- a/apps/browser-extension-wallet/src/routes/DappConnectorView.tsx +++ b/apps/browser-extension-wallet/src/routes/DappConnectorView.tsx @@ -1,8 +1,8 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { useWalletStore } from '@stores'; import { UnlockWalletContainer } from '@src/features/unlock-wallet'; import { useAppInit } from '@src/hooks'; -import { dAppRoutePaths } from '@routes'; +import { dAppRoutePaths, walletRoutePaths } from '@routes'; import '@lib/i18n'; import 'antd/dist/antd.css'; import { Route, Switch } from 'react-router-dom'; @@ -11,7 +11,6 @@ import { Connect as DappConnect, SignTxFlowContainer, SignDataFlowContainer, - NoWallet, DappTransactionSuccess, DappTransactionFail, DappCollateralContainer @@ -22,6 +21,9 @@ import { lockWalletSelector } from '@src/features/unlock-wallet/selectors'; import { useAppSettingsContext } from '@providers'; import dayjs from 'dayjs'; import duration from 'dayjs/plugin/duration'; +import { DappError } from '@src/features/dapp/components/DappError'; +import { tabs } from 'webextension-polyfill'; +import { useTranslation } from 'react-i18next'; dayjs.extend(duration); @@ -33,6 +35,7 @@ const isLastValidationExpired = (lastVerification: string, frequency: string): b // TODO: unify providers and logic to load wallet and such for popup, dapp and browser view in one place [LW-5341] export const DappConnectorView = (): React.ReactElement => { + const { t } = useTranslation(); const [{ lastMnemonicVerification, mnemonicVerificationFrequency }] = useAppSettingsContext(); const { inMemoryWallet, cardanoWallet, walletInfo, initialHdDiscoveryCompleted } = useWalletStore(); const { isWalletLocked, walletLock } = useWalletStore(lockWalletSelector); @@ -50,10 +53,25 @@ export const DappConnectorView = (): React.ReactElement => { load(); }, [isWalletLocked, cardanoWallet]); + const onCloseClick = useCallback(() => { + tabs.create({ url: `app.html#${walletRoutePaths.setup.home}` }); + window.close(); + }, []); + if (hasNoAvailableWallet) { return ( - + ); } @@ -74,7 +92,6 @@ export const DappConnectorView = (): React.ReactElement => { return ( - diff --git a/apps/browser-extension-wallet/src/stores/slices/__tests__/activity-detail-slice.test.ts b/apps/browser-extension-wallet/src/stores/slices/__tests__/activity-detail-slice.test.ts new file mode 100644 index 000000000..f2c7fe2b7 --- /dev/null +++ b/apps/browser-extension-wallet/src/stores/slices/__tests__/activity-detail-slice.test.ts @@ -0,0 +1,31 @@ +import { CardanoTxOut } from '@src/types'; +import { getTransactionAssetsId } from '../activity-detail-slice'; + +describe('Testing activity detail slice', () => { + test('testing getTransactionAssetsId', () => { + const outputs = [ + { + value: { + assets: new Map([ + ['id1', 'val1'], + ['id2', 'val2'] + ]) + } + }, + { + value: { + assets: new Map([ + ['id1', 'val3'], + ['id3', 'val4'] + ]) + } + }, + { + value: { + assets: new Map([]) + } + } + ]; + expect(getTransactionAssetsId(outputs as unknown as CardanoTxOut[])).toEqual(['id1', 'id2', 'id3']); + }); +}); diff --git a/apps/browser-extension-wallet/src/stores/slices/__tests__/transaction-detail-slice.test.ts b/apps/browser-extension-wallet/src/stores/slices/__tests__/transaction-detail-slice.test.ts index 88c16ff1b..6dc3f3518 100644 --- a/apps/browser-extension-wallet/src/stores/slices/__tests__/transaction-detail-slice.test.ts +++ b/apps/browser-extension-wallet/src/stores/slices/__tests__/transaction-detail-slice.test.ts @@ -1,21 +1,22 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { renderHook, act } from '@testing-library/react-hooks'; -import { BlockchainProviderSlice, ActivityDetailSlice, WalletInfoSlice } from '../../types'; +import { BlockchainProviderSlice, ActivityDetailSlice, WalletInfoSlice, UISlice } from '../../types'; import { transactionMock } from '../../../utils/mocks/test-helpers'; import { activityDetailSlice } from '../activity-detail-slice'; import '@testing-library/jest-dom'; import create, { GetState, SetState } from 'zustand'; import { mockBlockchainProviders } from '@src/utils/mocks/blockchain-providers'; -import { ActivityStatus } from '@lace/core'; +import { ActivityStatus, TransactionActivityType } from '@lace/core'; const mockActivityDetailSlice = ( set: SetState, - get: GetState + get: GetState ): ActivityDetailSlice => { get = () => ({ blockchainProvider: mockBlockchainProviders() } as BlockchainProviderSlice & ActivityDetailSlice & - WalletInfoSlice); + WalletInfoSlice & + UISlice); return activityDetailSlice({ set, get }); }; @@ -45,7 +46,7 @@ describe('Testing createStoreHook slice', () => { act(() => { result.current.setTransactionActivityDetail({ - type: 'incoming', + type: TransactionActivityType.incoming, status: ActivityStatus.SUCCESS, activity: transactionMock.tx, direction: transactionMock.direction diff --git a/apps/browser-extension-wallet/src/stores/slices/__tests__/wallet-info-slice.test.ts b/apps/browser-extension-wallet/src/stores/slices/__tests__/wallet-info-slice.test.ts index af83edc6d..01b28f34a 100644 --- a/apps/browser-extension-wallet/src/stores/slices/__tests__/wallet-info-slice.test.ts +++ b/apps/browser-extension-wallet/src/stores/slices/__tests__/wallet-info-slice.test.ts @@ -61,7 +61,7 @@ describe('Testing wallet info slice', () => { describe('environment names set correctly', () => { let useWalletInfoHook: UseStore; - process.env.AVAILABLE_CHAINS = process.env.AVAILABLE_CHAINS || 'Mainnet,Preprod,Preview'; + process.env.AVAILABLE_CHAINS = process.env.AVAILABLE_CHAINS || 'Mainnet,Preprod,Preview,Sanchonet'; beforeEach(() => { useWalletInfoHook = create(mockWalletInfoStore); diff --git a/apps/browser-extension-wallet/src/stores/slices/activity-detail-slice.ts b/apps/browser-extension-wallet/src/stores/slices/activity-detail-slice.ts index f151ed247..386dd7210 100644 --- a/apps/browser-extension-wallet/src/stores/slices/activity-detail-slice.ts +++ b/apps/browser-extension-wallet/src/stores/slices/activity-detail-slice.ts @@ -1,6 +1,13 @@ /* eslint-disable complexity */ import isEmpty from 'lodash/isEmpty'; -import { ActivityDetailSlice, ZustandHandlers, BlockchainProviderSlice, WalletInfoSlice, SliceCreator } from '../types'; +import { + ActivityDetailSlice, + ZustandHandlers, + BlockchainProviderSlice, + WalletInfoSlice, + SliceCreator, + UISlice +} from '../types'; import { CardanoTxOut, Transaction, ActivityDetail, TransactionActivityDetail } from '../../types'; import { blockTransformer, inputOutputTransformer } from '../../api/transformers'; import { Wallet } from '@lace/cardano'; @@ -9,8 +16,14 @@ import { inspectTxValues } from '@src/utils/tx-inspection'; import { firstValueFrom } from 'rxjs'; import { getAssetsInformation } from '@src/utils/get-assets-information'; import { MAX_POOLS_COUNT } from '@lace/staking'; -import { ActivityStatus, ActivityType } from '@lace/core'; +import { ActivityStatus, DelegationActivityType, TransactionActivityType } from '@lace/core'; +import type { ActivityType } from '@lace/core'; import { formatDate, formatTime } from '@src/utils/format-date'; +import { + certificateTransformer, + governanceProposalsTransformer, + votingProceduresTransformer +} from '@src/views/browser-view/features/activity/helpers/common-tx-transformer'; /** * validates if the transaction is confirmed @@ -21,17 +34,24 @@ const isConfirmedTransaction = (props: Transaction): props is Wallet.Cardano.Hyd /** * returns a list of assets ids that belong to the transaction */ -const getTransactionAssetsId = (outputs: CardanoTxOut[]) => { - const assetIds: Wallet.Cardano.AssetId[] = []; - const assetMaps = outputs.map((output) => output.value.assets); +export const getTransactionAssetsId = ( + outputs: CardanoTxOut[], + mint?: Wallet.Cardano.TokenMap +): Wallet.Cardano.AssetId[] => { + const uniqueAssetIds = new Set(); + // Merge all assets (TokenMaps) from the tx outputs and mint + const assetMaps = outputs.map((output) => output.value.assets) ?? []; + if (mint?.size > 0) assetMaps.push(mint); + + // Extract all unique asset ids from the array of TokenMaps for (const asset of assetMaps) { if (asset) { for (const id of asset.keys()) { - !assetIds.includes(id) && assetIds.push(id); + !uniqueAssetIds.has(id) && uniqueAssetIds.add(id); } } } - return assetIds; + return [...uniqueAssetIds.values()]; }; const transactionMetadataTransformer = ( @@ -44,11 +64,11 @@ const shouldIncludeFee = ( delegationInfo: Wallet.Cardano.StakeDelegationCertificate[] | undefined ) => !( - type === 'delegationRegistration' || + type === DelegationActivityType.delegationRegistration || // Existence of any (new) delegationInfo means that this "de-registration" // activity is accompanied by a "delegation" activity, which carries the fees. // However, fees should be shown if de-registration activity is standalone. - (type === 'delegationDeregistration' && !!delegationInfo?.length) + (type === DelegationActivityType.delegationDeregistration && !!delegationInfo?.length) ); const getPoolInfos = async (poolIds: Wallet.Cardano.PoolId[], stakePoolProvider: Wallet.StakePoolProvider) => { @@ -77,20 +97,21 @@ const buildGetActivityDetail = set, get }: ZustandHandlers< - ActivityDetailSlice & BlockchainProviderSlice & WalletInfoSlice + ActivityDetailSlice & BlockchainProviderSlice & WalletInfoSlice & UISlice >): ActivityDetailSlice['getActivityDetail'] => // eslint-disable-next-line max-statements, sonarjs/cognitive-complexity async ({ coinPrices, fiatCurrency }) => { const { blockchainProvider: { chainHistoryProvider, stakePoolProvider, assetProvider }, inMemoryWallet: wallet, + walletUI: { cardanoCoin }, activityDetail, walletInfo } = get(); set({ fetchingActivityInfo: true }); - if (activityDetail.type === 'rewards') { + if (activityDetail.type === TransactionActivityType.rewards) { const { activity, status, type } = activityDetail; const poolInfos = await getPoolInfos( activity.rewards.map(({ poolId }) => poolId), @@ -172,14 +193,14 @@ const buildGetActivityDetail = const deposit = // since one tx can be split into two (delegation, registration) actions, // ensure only the registration tx carries the deposit - implicitCoin.deposit && type === 'delegationRegistration' + implicitCoin.deposit && type === DelegationActivityType.delegationRegistration ? Wallet.util.lovelacesToAdaString(implicitCoin.deposit.toString()) : undefined; const depositReclaimValue = Wallet.util.calculateDepositReclaim(implicitCoin); const depositReclaim = // since one tx can be split into two (delegation, de-registration) actions, // ensure only the de-registration tx carries the reclaimed deposit - depositReclaimValue && type === 'delegationDeregistration' + depositReclaimValue && type === DelegationActivityType.delegationDeregistration ? Wallet.util.lovelacesToAdaString(depositReclaimValue.toString()) : undefined; const feeInAda = Wallet.util.lovelacesToAdaString(tx.body.fee.toString()); @@ -187,7 +208,7 @@ const buildGetActivityDetail = // Delegation tx additional data (LW-3324) const delegationInfo = tx.body.certificates?.filter( - (certificate) => certificate.__typename === 'StakeDelegationCertificate' + (certificate) => certificate.__typename === Wallet.Cardano.CertificateType.StakeDelegation ) as Wallet.Cardano.StakeDelegationCertificate[]; let transaction: ActivityDetail['activity'] = { @@ -200,10 +221,19 @@ const buildGetActivityDetail = addrOutputs: outputs, metadata: txMetadata, includedUtcDate: blocks?.utcDate, - includedUtcTime: blocks?.utcTime + includedUtcTime: blocks?.utcTime, + // TODO: store the raw data here and transform it later so we always have the raw data when needed.(LW-9570) + votingProcedures: votingProceduresTransformer(tx.body.votingProcedures), + proposalProcedures: governanceProposalsTransformer({ + cardanoCoin, + coinPrices, + fiatCurrency, + proposalProcedures: tx.body.proposalProcedures + }), + certificates: certificateTransformer(cardanoCoin, coinPrices, fiatCurrency, tx.body.certificates) }; - if (type === 'delegation' && delegationInfo) { + if (type === DelegationActivityType.delegation && delegationInfo) { const pools = await getPoolInfos( delegationInfo.map(({ poolId }) => poolId), stakePoolProvider @@ -231,7 +261,7 @@ const buildGetActivityDetail = * has all transactions search related actions and states */ export const activityDetailSlice: SliceCreator< - ActivityDetailSlice & BlockchainProviderSlice & WalletInfoSlice, + ActivityDetailSlice & BlockchainProviderSlice & WalletInfoSlice & UISlice, ActivityDetailSlice > = ({ set, get }) => ({ activityDetail: undefined, @@ -240,6 +270,6 @@ export const activityDetailSlice: SliceCreator< setTransactionActivityDetail: ({ activity, direction, status, type }) => set({ activityDetail: { activity, direction, status, type } }), setRewardsActivityDetail: ({ activity }) => - set({ activityDetail: { activity, status: ActivityStatus.SPENDABLE, type: 'rewards' } }), + set({ activityDetail: { activity, status: ActivityStatus.SPENDABLE, type: TransactionActivityType.rewards } }), resetActivityState: () => set({ activityDetail: undefined, fetchingActivityInfo: false }) }); diff --git a/apps/browser-extension-wallet/src/stores/slices/wallet-activities-slice.ts b/apps/browser-extension-wallet/src/stores/slices/wallet-activities-slice.ts index b6c3141db..2aeaa34f6 100644 --- a/apps/browser-extension-wallet/src/stores/slices/wallet-activities-slice.ts +++ b/apps/browser-extension-wallet/src/stores/slices/wallet-activities-slice.ts @@ -21,6 +21,7 @@ import { ActivityStatus, AssetActivityItemProps, AssetActivityListProps, + DelegationActivityType, TransactionActivityType } from '@lace/core'; import { CurrencyInfo, TxDirections } from '@src/types'; @@ -67,32 +68,22 @@ type MappedActivityListProps = Omit & { items: ExtendedActivityProps[]; }; export type FetchWalletActivitiesReturn = MappedActivityListProps[]; -export type DelegationTransactionType = Extract< - TransactionActivityType, - 'delegation' | 'delegationRegistration' | 'delegationDeregistration' ->; - -const delegationTransactionTypes: ReadonlySet = new Set([ - 'delegation', - 'delegationRegistration', - 'delegationDeregistration' -]); type DelegationActivityItemProps = Omit & { - type: DelegationTransactionType; + type: DelegationActivityType; }; const isDelegationActivity = (activity: ExtendedActivityProps): activity is DelegationActivityItemProps => - delegationTransactionTypes.has(activity.type as DelegationTransactionType); + activity.type in DelegationActivityType; const getDelegationAmount = (activity: DelegationActivityItemProps) => { const fee = new BigNumber(Number.parseFloat(activity.fee)); - if (activity.type === 'delegationRegistration') { + if (activity.type === DelegationActivityType.delegationRegistration) { return fee.plus(activity.deposit); } - if (activity.type === 'delegationDeregistration') { + if (activity.type === DelegationActivityType.delegationDeregistration) { return new BigNumber(activity.depositReclaim).minus(fee); } @@ -334,7 +325,7 @@ const mapWalletActivities = memoize( amount: `${getDelegationAmount(activity)} ${cardanoCoin.symbol}`, fiatAmount: `${getFiatAmount(getDelegationAmount(activity), cardanoFiatPrice)} ${fiatCurrency.code}` }), - ...(activity.type === 'self' && { + ...(activity.type === TransactionActivityType.self && { amount: `${activity.fee} ${cardanoCoin.symbol}`, fiatAmount: cardanoFiatPrice ? `${getFiatAmount(new BigNumber(activity.fee), cardanoFiatPrice)} ${fiatCurrency.code}` diff --git a/apps/browser-extension-wallet/src/stores/types.ts b/apps/browser-extension-wallet/src/stores/types.ts index c902f9f99..af0c10412 100644 --- a/apps/browser-extension-wallet/src/stores/types.ts +++ b/apps/browser-extension-wallet/src/stores/types.ts @@ -1,12 +1,7 @@ import { SetState, State, GetState, StoreApi } from 'zustand'; import { Wallet } from '@lace/cardano'; -import { - AssetActivityListProps, - ActivityStatus, - RewardsActivityType, - TransactionActivityType, - ActivityType -} from '@lace/core'; +import { AssetActivityListProps, ActivityStatus, TransactionActivityType, ActivityType } from '@lace/core'; + import { PriceResult } from '../hooks'; import { NetworkInformation, @@ -148,13 +143,13 @@ export interface ActivityDetailSlice { direction?: TxDirection; } & ( | { - type: RewardsActivityType; + type: TransactionActivityType.rewards; status: ActivityStatus.SPENDABLE; direction?: never; activity: { spendableEpoch: Cardano.EpochNo; spendableDate: Date; rewards: Reward[] }; } | { - type: TransactionActivityType; + type: Exclude; activity: Wallet.Cardano.HydratedTx | Wallet.Cardano.Tx; direction: TxDirection; } @@ -164,7 +159,7 @@ export interface ActivityDetailSlice { activity: Wallet.Cardano.HydratedTx | Wallet.Cardano.Tx; direction: TxDirection; status: ActivityStatus; - type: TransactionActivityType; + type: Exclude; }) => void; setRewardsActivityDetail: (params: { activity: { spendableEpoch: Cardano.EpochNo; spendableDate: Date; rewards: Reward[] }; diff --git a/apps/browser-extension-wallet/src/types/activity-detail.ts b/apps/browser-extension-wallet/src/types/activity-detail.ts index 2339669fe..eb0339f6a 100644 --- a/apps/browser-extension-wallet/src/types/activity-detail.ts +++ b/apps/browser-extension-wallet/src/types/activity-detail.ts @@ -4,7 +4,11 @@ import type { ActivityStatus, RewardsInfo, TransactionActivityType, - RewardsActivityType + ActivityType, + TxDetailsVotingProceduresTitles, + TxDetailsProposalProceduresTitles, + TxDetailsCertificateTitles, + TxDetails } from '@lace/core'; export enum TxDirections { @@ -33,6 +37,9 @@ type TransactionActivity = { addrOutputs?: TxOutputInput[]; metadata?: TransactionMetadataProps['metadata']; pools?: TransactionPool[]; + votingProcedures?: TxDetails[]; + proposalProcedures?: TxDetails[]; + certificates?: TxDetails[]; }; type RewardsActivity = { @@ -58,7 +65,7 @@ type BlocksInfo = { }; export type TransactionActivityDetail = { - type: TransactionActivityType; + type: Exclude; status: ActivityStatus; activity: TransactionActivity; blocks?: BlocksInfo; @@ -66,7 +73,7 @@ export type TransactionActivityDetail = { }; export type RewardsActivityDetail = { - type: RewardsActivityType; + type: TransactionActivityType.rewards; status: ActivityStatus.SPENDABLE; activity: RewardsActivity; }; diff --git a/apps/browser-extension-wallet/src/utils/__tests__/chain.test.ts b/apps/browser-extension-wallet/src/utils/__tests__/chain.test.ts index fdbe1e951..a2fd7e754 100644 --- a/apps/browser-extension-wallet/src/utils/__tests__/chain.test.ts +++ b/apps/browser-extension-wallet/src/utils/__tests__/chain.test.ts @@ -9,7 +9,8 @@ describe('Testing getBaseUrlForChain function', () => { const CARDANO_SERVICES_URLS = { Mainnet: 'Mainnet', Preprod: 'Preprod', - Preview: 'Preview' + Preview: 'Preview', + Sanchonet: 'Sanchonet' }; beforeEach(() => { @@ -22,12 +23,13 @@ describe('Testing getBaseUrlForChain function', () => { }); test('should return proper url for chainName or throw', async () => { process.env.USE_DEV_ENDPOINTS = 'true'; - const AVAILABLE_CHAINS = ['Mainnet', 'Preprod', 'Preview'] as unknown as Wallet.ChainName[]; + const AVAILABLE_CHAINS = ['Mainnet', 'Preprod', 'Preview', 'Sanchonet'] as unknown as Wallet.ChainName[]; jest.spyOn(config, 'config').mockReturnValue({ CARDANO_SERVICES_URLS, AVAILABLE_CHAINS } as config.Config); expect(getBaseUrlForChain('Mainnet')).toBe(CARDANO_SERVICES_URLS.Mainnet); expect(getBaseUrlForChain('Preprod')).toBe(CARDANO_SERVICES_URLS.Preprod); expect(getBaseUrlForChain('Preview')).toBe(CARDANO_SERVICES_URLS.Preview); + expect(getBaseUrlForChain('Sanchonet')).toBe(CARDANO_SERVICES_URLS.Sanchonet); }); test('should throw in case chain is not suported', async () => { diff --git a/apps/browser-extension-wallet/src/utils/__tests__/inspectTxType.test.ts b/apps/browser-extension-wallet/src/utils/__tests__/inspectTxType.test.ts index a5995ebcd..80492d53f 100644 --- a/apps/browser-extension-wallet/src/utils/__tests__/inspectTxType.test.ts +++ b/apps/browser-extension-wallet/src/utils/__tests__/inspectTxType.test.ts @@ -1,12 +1,16 @@ +/* eslint-disable sonarjs/no-identical-functions */ /* eslint-disable unicorn/no-null */ /* eslint-disable no-magic-numbers */ import '@testing-library/jest-dom'; import { inspectTxType, getTxDirection } from '../tx-inspection'; import { buildMockTx } from '../mocks/tx'; +import { mockConwayCertificates } from '../mocks/certificates'; import { Wallet } from '@lace/cardano'; import { TxDirections } from '@src/types'; import { Cardano } from '@cardano-sdk/core'; import { Hash28ByteBase16 } from '@cardano-sdk/crypto'; +import { TransactionActivityType, ActivityType } from '@lace/core'; +import * as Core from '@cardano-sdk/core'; const ADDRESS_1 = Wallet.Cardano.PaymentAddress( 'addr_test1qq585l3hyxgj3nas2v3xymd23vvartfhceme6gv98aaeg9muzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q2g7k3g' ); @@ -38,13 +42,18 @@ const createStubInputResolver = ( }) }); +jest.mock('@cardano-sdk/core', () => ({ + ...jest.requireActual('@cardano-sdk/core'), + createTxInspector: jest.fn(jest.requireActual('@cardano-sdk/core').createTxInspector) +})); + describe('testing tx-inspection utils', () => { describe('Testing getTxDirection function', () => { test('should return proper direction', () => { - expect(getTxDirection({ type: 'incoming' })).toEqual(TxDirections.Incoming); - expect(getTxDirection({ type: 'rewards' })).toEqual(TxDirections.Outgoing); - expect(getTxDirection({ type: 'outgoing' })).toEqual(TxDirections.Outgoing); - expect(getTxDirection({ type: 'self' })).toEqual(TxDirections.Self); + expect(getTxDirection({ type: TransactionActivityType.incoming })).toEqual(TxDirections.Incoming); + expect(getTxDirection({ type: TransactionActivityType.rewards })).toEqual(TxDirections.Outgoing); + expect(getTxDirection({ type: TransactionActivityType.outgoing })).toEqual(TxDirections.Outgoing); + expect(getTxDirection({ type: TransactionActivityType.self })).toEqual(TxDirections.Self); }); }); describe('Testing inspectTxType function', () => { @@ -326,5 +335,177 @@ describe('testing tx-inspection utils', () => { expect(result).toBe('incoming'); }); + + describe('conway era transaction types', () => { + describe('certificates', () => { + const conwayCertificates: { cert: Wallet.Cardano.Certificate; expectedReturn: ActivityType }[] = []; + + for (const [certificateType, certificate] of Object.entries(mockConwayCertificates)) { + conwayCertificates.push({ cert: certificate, expectedReturn: certificateType as ActivityType }); + } + + it.each(conwayCertificates)( + "should return '$expectedReturn' if a certificate of type $cert.__typename exists in the transaction body", + async ({ cert, expectedReturn }) => { + const mockTx = buildMockTx({ + certificates: [cert], + inputs: [ + { + address: ADDRESS_1, + ...DEAFULT_TX_INPUT_INFO + } + ], + outputs: [ + { + address: ADDRESS_2, + ...DEFAULT_OUTPUT_VALUE + }, + { + address: ADDRESS_1, + ...DEFAULT_OUTPUT_VALUE + } + ] + }); + const result = await inspectTxType({ + tx: mockTx, + walletAddresses: [ + { address: ADDRESS_1, rewardAccount: REWARD_ACCOUNT } + ] as Wallet.KeyManagement.GroupedAddress[], + inputResolver: { resolveInput: jest.fn().mockResolvedValue(null) } + }); + expect(result).toEqual(expectedReturn); + } + ); + }); + + describe('governance actions', () => { + it('should return "vote" if votingProcedures are present', async () => { + const createTxInspectorSpy = jest.spyOn(Core, 'createTxInspector').mockReturnValue( + async () => + await ({ + sent: { inputs: [1] }, + delegation: [], + stakeKeyRegistration: [], + stakeKeyDeregistration: [] + } as never) + ); + + const mockTx = buildMockTx({ + inputs: [ + { + address: ADDRESS_1, + ...DEAFULT_TX_INPUT_INFO + } + ], + outputs: [ + { + address: ADDRESS_2, + ...DEFAULT_OUTPUT_VALUE + }, + { + address: ADDRESS_1, + ...DEFAULT_OUTPUT_VALUE + } + ] + }); + const result = await inspectTxType({ + tx: { + ...mockTx, + body: { + ...mockTx.body, + votingProcedures: [ + { + voter: { + __typename: Wallet.Cardano.VoterType.dRepKeyHash, + credential: { + hash: Wallet.Crypto.Hash28ByteBase16( + 'c780b43ca9577ea3f28f1fbd39a4d13c3ad9df6987051f5167815974' + ), + type: Wallet.Cardano.CredentialType.KeyHash + } + }, + votes: [ + { + actionId: { + actionIndex: 1, + id: DEAFULT_TX_INPUT_INFO.txId + }, + votingProcedure: { + vote: 1, + // eslint-disable-next-line unicorn/no-null + anchor: null + } + } + ] + } + ] + } + }, + walletAddresses: [ + { address: ADDRESS_1, rewardAccount: REWARD_ACCOUNT } + ] as Wallet.KeyManagement.GroupedAddress[], + inputResolver: { resolveInput: jest.fn().mockResolvedValue(null) } + }); + + expect(result).toEqual('vote'); + createTxInspectorSpy.mockRestore(); + }); + + it('should return "submitProposal" if proposalProcedures are present', async () => { + const createTxInspectorSpy = jest.spyOn(Core, 'createTxInspector').mockReturnValue( + async () => + await ({ + sent: { inputs: [1] }, + delegation: [], + stakeKeyRegistration: [], + stakeKeyDeregistration: [] + } as never) + ); + const mockTx = buildMockTx({ + inputs: [ + { + address: ADDRESS_1, + ...DEAFULT_TX_INPUT_INFO + } + ], + outputs: [ + { + address: ADDRESS_2, + ...DEFAULT_OUTPUT_VALUE + }, + { + address: ADDRESS_1, + ...DEFAULT_OUTPUT_VALUE + } + ] + }); + const result = await inspectTxType({ + tx: { + ...mockTx, + body: { + ...mockTx.body, + proposalProcedures: [ + { + rewardAccount: REWARD_ACCOUNT, + // eslint-disable-next-line unicorn/no-null + anchor: null, + governanceAction: { + __typename: Wallet.Cardano.GovernanceActionType.parameter_change_action + } as Wallet.Cardano.GovernanceAction, + deposit: BigInt(1) + } + ] + } + }, + walletAddresses: [ + { address: ADDRESS_1, rewardAccount: REWARD_ACCOUNT } + ] as Wallet.KeyManagement.GroupedAddress[], + inputResolver: { resolveInput: jest.fn().mockResolvedValue(null) } + }); + expect(result).toEqual('ParameterChangeAction'); + createTxInspectorSpy.mockRestore(); + }); + }); + }); }); }); diff --git a/apps/browser-extension-wallet/src/utils/chain.ts b/apps/browser-extension-wallet/src/utils/chain.ts index 87d68ea4f..f8077af81 100644 --- a/apps/browser-extension-wallet/src/utils/chain.ts +++ b/apps/browser-extension-wallet/src/utils/chain.ts @@ -14,6 +14,9 @@ export const getBaseUrlForChain = (chainName: Wallet.ChainName): string => { case 'Preview': url = CARDANO_SERVICES_URLS.Preview; break; + case 'Sanchonet': + url = CARDANO_SERVICES_URLS.Sanchonet; + break; default: throw new Error('Incorrect chain supplied'); } diff --git a/apps/browser-extension-wallet/src/utils/get-chain-name.ts b/apps/browser-extension-wallet/src/utils/get-chain-name.ts index dea3b94f4..8b6c1dc8f 100644 --- a/apps/browser-extension-wallet/src/utils/get-chain-name.ts +++ b/apps/browser-extension-wallet/src/utils/get-chain-name.ts @@ -8,6 +8,8 @@ export const getChainName = (chainId: Wallet.Cardano.ChainId): Wallet.ChainName return 'Preprod'; case Wallet.Cardano.ChainIds.Preview.networkMagic: return 'Preview'; + case Wallet.Cardano.ChainIds.Sanchonet.networkMagic: + return 'Sanchonet'; } throw new Error('Chain name is not in known ChainIds'); diff --git a/apps/browser-extension-wallet/src/utils/mocks/certificates.ts b/apps/browser-extension-wallet/src/utils/mocks/certificates.ts new file mode 100644 index 000000000..a039f264b --- /dev/null +++ b/apps/browser-extension-wallet/src/utils/mocks/certificates.ts @@ -0,0 +1,73 @@ +/* eslint-disable unicorn/no-null, no-magic-numbers */ +import { Wallet } from '@lace/cardano'; +import { ActivityType, ConwayEraCertificatesTypes } from '@lace/core'; + +const DEFAULT_DEPOSIT = BigInt(10_000); +const POOL_ID = Wallet.Cardano.PoolId('pool185g59xpqzt7gf0ljr8v8f3akl95qnmardf2f8auwr3ffx7atjj5'); +const CREDENTIAL = { + type: Wallet.Cardano.CredentialType.KeyHash, + hash: Wallet.Crypto.Hash28ByteBase16('0d94e174732ef9aae73f395ab44507bfa983d65023c11a951f0c32e4') +}; + +export const mockConwayCertificates: Partial< + { + [Type in ActivityType]: Wallet.Cardano.Certificate; + } +> = { + [ConwayEraCertificatesTypes.RegisterDelegateRepresentative]: { + __typename: Wallet.Cardano.CertificateType.RegisterDelegateRepresentative, + deposit: DEFAULT_DEPOSIT, + dRepCredential: CREDENTIAL, + anchor: null + }, + [ConwayEraCertificatesTypes.UnregisterDelegateRepresentative]: { + __typename: Wallet.Cardano.CertificateType.UnregisterDelegateRepresentative, + deposit: DEFAULT_DEPOSIT, + dRepCredential: CREDENTIAL + }, + [ConwayEraCertificatesTypes.UpdateDelegateRepresentative]: { + __typename: Wallet.Cardano.CertificateType.UpdateDelegateRepresentative, + dRepCredential: CREDENTIAL, + anchor: null + }, + [ConwayEraCertificatesTypes.VoteDelegation]: { + __typename: Wallet.Cardano.CertificateType.VoteDelegation, + dRep: CREDENTIAL, + stakeCredential: CREDENTIAL + }, + [ConwayEraCertificatesTypes.StakeVoteDelegation]: { + __typename: Wallet.Cardano.CertificateType.StakeVoteDelegation, + stakeCredential: CREDENTIAL, + dRep: CREDENTIAL, + poolId: POOL_ID + }, + [ConwayEraCertificatesTypes.VoteRegistrationDelegation]: { + __typename: Wallet.Cardano.CertificateType.VoteRegistrationDelegation, + stakeCredential: CREDENTIAL, + deposit: DEFAULT_DEPOSIT, + dRep: CREDENTIAL + }, + [ConwayEraCertificatesTypes.StakeVoteRegistrationDelegation]: { + __typename: Wallet.Cardano.CertificateType.StakeVoteRegistrationDelegation, + poolId: POOL_ID, + deposit: DEFAULT_DEPOSIT, + dRep: CREDENTIAL, + stakeCredential: CREDENTIAL + }, + [ConwayEraCertificatesTypes.StakeRegistrationDelegation]: { + __typename: Wallet.Cardano.CertificateType.StakeRegistrationDelegation, + stakeCredential: CREDENTIAL, + poolId: POOL_ID, + deposit: DEFAULT_DEPOSIT + }, + [ConwayEraCertificatesTypes.AuthorizeCommitteeHot]: { + __typename: Wallet.Cardano.CertificateType.AuthorizeCommitteeHot, + coldCredential: CREDENTIAL, + hotCredential: CREDENTIAL + }, + [ConwayEraCertificatesTypes.ResignCommitteeCold]: { + __typename: Wallet.Cardano.CertificateType.ResignCommitteeCold, + coldCredential: CREDENTIAL, + anchor: null + } +}; diff --git a/apps/browser-extension-wallet/src/utils/mocks/governance.ts b/apps/browser-extension-wallet/src/utils/mocks/governance.ts new file mode 100644 index 000000000..d5b25abfd --- /dev/null +++ b/apps/browser-extension-wallet/src/utils/mocks/governance.ts @@ -0,0 +1,62 @@ +import { Wallet } from '@lace/cardano'; + +const VOTER_CREDENTIAL = { + type: Wallet.Cardano.CredentialType.KeyHash, + hash: Wallet.Crypto.Hash28ByteBase16('0d94e174732ef9aae73f395ab44507bfa983d65023c11a951f0c32e4') +}; + +export const mockVotingProcedures: Wallet.Cardano.VotingProcedures = [ + { + voter: { + __typename: Wallet.Cardano.VoterType.dRepKeyHash, + credential: { + hash: Wallet.Crypto.Hash28ByteBase16('c780b43ca9577ea3f28f1fbd39a4d13c3ad9df6987051f5167815974'), + type: Wallet.Cardano.CredentialType.KeyHash + } + }, + votes: [ + { + actionId: { + actionIndex: 1, + id: Wallet.Cardano.TransactionId('bb217abaca60fc0ca68c1555eca6a96d2478547818ae76ce6836133f3cc546e0') + }, + votingProcedure: { + // eslint-disable-next-line unicorn/no-null + anchor: null, + vote: Wallet.Cardano.Vote.yes + } + } + ] + }, + { + voter: { + __typename: Wallet.Cardano.VoterType.dRepKeyHash, + credential: VOTER_CREDENTIAL + } as Wallet.Cardano.DrepKeyHashVoter, + votes: [ + // multiple voters + { + actionId: { + actionIndex: 1, + id: Wallet.Cardano.TransactionId('bb217abaca60fc0ca68c1555eca6a96d2478547818ae76ce6836133f3cc546e0') + }, + votingProcedure: { + // eslint-disable-next-line unicorn/no-null + anchor: null, + vote: Wallet.Cardano.Vote.abstain + } + }, + { + actionId: { + actionIndex: 1, + id: Wallet.Cardano.TransactionId('bb217abaca60fc0ca68c1555eca6a96d2478547818ae76ce6836133f3cc546e0') + }, + votingProcedure: { + // eslint-disable-next-line unicorn/no-null + anchor: null, + vote: Wallet.Cardano.Vote.no + } + } + ] + } +]; diff --git a/apps/browser-extension-wallet/src/utils/mocks/raw-transactions.ts b/apps/browser-extension-wallet/src/utils/mocks/raw-transactions.ts new file mode 100644 index 000000000..31ce46f3f --- /dev/null +++ b/apps/browser-extension-wallet/src/utils/mocks/raw-transactions.ts @@ -0,0 +1,81 @@ +/* eslint-disable no-magic-numbers */ +import { Wallet } from '@lace/cardano'; +import { ConwayEraCertificatesTypes } from '@lace/core'; +import { mockVotingProcedures } from './governance'; +import { mockConwayCertificates } from './certificates'; + +export const body: Wallet.Cardano.HydratedTxBody = { + fee: BigInt('168273'), + outputs: [ + { + address: Wallet.Cardano.PaymentAddress( + 'addr_test1qqplclydk8yhxl66ku2q2k869xkzxjtadumefazhcg2teewydv5nffw36jhxyf27hqldy5nwu9mwvrly047f9tqlru5st9mpv2' + ), + value: { + coins: BigInt('3000000'), + assets: new Map([ + [Wallet.Cardano.AssetId('6b8d07d69639e9413dd637a1a815a7323c69c86abbafb66dbfdb1aa7'), BigInt('3000000')] + ]) as Wallet.Cardano.TokenMap + } + } + ], + inputs: [ + { + index: 1, + txId: Wallet.Cardano.TransactionId('e6eb1c8c806ae7f4d9fe148e9c23853607ffba692ef0a464688911ad3374a932'), + address: Wallet.Cardano.PaymentAddress( + 'addr_test1qqplclydk8yhxl66ku2q2k869xkzxjtadumefazhcg2teewydv5nffw36jhxyf27hqldy5nwu9mwvrly047f9tqlru5st9mpv2' + ) + } + ] +}; + +export const partialBlockHeader: Wallet.Cardano.PartialBlockHeader = { + blockNo: Wallet.Cardano.BlockNo(500), + hash: Wallet.Cardano.BlockId('96fbe9b0d4930626fc87ea7f1b6360035e9b8a714e9514f1b836190e95edd59e'), + slot: Wallet.Cardano.Slot(3000) +}; + +export const tx: Wallet.Cardano.HydratedTx = { + id: Wallet.Cardano.TransactionId('e6eb1c8c806ae7f4d9fe148e9c23853607ffba692ef0a464688911ad3374a932'), + index: 0, + blockHeader: partialBlockHeader, + body, + txSize: 297, + inputSource: Wallet.Cardano.InputSource.inputs, + witness: { + signatures: new Map() + } +}; + +export const voteTx: Wallet.Cardano.HydratedTx = { + ...tx, + body: { + ...tx.body, + votingProcedures: mockVotingProcedures + } +}; + +export const drepRegistrationTx: Wallet.Cardano.HydratedTx = { + ...tx, + body: { + ...tx.body, + certificates: [mockConwayCertificates[ConwayEraCertificatesTypes.RegisterDelegateRepresentative]] + } +}; + +export const drepRetirementTx: Wallet.Cardano.HydratedTx = { + ...tx, + body: { + ...tx.body, + certificates: [mockConwayCertificates[ConwayEraCertificatesTypes.UnregisterDelegateRepresentative]] + } +}; + +export const voteDelegationTx: Wallet.Cardano.HydratedTx = { + ...tx, + body: { + ...tx.body, + certificates: [mockConwayCertificates[ConwayEraCertificatesTypes.VoteDelegation]] + } +}; diff --git a/apps/browser-extension-wallet/src/utils/mocks/test-helpers.tsx b/apps/browser-extension-wallet/src/utils/mocks/test-helpers.tsx index 460ddb480..a8deeabe9 100644 --- a/apps/browser-extension-wallet/src/utils/mocks/test-helpers.tsx +++ b/apps/browser-extension-wallet/src/utils/mocks/test-helpers.tsx @@ -43,7 +43,8 @@ export const mockKeyAgentDataTestnet: Wallet.KeyManagement.SerializableKeyAgentD export const mockKeyAgentsByChain = { Mainnet: { keyAgentData: { ...mockKeyAgentDataTestnet, chainId: Wallet.Cardano.ChainIds.Mainnet } }, Preprod: { keyAgentData: { ...mockKeyAgentDataTestnet, chainId: Wallet.Cardano.ChainIds.Preprod } }, - Preview: { keyAgentData: { ...mockKeyAgentDataTestnet, chainId: Wallet.Cardano.ChainIds.Preview } } + Preview: { keyAgentData: { ...mockKeyAgentDataTestnet, chainId: Wallet.Cardano.ChainIds.Preview } }, + Sanchonet: { keyAgentData: { ...mockKeyAgentDataTestnet, chainId: Wallet.Cardano.ChainIds.Sanchonet } } }; export const mockWalletState: ObservableWalletState = { diff --git a/apps/browser-extension-wallet/src/utils/mocks/tx.ts b/apps/browser-extension-wallet/src/utils/mocks/tx.ts index 96c2a6264..8ea60d13c 100644 --- a/apps/browser-extension-wallet/src/utils/mocks/tx.ts +++ b/apps/browser-extension-wallet/src/utils/mocks/tx.ts @@ -18,6 +18,9 @@ export const buildMockTx = ( } = {} ): Wallet.Cardano.HydratedTx => ({ + auxiliaryData: { + blob: new Map([[BigInt(1), 'metadataMock']]) + }, blockHeader: { blockNo: Wallet.Cardano.BlockNo(200), hash: Wallet.Cardano.BlockId('0dbe461fb5f981c0d01615332b8666340eb1a692b3034f46bcb5f5ea4172b2ed'), @@ -26,6 +29,9 @@ export const buildMockTx = ( body: { certificates: args.certificates, fee: BigInt(170_000), + mint: new Map([ + [Wallet.Cardano.AssetId('659f2917fb63f12b33667463ee575eeac1845bbc736b9c0bbc40ba8254534c41'), BigInt(3)] + ]), inputs: args.inputs ?? [ { address: sendingAddress, diff --git a/apps/browser-extension-wallet/src/utils/tx-inspection.ts b/apps/browser-extension-wallet/src/utils/tx-inspection.ts index b17428687..3cdcfec00 100644 --- a/apps/browser-extension-wallet/src/utils/tx-inspection.ts +++ b/apps/browser-extension-wallet/src/utils/tx-inspection.ts @@ -9,9 +9,18 @@ import { totalAddressOutputsValueInspector } from '@cardano-sdk/core'; import { Wallet } from '@lace/cardano'; -import { ActivityType, TransactionActivityType } from '@lace/core'; +import { + ActivityType, + DelegationActivityType, + TransactionActivityType, + ConwayEraGovernanceActions, + ConwayEraCertificatesTypes, + Cip1694GovernanceActivityType +} from '@lace/core'; import { TxDirection, TxDirections } from '@src/types'; +const { CertificateType, GovernanceActionType, Vote, VoterType } = Wallet.Cardano; + const hasWalletStakeAddress = ( withdrawals: Wallet.Cardano.HydratedTx['body']['withdrawals'], stakeAddress: Wallet.Cardano.RewardAccount @@ -23,17 +32,100 @@ interface TxTypeProps { export const getTxDirection = ({ type }: TxTypeProps): TxDirections => { switch (type) { - case 'incoming': + case TransactionActivityType.incoming: return TxDirections.Incoming; - case 'rewards': - return TxDirections.Outgoing; - case 'outgoing': + case ConwayEraGovernanceActions.vote: + case TransactionActivityType.rewards: + case TransactionActivityType.outgoing: return TxDirections.Outgoing; - case 'self': + case TransactionActivityType.self: return TxDirections.Self; } }; +const governanceCertificateInspection = ( + certificates: Wallet.Cardano.Certificate[] +): ConwayEraCertificatesTypes | ConwayEraGovernanceActions => { + const signedCertificateTypenames: Wallet.Cardano.CertificateType[] = certificates.reduce( + (acc, cert) => [...acc, cert.__typename], + [] + ); + // Assumes single certificate only, should update + + switch (true) { + case signedCertificateTypenames.includes(CertificateType.RegisterDelegateRepresentative): + // TODO: can we map to Cip30TxType instead? + return ConwayEraCertificatesTypes.RegisterDelegateRepresentative; + case signedCertificateTypenames.includes(CertificateType.UnregisterDelegateRepresentative): + return ConwayEraCertificatesTypes.UnregisterDelegateRepresentative; + case signedCertificateTypenames.includes(CertificateType.UpdateDelegateRepresentative): + return ConwayEraCertificatesTypes.UpdateDelegateRepresentative; + case signedCertificateTypenames.includes(CertificateType.StakeVoteDelegation): + return ConwayEraCertificatesTypes.StakeVoteDelegation; + case signedCertificateTypenames.includes(CertificateType.StakeRegistrationDelegation): + return ConwayEraCertificatesTypes.StakeRegistrationDelegation; + case signedCertificateTypenames.includes(CertificateType.VoteRegistrationDelegation): + return ConwayEraCertificatesTypes.VoteRegistrationDelegation; + case signedCertificateTypenames.includes(CertificateType.VoteDelegation): + return ConwayEraCertificatesTypes.VoteDelegation; + case signedCertificateTypenames.includes(CertificateType.StakeVoteRegistrationDelegation): + return ConwayEraCertificatesTypes.StakeVoteRegistrationDelegation; + case signedCertificateTypenames.includes(CertificateType.AuthorizeCommitteeHot): + return ConwayEraCertificatesTypes.AuthorizeCommitteeHot; + case signedCertificateTypenames.includes(CertificateType.ResignCommitteeCold): + return ConwayEraCertificatesTypes.ResignCommitteeCold; + } +}; + +// Assumes single procedure only +export const cip1694GovernanceActionsInspection = ( + procedure: Wallet.Cardano.ProposalProcedure +): Cip1694GovernanceActivityType => { + switch (procedure.governanceAction.__typename) { + case GovernanceActionType.parameter_change_action: + return Cip1694GovernanceActivityType.ParameterChangeAction; + case GovernanceActionType.hard_fork_initiation_action: + return Cip1694GovernanceActivityType.HardForkInitiationAction; + case GovernanceActionType.treasury_withdrawals_action: + return Cip1694GovernanceActivityType.TreasuryWithdrawalsAction; + case GovernanceActionType.no_confidence: + return Cip1694GovernanceActivityType.NoConfidence; + case GovernanceActionType.update_committee: + return Cip1694GovernanceActivityType.UpdateCommittee; + case GovernanceActionType.new_constitution: + return Cip1694GovernanceActivityType.NewConstitution; + case GovernanceActionType.info_action: + return Cip1694GovernanceActivityType.InfoAction; + } +}; + +const getWalletAccounts = (walletAddresses: Wallet.KeyManagement.GroupedAddress[]) => + walletAddresses.reduce( + (acc, curr) => ({ + paymentAddresses: [...acc.paymentAddresses, curr.address], + rewardAccounts: [...acc.rewardAccounts, curr.rewardAccount] + }), + { paymentAddresses: [], rewardAccounts: [] } + ); + +const txIncludesConwayCertificates = (certificates?: Wallet.Cardano.Certificate[]) => + certificates?.length > 0 + ? certificates.some((certificate) => + Object.values(ConwayEraCertificatesTypes).includes( + certificate.__typename as unknown as ConwayEraCertificatesTypes + ) + ) + : false; + +const isTxWithRewardsWithdrawal = ( + totalWithdrawals: bigint, + walletAddresses: Wallet.KeyManagement.GroupedAddress[], + txWithdrawals?: Wallet.Cardano.Withdrawal[] +) => + totalWithdrawals > BigInt(0) && + txWithdrawals.length > 0 && + walletAddresses.some((addr) => hasWalletStakeAddress(txWithdrawals, addr.rewardAccount)); + const selfTxInspector = (addresses: Wallet.Cardano.PaymentAddress[]) => async (tx: Wallet.Cardano.HydratedTx) => { const notOwnInputs = tx.body.inputs.some((input) => !addresses.includes(input.address)); if (notOwnInputs) return false; @@ -49,14 +141,8 @@ export const inspectTxType = async ({ walletAddresses: Wallet.KeyManagement.GroupedAddress[]; tx: Wallet.Cardano.HydratedTx; inputResolver: Wallet.Cardano.InputResolver; -}): Promise => { - const { paymentAddresses, rewardAccounts } = walletAddresses.reduce( - (acc, curr) => ({ - paymentAddresses: [...acc.paymentAddresses, curr.address], - rewardAccounts: [...acc.rewardAccounts, curr.rewardAccount] - }), - { paymentAddresses: [], rewardAccounts: [] } - ); +}): Promise> => { + const { paymentAddresses, rewardAccounts } = getWalletAccounts(walletAddresses); const inspectionProperties = await createTxInspector({ sent: sentInspector({ @@ -71,26 +157,38 @@ export const inspectTxType = async ({ selfTransaction: selfTxInspector(paymentAddresses) })(tx); - const withRewardsWithdrawal = - inspectionProperties.totalWithdrawals > BigInt(0) && - walletAddresses.some((addr) => hasWalletStakeAddress(tx.body.withdrawals, addr.rewardAccount)); + if (txIncludesConwayCertificates(tx.body.certificates)) { + return governanceCertificateInspection(tx.body.certificates); + } + + const withRewardsWithdrawal = isTxWithRewardsWithdrawal( + inspectionProperties.totalWithdrawals, + walletAddresses, + tx.body.withdrawals + ); if (inspectionProperties.sent.inputs.length > 0 || withRewardsWithdrawal) { switch (true) { case !!inspectionProperties.delegation[0]?.poolId: - return 'delegation'; + return DelegationActivityType.delegation; case inspectionProperties.stakeKeyRegistration.length > 0: - return 'delegationRegistration'; + return DelegationActivityType.delegationRegistration; case inspectionProperties.stakeKeyDeregistration.length > 0: - return 'delegationDeregistration'; + return DelegationActivityType.delegationDeregistration; + // Voting procedures take priority over proposals + // TODO: use proper inspector when available on sdk side (LW-9569) + case tx.body.votingProcedures?.length > 0: + return ConwayEraGovernanceActions.vote; + case tx.body.proposalProcedures?.length > 0: + return cip1694GovernanceActionsInspection(tx.body.proposalProcedures[0]); case inspectionProperties.selfTransaction: - return 'self'; + return TransactionActivityType.self; default: - return 'outgoing'; + return TransactionActivityType.outgoing; } } - return 'incoming'; + return TransactionActivityType.incoming; }; export const inspectTxValues = async ({ @@ -115,3 +213,58 @@ export const inspectTxValues = async ({ return inspectionProperties.totalOutputsValue; }; + +export enum VoterTypeEnum { + CONSTITUTIONAL_COMMITTEE = 'constitutionalCommittee', + SPO = 'spo', + DREP = 'drep' +} + +export const getVoterType = (voterType: Wallet.Cardano.VoterType): VoterTypeEnum => { + switch (voterType) { + case VoterType.ccHotKeyHash: + case VoterType.ccHotScriptHash: + return VoterTypeEnum.CONSTITUTIONAL_COMMITTEE; + case VoterType.stakePoolKeyHash: + return VoterTypeEnum.SPO; + case VoterType.dRepKeyHash: + case VoterType.dRepScriptHash: + return VoterTypeEnum.DREP; + default: + return VoterTypeEnum.DREP; + } +}; + +export enum CredentialType { + KeyHash = 'KeyHash', + ScriptHash = 'ScriptHash' +} + +export const getCredentialType = (credentialType: Wallet.Cardano.CredentialType): CredentialType => { + switch (credentialType) { + case Wallet.Cardano.CredentialType.KeyHash: + return CredentialType.KeyHash; + case Wallet.Cardano.CredentialType.ScriptHash: + return CredentialType.ScriptHash; + default: + return CredentialType.ScriptHash; + } +}; + +export enum VotesEnum { + YES = 'yes', + NO = 'no', + ABSTAIN = 'abstain' +} + +export const getVote = (vote: Wallet.Cardano.Vote): VotesEnum => { + switch (vote) { + case Vote.yes: + return VotesEnum.YES; + case Vote.no: + return VotesEnum.NO; + case Vote.abstain: + default: + return VotesEnum.ABSTAIN; + } +}; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/activity/components/ActivityDetail.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/activity/components/ActivityDetail.tsx index cc713a0dc..e899c48de 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/activity/components/ActivityDetail.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/activity/components/ActivityDetail.tsx @@ -8,10 +8,12 @@ import { ActivityStatus, TxOutputInput, TxSummary, - ActivityType, useTranslate, - RewardsDetails + RewardsDetails, + TransactionActivityType, + DelegationActivityType } from '@lace/core'; +import type { ActivityType } from '@lace/core'; import { PriceResult } from '@hooks'; import { useWalletStore } from '@stores'; import { ActivityDetail as ActivityDetailType } from '@src/types'; @@ -78,12 +80,11 @@ interface ActivityDetailProps { } const getTypeLabel = (type: ActivityType, t: ReturnType['t']) => { - if (type === 'rewards') return t('package.core.activityDetails.rewards'); - if (type === 'delegation') return t('package.core.activityDetails.delegation'); - if (type === 'delegationRegistration') return t('package.core.activityDetails.registration'); - if (type === 'delegationDeregistration') return t('package.core.activityDetails.deregistration'); - if (type === 'incoming') return t('package.core.activityDetails.received'); - return t('package.core.activityDetails.sent'); + if (type === DelegationActivityType.delegationRegistration) return t('package.core.activityDetails.registration'); + if (type === DelegationActivityType.delegationDeregistration) return t('package.core.activityDetails.deregistration'); + if (type === TransactionActivityType.incoming) return t('package.core.activityDetails.received'); + if (type === TransactionActivityType.outgoing) return t('package.core.activityDetails.sent'); + return t(`package.core.activityDetails.${type}`); }; export const ActivityDetail = ({ price }: ActivityDetailProps): ReactElement => { @@ -97,7 +98,7 @@ export const ActivityDetail = ({ price }: ActivityDetailProps): ReactElement => const currentTransactionStatus = useMemo( () => - activityDetail.type !== 'rewards' + activityDetail.type !== TransactionActivityType.rewards ? getCurrentTransactionStatus(walletActivities, activityDetail.activity.id) ?? activityInfo?.status : activityInfo?.status, [activityDetail.activity, activityDetail.type, activityInfo?.status, walletActivities] diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/activity/components/TransactionDetailsProxy.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/activity/components/TransactionDetailsProxy.tsx index f96efee30..fb8919949 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/activity/components/TransactionDetailsProxy.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/activity/components/TransactionDetailsProxy.tsx @@ -1,10 +1,11 @@ import React, { ReactElement, useMemo } from 'react'; -import { ActivityStatus, TransactionDetails } from '@lace/core'; +import { ActivityStatus, DelegationActivityType, TransactionDetails } from '@lace/core'; import { AddressListType, getTransactionData } from './ActivityDetail'; import { useWalletStore } from '@src/stores'; import { useAnalyticsContext, useExternalLinkOpener } from '@providers'; import { useAddressBookContext, withAddressBookContext } from '@src/features/address-book/context'; import type { TransactionActivityDetail, TxDirection } from '@src/types'; +import { TxDirections } from '@src/types'; import { APP_MODE_POPUP } from '@src/utils/constants'; import { config } from '@src/config'; import { PostHogAction } from '@providers/AnalyticsProvider/analyticsTracker'; @@ -34,10 +35,10 @@ export const TransactionDetailsProxy = withAddressBookContext( [CEXPLORER_BASE_URL, CEXPLORER_URL_PATHS.Tx, environmentName] ); const getHeaderDescription = () => { - if (activityInfo.type === 'delegation') return '1 token'; + if (activityInfo.type === DelegationActivityType.delegation) return '1 token'; return ` (${activityInfo?.assetAmount})`; }; - const isIncomingTransaction = direction === 'Incoming'; + const isIncomingTransaction = direction === TxDirections.Incoming; const { addrOutputs, addrInputs, @@ -48,7 +49,10 @@ export const TransactionDetailsProxy = withAddressBookContext( pools, deposit, depositReclaim, - metadata + metadata, + proposalProcedures, + votingProcedures, + certificates } = activityInfo.activity; const txSummary = useMemo( () => @@ -61,10 +65,10 @@ export const TransactionDetailsProxy = withAddressBookContext( [isIncomingTransaction, addrOutputs, addrInputs, walletInfo.addresses] ); - const handleOpenExternalLink = () => { + const handleOpenExternalHashLink = () => { analytics.sendEventToPostHog(PostHogAction.ActivityActivityDetailTransactionHashClick); const externalLink = `${explorerBaseUrl}/${hash}`; - externalLink && status === 'success' && openExternalLink(externalLink); + externalLink && status === ActivityStatus.SUCCESS && openExternalLink(externalLink); }; const addressToNameMap = useMemo( @@ -93,9 +97,13 @@ export const TransactionDetailsProxy = withAddressBookContext( addressToNameMap={addressToNameMap} coinSymbol={cardanoCoin.symbol} isPopupView={isPopupView} - openExternalLink={handleOpenExternalLink} sendAnalyticsInputs={() => analytics.sendEventToPostHog(PostHogAction.ActivityActivityDetailInputsClick)} sendAnalyticsOutputs={() => analytics.sendEventToPostHog(PostHogAction.ActivityActivityDetailOutputsClick)} + proposalProcedures={proposalProcedures} + votingProcedures={votingProcedures} + certificates={certificates} + handleOpenExternalHashLink={handleOpenExternalHashLink} + openExternalLink={openExternalLink} /> ); } diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/activity/helpers/common-tx-transformer.ts b/apps/browser-extension-wallet/src/views/browser-view/features/activity/helpers/common-tx-transformer.ts index de8f15bc6..fa3c62961 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/activity/helpers/common-tx-transformer.ts +++ b/apps/browser-extension-wallet/src/views/browser-view/features/activity/helpers/common-tx-transformer.ts @@ -1,13 +1,31 @@ +/* eslint-disable max-statements */ +/* eslint-disable sonarjs/cognitive-complexity */ +/* eslint-disable complexity */ import BigNumber from 'bignumber.js'; import { Wallet } from '@lace/cardano'; import { CurrencyInfo, TxDirections } from '@types'; -import { inspectTxValues, inspectTxType } from '@src/utils/tx-inspection'; +import { inspectTxValues, inspectTxType, getVoterType, getVote } from '@src/utils/tx-inspection'; import { formatDate, formatTime } from '@src/utils/format-date'; import { getTransactionTotalAmount } from '@src/utils/get-transaction-total-amount'; import type { TransformedActivity, TransformedTransactionActivity } from './types'; -import { ActivityStatus } from '@lace/core'; +import { + ActivityStatus, + DelegationActivityType, + TxDetails, + TxDetailsCertificateTitles, + TxDetailsVotingProceduresTitles, + TxDetailsProposalProceduresTitles, + ConwayEraCertificatesTypes, + TxDetail +} from '@lace/core'; import capitalize from 'lodash/capitalize'; import dayjs from 'dayjs'; +import isEmpty from 'lodash/isEmpty'; +import { PriceResult } from '@hooks'; +import { formatPercentages } from '@lace/common'; +import { depositPaidWithSymbol } from '@src/features/dapp/components/confirm-transaction/utils'; + +const { util, GovernanceActionType, PlutusLanguageVersion, CertificateType } = Wallet.Cardano; export interface TxTransformerInput { tx: Wallet.TxInFlight | Wallet.Cardano.HydratedTx; @@ -42,13 +60,13 @@ const splitDelegationTx = (tx: TransformedActivity): TransformedTransactionActiv return [ { ...tx, - type: 'delegation', + type: DelegationActivityType.delegation, // Deposit already shown in the delegationRegistration deposit: undefined }, { ...tx, - type: 'delegationRegistration', + type: DelegationActivityType.delegationRegistration, // Let registration show just the deposit, // and the other transaction show fee to avoid duplicity fee: '0' @@ -58,13 +76,13 @@ const splitDelegationTx = (tx: TransformedActivity): TransformedTransactionActiv return [ { ...tx, - type: 'delegation', + type: DelegationActivityType.delegation, // Reclaimed deposit already shown in the delegationDeregistration depositReclaim: undefined }, { ...tx, - type: 'delegationDeregistration', + type: DelegationActivityType.delegationDeregistration, // Let de-registration show just the returned deposit, // and the other transaction show fee to avoid duplicity fee: '0' @@ -75,7 +93,7 @@ const splitDelegationTx = (tx: TransformedActivity): TransformedTransactionActiv return [ { ...tx, - type: 'delegation' + type: DelegationActivityType.delegation } ]; }; @@ -116,7 +134,7 @@ export const txTransformer = async ({ status, resolveInput }: TxTransformerInput): Promise => { - const implicitCoin = Wallet.Cardano.util.computeImplicitCoin(protocolParameters, tx.body); + const implicitCoin = util.computeImplicitCoin(protocolParameters, tx.body); const deposit = implicitCoin.deposit ? Wallet.util.lovelacesToAdaString(implicitCoin.deposit.toString()) : undefined; const depositReclaimValue = Wallet.util.calculateDepositReclaim(implicitCoin); const depositReclaim = depositReclaimValue @@ -176,7 +194,7 @@ export const txTransformer = async ({ inputResolver: { resolveInput } }); - if (type === 'delegation') { + if (type === DelegationActivityType.delegation) { return splitDelegationTx(baseTransformedActivity); } @@ -188,3 +206,560 @@ export const txTransformer = async ({ } ]; }; + +const drepMapper = (drep: Wallet.Cardano.DelegateRepresentative) => { + if (Wallet.Cardano.isDRepAlwaysAbstain(drep)) { + return 'alwaysAbstain'; + } else if (Wallet.Cardano.isDRepAlwaysNoConfidence(drep)) { + return 'alwaysNoConfidence'; + } else if (Wallet.Cardano.isDRepCredential(drep)) { + return Wallet.Cardano.DRepID(drep.hash); + } + throw new Error('incorrect drep supplied'); +}; + +export const certificateTransformer = ( + cardanoCoin: Wallet.CoinId, + coinPrices: PriceResult, + fiatCurrency: CurrencyInfo, + certificates?: Wallet.Cardano.Certificate[] +): TxDetails[] => + certificates + ?.filter((certificate) => + Object.values(ConwayEraCertificatesTypes).includes( + certificate.__typename as unknown as ConwayEraCertificatesTypes + ) + ) + .map((conwayEraCertificate) => { + const transformedCertificate: TxDetails = [ + { title: 'certificateType', details: [conwayEraCertificate.__typename] } + ]; + + if ('coldCredential' in conwayEraCertificate) { + transformedCertificate.push({ + title: 'coldCredential', + details: [conwayEraCertificate.coldCredential.hash.toString()] + }); + } + + if ('hotCredential' in conwayEraCertificate) { + transformedCertificate.push({ + title: 'hotCredential', + details: [conwayEraCertificate.hotCredential.hash.toString()] + }); + } + + if ('stakeCredential' in conwayEraCertificate) { + transformedCertificate.push({ + title: 'stakeKey', + details: [conwayEraCertificate.stakeCredential.hash.toString()] + }); + } + + if ('dRepCredential' in conwayEraCertificate) { + transformedCertificate.push({ + title: 'drepId', + details: [conwayEraCertificate.dRepCredential.hash.toString()] + }); + } + + if ('anchor' in conwayEraCertificate && conwayEraCertificate.anchor) { + transformedCertificate.push( + { + title: 'anchorHash', + details: [conwayEraCertificate.anchor.dataHash.toString()] + }, + { + title: 'anchorURL', + details: [conwayEraCertificate.anchor.url] + } + ); + } + + if ('poolId' in conwayEraCertificate) { + transformedCertificate.push({ + title: 'poolId', + details: [conwayEraCertificate.poolId.toString()] + }); + } + + if ('dRep' in conwayEraCertificate) { + transformedCertificate.push({ + title: 'drep', + details: [drepMapper(conwayEraCertificate.dRep)] + }); + } + + if ('deposit' in conwayEraCertificate) { + const depositTitle = + conwayEraCertificate.__typename === CertificateType.UnregisterDelegateRepresentative + ? 'depositReturned' + : 'depositPaid'; + transformedCertificate.push({ + title: depositTitle, + info: `${depositTitle}Info`, + details: [ + [ + Wallet.util.getFormattedAmount({ amount: conwayEraCertificate.deposit.toString(), cardanoCoin }), + `${Wallet.util.convertLovelaceToFiat({ + lovelaces: conwayEraCertificate.deposit.toString(), + fiat: coinPrices?.cardano?.price + })} ${fiatCurrency?.code}` + ] + ] + }); + } + + return transformedCertificate; + }); + +export const votingProceduresTransformer = ( + votingProcedures: Wallet.Cardano.VotingProcedures +): TxDetails[] => { + const votingProcedureDetails: TxDetails[] = []; + + votingProcedures?.forEach((procedure) => + procedure.votes.forEach((vote) => { + const detail: TxDetails = [ + { + title: 'voterType', + details: [getVoterType(procedure.voter.__typename)] + }, + { + title: 'voterCredential', + details: [procedure.voter.credential.hash.toString()] + }, + { title: 'voteTypes', details: [getVote(vote.votingProcedure.vote)] } + ]; + + if (vote.votingProcedure.anchor) { + detail.push( + { title: 'anchorURL', details: [vote.votingProcedure.anchor.url] }, + { title: 'anchorHash', details: [vote.votingProcedure.anchor.dataHash.toString()] } + ); + } + + votingProcedureDetails.push(detail.filter((el: TxDetail) => !isEmpty(el))); + }) + ); + + return votingProcedureDetails; +}; + +export const governanceProposalsTransformer = ({ + cardanoCoin, + coinPrices, + fiatCurrency, + proposalProcedures +}: { + cardanoCoin: Wallet.CoinId; + coinPrices: PriceResult; + fiatCurrency: CurrencyInfo; + proposalProcedures?: Wallet.Cardano.ProposalProcedure[]; +}): TxDetails[] => + proposalProcedures?.map((procedure) => { + const transformedProposal: TxDetails = [ + { title: 'type', details: [procedure.governanceAction.__typename] }, + { + ...(procedure.governanceAction.__typename === GovernanceActionType.parameter_change_action && { + title: 'deposit', + info: 'deposit', + details: [ + [ + Wallet.util.getFormattedAmount({ amount: procedure.deposit.toString(), cardanoCoin }), + `${Wallet.util.convertLovelaceToFiat({ + lovelaces: procedure.deposit.toString(), + fiat: coinPrices?.cardano?.price + })} ${fiatCurrency?.code}` + ] + ] + }) + }, + { title: 'rewardAccount', details: [procedure.anchor.dataHash.toString()] }, + { title: 'anchorURL', details: [procedure.anchor.url] }, + { title: 'anchorHash', details: [procedure.anchor.dataHash.toString()] } + ]; + + if ('governanceActionId' in procedure.governanceAction && procedure.governanceAction.governanceActionId) { + transformedProposal.push( + { + title: 'governanceActionID', + details: [procedure.governanceAction.governanceActionId.id.toString()] + }, + { + title: 'actionIndex', + details: [procedure.governanceAction.governanceActionId.actionIndex.toString()] + } + ); + } + + if ('withdrawals' in procedure.governanceAction) { + procedure.governanceAction.withdrawals.forEach(({ rewardAccount, coin }) => { + transformedProposal.push({ + header: 'withdrawal', + details: [ + { + title: 'withdrawalRewardAccount', + details: [rewardAccount] + }, + { + title: 'withdrawalAmount', + details: [ + Wallet.util.getFormattedAmount({ + amount: coin.toString(), + cardanoCoin + }) + ] + } + ] + }); + }); + } + + if ('constitution' in procedure.governanceAction) { + transformedProposal.push( + { + title: 'constitutionAnchorURL', + details: [procedure.governanceAction.constitution.anchor.url] + }, + { + title: 'constitutionScriptHash', + details: [procedure.governanceAction.constitution.scriptHash.toString()] + } + ); + } + + if ('membersToBeAdded' in procedure.governanceAction) { + const membersToBeAdded: TxDetail[] = []; + procedure.governanceAction.membersToBeAdded.forEach(({ coldCredential, epoch }) => { + membersToBeAdded.push( + { + title: 'coldCredentialHash', + details: [coldCredential.hash] + }, + { + title: 'epoch', + details: [epoch.toString()] + } + ); + }); + + if (membersToBeAdded.length > 0) { + transformedProposal.push({ + header: 'membersToBeAdded', + details: membersToBeAdded + }); + } + } + + if ('membersToBeRemoved' in procedure.governanceAction) { + const membersToBeRemoved: TxDetail[] = []; + procedure.governanceAction.membersToBeRemoved.forEach(({ hash }) => { + membersToBeRemoved.push({ + title: 'hash', + details: [hash.toString()] + }); + }); + + if (membersToBeRemoved.length > 0) { + transformedProposal.push({ + header: 'membersToBeRemoved', + details: membersToBeRemoved + }); + } + } + + if ('newQuorumThreshold' in procedure.governanceAction) { + transformedProposal.push({ + title: 'newQuorumThreshold', + details: [ + `${formatPercentages( + procedure.governanceAction.newQuorumThreshold.numerator / + procedure.governanceAction.newQuorumThreshold.denominator + )}%` + ] + }); + } + + if ('protocolVersion' in procedure.governanceAction) { + transformedProposal.push( + { + title: 'protocolVersionMajor', + details: [procedure.governanceAction.protocolVersion.major.toString()] + }, + { + title: 'protocolVersionMinor', + details: [procedure.governanceAction.protocolVersion.minor.toString()] + } + ); + if (procedure.governanceAction.protocolVersion.patch) { + transformedProposal.push({ + title: 'protocolVersionPatch', + details: [procedure.governanceAction.protocolVersion.patch.toString()] + }); + } + } + + if (procedure.governanceAction.__typename === GovernanceActionType.parameter_change_action) { + const { + protocolParamUpdate: { + maxExecutionUnitsPerTransaction, + maxExecutionUnitsPerBlock, + maxBlockBodySize, + maxTxSize, + maxBlockHeaderSize, + maxValueSize, + maxCollateralInputs, + minFeeCoefficient, + minFeeConstant, + stakeKeyDeposit, + poolDeposit, + monetaryExpansion, + treasuryExpansion, + minPoolCost, + coinsPerUtxoByte, + poolInfluence, + poolRetirementEpochBound, + desiredNumberOfPools, + collateralPercentage, + costModels, + governanceActionValidityPeriod, + governanceActionDeposit, + dRepDeposit, + dRepInactivityPeriod, + minCommitteeSize, + committeeTermLimit, + dRepVotingThresholds, + prices + } + } = procedure.governanceAction; + transformedProposal.push( + { + header: 'maxTxExUnits', + details: [ + { + title: 'memory', + details: [maxExecutionUnitsPerTransaction.memory.toString()] + }, + { + title: 'step', + details: [maxExecutionUnitsPerTransaction.steps.toString()] + } + ] + }, + { + header: 'maxBlockExUnits', + details: [ + { + title: 'memory', + details: [maxExecutionUnitsPerBlock.memory.toString()] + }, + { + title: 'step', + details: [maxExecutionUnitsPerBlock.steps.toString()] + } + ] + }, + { + header: 'networkGroup', + details: [ + { + title: 'maxBBSize', + details: [maxBlockBodySize?.toString()] + }, + { + title: 'maxTxSize', + details: [maxTxSize?.toString()] + }, + { + title: 'maxBHSize', + details: [maxBlockHeaderSize?.toString()] + }, + { + title: 'maxValSize', + details: [maxValueSize?.toString()] + }, + { + title: 'maxCollateralInputs', + details: [maxCollateralInputs?.toString()] + } + ] + }, + { + header: 'economicGroup', + details: [ + { + title: 'minFeeA', + details: [minFeeCoefficient?.toString()] + }, + { + title: 'minFeeB', + details: [minFeeConstant?.toString()] + }, + { + title: 'keyDeposit', + details: [stakeKeyDeposit?.toString()] + }, + { + title: 'poolDeposit', + details: [poolDeposit?.toString()] + }, + { + title: 'rho', + details: [monetaryExpansion?.toString()] + }, + { + title: 'tau', + details: [treasuryExpansion?.toString()] + }, + { + title: 'minPoolCost', + details: [minPoolCost?.toString()] + }, + { + title: 'coinsPerUTxOByte', + details: [coinsPerUtxoByte?.toString()] + } + ] + }, + { + header: 'costModels', + details: [ + { + title: 'PlutusV1', + details: costModels.get(PlutusLanguageVersion.V1).map((model) => model.toString()) + }, + { + title: 'PlutusV2', + details: costModels.get(PlutusLanguageVersion.V2).map((model) => model.toString()) + } + ] + }, + { + header: 'technicalGroup', + details: [ + { + title: 'a0', + details: [poolInfluence?.toString()] + }, + { + title: 'eMax', + details: [poolRetirementEpochBound?.toString()] + }, + { + title: 'nOpt', + details: [desiredNumberOfPools?.toString()] + }, + { + title: 'collateralPercentage', + details: [collateralPercentage?.toString()] + } + ] + }, + { + header: 'prices', + details: [ + { + title: 'memory', + details: [prices.memory.toString()] + }, + { + title: 'step', + details: [prices.steps.toString()] + } + ] + }, + { + header: 'governanceGroup', + details: [ + { + title: 'govActionLifetime', + details: [governanceActionValidityPeriod?.toString()] + }, + { + title: 'govActionDeposit', + details: [depositPaidWithSymbol(BigInt(governanceActionDeposit), cardanoCoin)] + }, + { + title: 'drepDeposit', + details: [depositPaidWithSymbol(BigInt(dRepDeposit), cardanoCoin)] + }, + { + title: 'drepActivity', + details: [dRepInactivityPeriod?.toString()] + }, + { + title: 'ccMinSize', + details: [minCommitteeSize?.toString()] + }, + { + title: 'ccMaxTermLength', + details: [committeeTermLimit?.toString()] + } + ] + } + ); + + if (dRepVotingThresholds) { + const { + motionNoConfidence, + committeeNormal, + commiteeNoConfidence, + hardForkInitiation, + ppNetworkGroup, + ppEconomicGroup, + ppTechnicalGroup, + ppGovernanceGroup, + treasuryWithdrawal, + updateConstitution + } = dRepVotingThresholds; + transformedProposal.push({ + header: 'dRepVotingThresholds', + details: [ + { + title: 'motionNoConfidence', + details: [`${formatPercentages(motionNoConfidence.numerator / motionNoConfidence.denominator)}%`] + }, + { + title: 'committeeNormal', + details: [`${formatPercentages(committeeNormal.numerator / committeeNormal.denominator)}%`] + }, + { + title: 'committeeNoConfidence', + details: [`${formatPercentages(commiteeNoConfidence.numerator / commiteeNoConfidence.denominator)}%`] + }, + { + title: 'updateConstitution', + details: [`${formatPercentages(updateConstitution.numerator / updateConstitution.denominator)}%`] + }, + { + title: 'hardForkInitiation', + details: [`${formatPercentages(hardForkInitiation.numerator / hardForkInitiation.denominator)}%`] + }, + { + title: 'ppNetworkGroup', + details: [`${formatPercentages(ppNetworkGroup.numerator / ppNetworkGroup.denominator)}%`] + }, + { + title: 'ppEconomicGroup', + details: [`${formatPercentages(ppEconomicGroup.numerator / ppEconomicGroup.denominator)}%`] + }, + { + title: 'ppTechnicalGroup', + details: [`${formatPercentages(ppTechnicalGroup.numerator / ppTechnicalGroup.denominator)}%`] + }, + { + title: 'ppGovernanceGroup', + details: [`${formatPercentages(ppGovernanceGroup.numerator / ppGovernanceGroup.denominator)}%`] + }, + { + title: 'treasuryWithdrawal', + details: [`${formatPercentages(treasuryWithdrawal.numerator / treasuryWithdrawal.denominator)}%`] + } + ] + }); + } + } + + return transformedProposal; + }); diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/activity/helpers/filter-outputs-by-tx-direction.ts b/apps/browser-extension-wallet/src/views/browser-view/features/activity/helpers/filter-outputs-by-tx-direction.ts index 079054796..d7747862e 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/activity/helpers/filter-outputs-by-tx-direction.ts +++ b/apps/browser-extension-wallet/src/views/browser-view/features/activity/helpers/filter-outputs-by-tx-direction.ts @@ -1,5 +1,5 @@ import { Wallet } from '@lace/cardano'; -import { CardanoTxOut, TxDirection } from '@src/types'; +import { CardanoTxOut, TxDirection, TxDirections } from '@src/types'; /** * filter outputs based on if is an incoming or outgoing tx @@ -9,7 +9,7 @@ export const filterOutputsByTxDirection = ( direction: TxDirection, destinationAddresses: Wallet.Cardano.PaymentAddress[] ): CardanoTxOut[] => { - const isIncomingTx = direction === 'Incoming'; + const isIncomingTx = direction === TxDirections.Incoming; return outputs.filter((output) => isIncomingTx ? destinationAddresses.includes(output.address) : !destinationAddresses.includes(output.address) ); diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/activity/helpers/reward-history-transformer.ts b/apps/browser-extension-wallet/src/views/browser-view/features/activity/helpers/reward-history-transformer.ts index a08f6c785..0d38e5e99 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/activity/helpers/reward-history-transformer.ts +++ b/apps/browser-extension-wallet/src/views/browser-view/features/activity/helpers/reward-history-transformer.ts @@ -4,9 +4,9 @@ import type { TransformedRewardsActivity } from './types'; import dayjs from 'dayjs'; import { formatDate, formatTime } from '@src/utils/format-date'; import BigNumber from 'bignumber.js'; -import type { CurrencyInfo } from '@src/types'; +import { TxDirections, CurrencyInfo } from '@src/types'; import type { Reward } from '@cardano-sdk/core'; -import { ActivityStatus } from '@lace/core'; +import { ActivityStatus, TransactionActivityType } from '@lace/core'; interface RewardHistoryTransformerInput { rewards: Reward[]; // TODO this supposes rewards grouped by epoch which is a bit fragile @@ -34,8 +34,8 @@ export const rewardHistoryTransformer = ({ const totalRewardsAmount = Wallet.BigIntMath.sum(rewards.map(({ rewards: _rewards }) => _rewards)); return { - type: 'rewards', - direction: 'Incoming', + type: TransactionActivityType.rewards, + direction: TxDirections.Incoming, amount: Wallet.util.getFormattedAmount({ amount: totalRewardsAmount.toString(), cardanoCoin }), fiatAmount: getFormattedFiatAmount({ amount: new BigNumber(totalRewardsAmount.toString()), diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/activity/helpers/types.ts b/apps/browser-extension-wallet/src/views/browser-view/features/activity/helpers/types.ts index 6a08ada9e..7ad6c518f 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/activity/helpers/types.ts +++ b/apps/browser-extension-wallet/src/views/browser-view/features/activity/helpers/types.ts @@ -1,10 +1,5 @@ -import type { - ActivityAssetProp, - ActivityStatus, - ActivityType, - RewardsActivityType, - TransactionActivityType -} from '@lace/core'; +import type { ActivityAssetProp, ActivityStatus, ActivityType, TransactionActivityType } from '@lace/core'; +import { TxDirection } from '@src/types'; export type TransformedActivity = { id?: string; @@ -24,7 +19,7 @@ export type TransformedActivity = { */ fiatAmount: string; /** - * Activity status: `sending` | `success` | 'error + * Activity status: `sending` | `success` | 'error' | 'spendable' */ status?: ActivityStatus; /** @@ -45,13 +40,15 @@ export type TransformedActivity = { * Direction: 'Incoming' | 'Outgoing' | 'Self' * TODO: Create a separate package for common types across apps/packages */ - direction?: 'Incoming' | 'Outgoing' | 'Self'; + direction?: TxDirection; /** * assets details */ assets?: ActivityAssetProp[]; }; -export type TransformedTransactionActivity = TransformedActivity & { type: TransactionActivityType }; +export type TransformedTransactionActivity = TransformedActivity & { + type: Exclude; +}; -export type TransformedRewardsActivity = TransformedActivity & { type: RewardsActivityType }; +export type TransformedRewardsActivity = TransformedActivity & { type: TransactionActivityType.rewards }; diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/util.ts b/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/util.ts index 68040654f..568dfb3f5 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/util.ts +++ b/apps/browser-extension-wallet/src/views/browser-view/features/send-transaction/components/Form/util.ts @@ -19,7 +19,10 @@ export const formatAdaAllocation = ({ cardanoCoin: Wallet.CoinId; fiatCurrency: CurrencyInfo; }): ResultFormatAdaAllocation => ({ - adaAmount: `${Wallet.util.lovelacesToAdaString(missingCoins)} ${cardanoCoin.symbol}`, + adaAmount: Wallet.util.getFormattedAmount({ + amount: missingCoins, + cardanoCoin + }), fiatAmount: `$${Wallet.util.convertLovelaceToFiat({ lovelaces: missingCoins ?? '0', fiat diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/settings/components/Collateral/send/CollateralStepSend.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/settings/components/Collateral/send/CollateralStepSend.tsx index 85af5001e..56a6dfb01 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/settings/components/Collateral/send/CollateralStepSend.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/settings/components/Collateral/send/CollateralStepSend.tsx @@ -91,7 +91,10 @@ export const CollateralStepSend = ({ })}
{renderAmountInfo( - `${Wallet.util.lovelacesToAdaString(txFee.toString())} ${cardanoCoin.symbol}`, + Wallet.util.getFormattedAmount({ + amount: txFee.toString(), + cardanoCoin + }), `${Wallet.util.convertAdaToFiat({ ada: Wallet.util.lovelacesToAdaString(txFee.toString()), fiat: priceResult?.cardano?.price || 0 diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/settings/components/NetworkChoice.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/settings/components/NetworkChoice.tsx index 288e5e6d4..6f73351f8 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/settings/components/NetworkChoice.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/settings/components/NetworkChoice.tsx @@ -17,23 +17,27 @@ const { AVAILABLE_CHAINS } = config(); type networkEventSettings = | PostHogAction.SettingsNetworkPreviewClick | PostHogAction.SettingsNetworkPreprodClick - | PostHogAction.SettingsNetworkMainnetClick; + | PostHogAction.SettingsNetworkMainnetClick + | PostHogAction.SettingsNetworkSanchonetClick; type networkEventUserWalletProfile = | PostHogAction.UserWalletProfileNetworkPreviewClick | PostHogAction.UserWalletProfileNetworkPreprodClick - | PostHogAction.UserWalletProfileNetworkMainnetClick; + | PostHogAction.UserWalletProfileNetworkMainnetClick + | PostHogAction.UserWalletProfileNetworkSanchonetClick; -const settingsEventByNetworkName: Partial> = { +const settingsEventByNetworkName: Record = { Mainnet: PostHogAction.SettingsNetworkMainnetClick, Preprod: PostHogAction.SettingsNetworkPreprodClick, - Preview: PostHogAction.SettingsNetworkPreviewClick + Preview: PostHogAction.SettingsNetworkPreviewClick, + Sanchonet: PostHogAction.SettingsNetworkSanchonetClick }; -const walletProfileEventByNetworkName: Partial> = { +const walletProfileEventByNetworkName: Record = { Mainnet: PostHogAction.UserWalletProfileNetworkMainnetClick, Preprod: PostHogAction.UserWalletProfileNetworkPreprodClick, - Preview: PostHogAction.UserWalletProfileNetworkPreviewClick + Preview: PostHogAction.UserWalletProfileNetworkPreviewClick, + Sanchonet: PostHogAction.UserWalletProfileNetworkSanchonetClick }; export const NetworkChoice = ({ section }: { section?: 'settings' | 'wallet-profile' }): React.ReactElement => { @@ -51,6 +55,8 @@ export const NetworkChoice = ({ section }: { section?: 'settings' | 'wallet-prof return t('general.networks.preprod'); case 'Preview': return t('general.networks.preview'); + case 'Sanchonet': + return t('general.networks.sanchonet'); default: return ''; } diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/StakePoolDetails/StakePoolConfirmation.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/StakePoolDetails/StakePoolConfirmation.tsx index 972515bc6..ac4ed66e5 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/StakePoolDetails/StakePoolConfirmation.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/staking/components/StakePoolDetails/StakePoolConfirmation.tsx @@ -157,7 +157,10 @@ export const StakePoolConfirmation = ({ popupView }: StakePoolConfirmationProps) })}
{renderAmountInfo( - `${Wallet.util.lovelacesToAdaString(deposit.toString())} ${cardanoCoin.symbol}`, + Wallet.util.getFormattedAmount({ + amount: deposit.toString(), + cardanoCoin + }), `${Wallet.util.convertAdaToFiat({ ada: Wallet.util.lovelacesToAdaString(deposit.toString()), fiat: priceResult?.cardano?.price || 0 @@ -175,7 +178,10 @@ export const StakePoolConfirmation = ({ popupView }: StakePoolConfirmationProps) })}
{renderAmountInfo( - `${Wallet.util.lovelacesToAdaString(delegationTxFee)} ${cardanoCoin.symbol}`, + Wallet.util.getFormattedAmount({ + amount: delegationTxFee, + cardanoCoin + }), `${Wallet.util.convertAdaToFiat({ ada: Wallet.util.lovelacesToAdaString(delegationTxFee), fiat: priceResult?.cardano?.price || 0 diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/voting/components/CatalystRegistrationFlow.tsx b/apps/browser-extension-wallet/src/views/browser-view/features/voting/components/CatalystRegistrationFlow.tsx index 4230c2406..c3a6d897e 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/voting/components/CatalystRegistrationFlow.tsx +++ b/apps/browser-extension-wallet/src/views/browser-view/features/voting/components/CatalystRegistrationFlow.tsx @@ -32,7 +32,12 @@ export const CatalystRegistrationFlow = ({ onCurrentStepChange }: CatalystRegistrationFlowProps): React.ReactElement => { const [confirmationStage, setConfirmationStage] = useState('unlock'); - const { walletInfo, inMemoryWallet } = useWalletStore(); + const { + walletInfo, + inMemoryWallet, + walletUI: { cardanoCoin } + } = useWalletStore(); + const { t } = useTranslation(); const totalBalance = useObservable(inMemoryWallet.balance.utxo.total$, DEFAULT_WALLET_BALANCE.utxo.total$); @@ -110,7 +115,10 @@ export const CatalystRegistrationFlow = ({ } diff --git a/apps/browser-extension-wallet/test/__mocks__/set-env-vars.js b/apps/browser-extension-wallet/test/__mocks__/set-env-vars.js index 3ea47c192..e5ca3415c 100644 --- a/apps/browser-extension-wallet/test/__mocks__/set-env-vars.js +++ b/apps/browser-extension-wallet/test/__mocks__/set-env-vars.js @@ -1,11 +1,12 @@ process.env.CARDANO_SERVICES_URL_PREVIEW = 'https://preview-prod.com'; process.env.CARDANO_SERVICES_URL_PREPROD = 'https://preprod-prod.com'; process.env.CARDANO_SERVICES_URL_MAINNET = 'https://mainnet-url.com'; -process.env.AVAILABLE_CHAINS = 'Preprod,Preview,Mainnet'; +process.env.CARDANO_SERVICES_URL_SANCHONET = 'https://sanchonet-url.com'; +process.env.AVAILABLE_CHAINS = 'Preprod,Preview,Mainnet,Sanchonet'; process.env.CEXPLORER_URL_MAINNET = 'https://cexplorer.io'; process.env.CEXPLORER_URL_PREVIEW = 'https://preview.cexplorer.io'; process.env.CEXPLORER_URL_PREPROD = 'https://preprod.cexplorer.io'; -process.env.CEXPLORER_URL_TESTNET = 'https://testnet.cexplorer.io'; +process.env.CEXPLORER_URL_SANCHONET = 'https://sanchonet.cexplorer.io'; process.env.USE_HIDE_MY_BALANCE = 'true'; process.env.USE_POSTHOG_ANALYTICS = 'true'; process.env.USE_POSTHOG_ANALYTICS_FOR_OPTED_OUT = 'false'; diff --git a/apps/browser-extension-wallet/webpack-utils.js b/apps/browser-extension-wallet/webpack-utils.js index b1b965ab4..6c1eb49a3 100644 --- a/apps/browser-extension-wallet/webpack-utils.js +++ b/apps/browser-extension-wallet/webpack-utils.js @@ -14,7 +14,7 @@ const transformManifest = (content, mode) => { manifest.content_security_policy.extension_pages = manifest.content_security_policy.extension_pages .replace( '$CARDANO_SERVICES_URLS', - `${process.env.CARDANO_SERVICES_URL_MAINNET} ${process.env.CARDANO_SERVICES_URL_PREPROD} ${process.env.CARDANO_SERVICES_URL_PREVIEW}` + `${process.env.CARDANO_SERVICES_URL_MAINNET} ${process.env.CARDANO_SERVICES_URL_PREPROD} ${process.env.CARDANO_SERVICES_URL_PREVIEW} ${process.env.CARDANO_SERVICES_URL_SANCHONET}` ) .replace( '$ADA_HANDLE_URLS', diff --git a/package.json b/package.json index 0169b351c..a8f1c10df 100644 --- a/package.json +++ b/package.json @@ -162,6 +162,7 @@ "madr-tools": "^1.0.0", "markdownlint-cli": "^0.31.1", "node-gyp": "^9.4.0", + "node-polyfill-webpack-plugin": "^3.0.0", "node-sass": "^7.0.1", "p-retry": "5.1.2", "postcss": "8.3.6", diff --git a/packages/cardano/src/wallet/types.ts b/packages/cardano/src/wallet/types.ts index 30db2be74..21e2dd835 100644 --- a/packages/cardano/src/wallet/types.ts +++ b/packages/cardano/src/wallet/types.ts @@ -28,14 +28,32 @@ export enum TransactionStatus { SPENDABLE = 'spendable' } +export enum Cip30TxType { + Send = 'Send', + Mint = 'Mint', + Burn = 'Burn', + DRepRegistration = 'DRepRegistration', + DRepRetirement = 'DRepRetirement', + DRepUpdate = 'DRepUpdate', + VoteDelegation = 'VoteDelegation', + VotingProcedures = 'VotingProcedures', + VoteRegistrationDelegation = 'VoteRegistrationDelegation', + StakeRegistrationDelegation = 'StakeRegistrationDelegation', + StakeVoteDelegationRegistration = 'StakeVoteDelegationRegistration', + StakeVoteDelegation = 'StakeVoteDelegation', + ProposalProcedures = 'ProposalProcedures' +} + +export type Cip30SignTxOutput = { + coins: string; + recipient: string; + assets?: Cip30SignTxAssetItem[]; +}; + export type Cip30SignTxSummary = { fee: string; - outputs: { - coins: string; - recipient: string; - assets?: Cip30SignTxAssetItem[]; - }[]; - type: 'Send' | 'Mint'; + outputs: Cip30SignTxOutput[]; + type: Cip30TxType; mintedAssets?: Cip30SignTxAssetItem[]; burnedAssets?: Cip30SignTxAssetItem[]; collateral?: string; @@ -49,8 +67,8 @@ export type Cip30SignTxAssetItem = { export enum WalletManagerProviderTypes { CARDANO_SERVICES_PROVIDER = 'cardano-services-provider' } -// Exclude Sanchonet until in main branch -export type ChainName = keyof Omit; + +export type ChainName = keyof typeof Cardano.ChainIds; export interface CreateHardwareWalletArgs { deviceConnection: DeviceConnection; diff --git a/packages/common/src/analytics/types.ts b/packages/common/src/analytics/types.ts index ac17cb584..3bd6ec84d 100644 --- a/packages/common/src/analytics/types.ts +++ b/packages/common/src/analytics/types.ts @@ -143,6 +143,7 @@ export enum PostHogAction { SettingsNetworkPreviewClick = 'settings | network | preview | click', SettingsNetworkPreprodClick = 'settings | network | preprod | click', SettingsNetworkMainnetClick = 'settings | network | mainnet | click', + SettingsNetworkSanchonetClick = 'settings | network | sanchonet | click', SettingsNetworkXClick = 'settings | network | x | click', SettingsAuthorizedDappsClick = 'settings | authorized dapps | click', SettingsAuthorizedDappsTrashBinIconClick = 'settings | authorized dapps | trash bin icon | click', @@ -192,6 +193,7 @@ export enum PostHogAction { UserWalletProfileNetworkPreviewClick = 'user/wallet profile | network | preview | click', UserWalletProfileNetworkPreprodClick = 'user/wallet profile | network | preprod | click', UserWalletProfileNetworkMainnetClick = 'user/wallet profile | network | mainnet | click', + UserWalletProfileNetworkSanchonetClick = 'user/wallet profile | network | sanchonet | click', UserWalletProfileLockWalletClick = 'user/wallet profile | lock wallet | click', // Lace Logo WalletLaceClick = 'wallet | lace | click', diff --git a/packages/core/.storybook/__mocks__/cardano.ts b/packages/core/.storybook/__mocks__/cardano.ts new file mode 100644 index 000000000..6b4b8382d --- /dev/null +++ b/packages/core/.storybook/__mocks__/cardano.ts @@ -0,0 +1,6 @@ +import { Cardano, util } from '@cardano-sdk/core'; + +export const Wallet = { + util, + Cardano +}; diff --git a/packages/core/.storybook/main.js b/packages/core/.storybook/main.js index d670e8c07..50676894f 100644 --- a/packages/core/.storybook/main.js +++ b/packages/core/.storybook/main.js @@ -1,3 +1,7 @@ +const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); +const { NormalModuleReplacementPlugin } = require('webpack'); +const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); + module.exports = { stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], addons: [ @@ -20,6 +24,28 @@ module.exports = { const fileLoaderRule = config.module.rules.find((rule) => rule.test?.test('.svg')); fileLoaderRule.exclude = /\.svg$/; + const jsRuleIndex = config.module.rules.findIndex((rule) => rule.test?.test('.js')); + + // Please refer to the apps/browser-extension-wallet/webpack.common.js + config.module.rules[jsRuleIndex] = { + test: /\.(js|jsx|ts|tsx)$/, + exclude: /node_modules\/(?!(@cardano-sdk)\/).*/, + loader: 'swc-loader', + options: { + jsc: { + parser: { + syntax: 'typescript', + tsx: true + }, + target: 'es2019', + loose: false + } + }, + resolve: { + fullySpecified: false + } + }; + config.module.rules.push({ test: /\.svg$/i, issuer: /\.[jt]sx?$/, @@ -34,6 +60,14 @@ module.exports = { }); config.resolve.extensions.push('.svg'); + (config.resolve.plugins = config.resolve.plugins || []).push( + new TsconfigPathsPlugin({ configFile: 'src/tsconfig.json' }) + ); + + config.plugins.push( + new NormalModuleReplacementPlugin(/@lace\/cardano/, require.resolve('./__mocks__/cardano.ts')), + new NodePolyfillPlugin() + ); return config; }, @@ -41,7 +75,10 @@ module.exports = { builder: 'webpack5', options: { lazyCompilation: true, - fsCache: true + fsCache: true, + builder: { + useSWC: true // This flag is automatically set by Storybook for all new Webpack5 projects (except Angular) in Storybook 7.6 + } } }, features: { diff --git a/packages/core/package.json b/packages/core/package.json index 7d989aeea..fd1a84619 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -58,7 +58,7 @@ "react-dom": "17.0.2", "react-i18next": "11.11.4", "react-infinite-scroll-component": "^6.1.0", - "uuid": "^9.0.1", + "uuid": "^8.3.2", "zxcvbn": "^4.4.2" }, "devDependencies": { @@ -80,9 +80,10 @@ "@storybook/testing-library": "^0.0.13", "@types/babel__preset-env": "^7", "@types/debounce-promise": "^3.1.6", - "@types/uuid": "^9", + "@types/uuid": "^8", "sass": "^1.68.0", "storybook": "^7.4.3", + "tsconfig-paths-webpack-plugin": "3.5.2", "typescript": "^4.3.5" }, "peerDependencies": { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 061156c40..e7bcf308f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -28,5 +28,15 @@ export * from '@ui/components/DappTransaction'; export * from '@ui/components/Send/SendTransactionCost'; export * from '@ui/components/MnemonicWordsAutoComplete'; export * from '@ui/components/AddressCard'; +export * from '@ui/components/ConfirmDRepRegistration'; +export * from '@ui/components/ConfirmDRepRetirement'; +export * from '@ui/components/ConfirmDRepUpdate'; +export * from '@ui/components/ConfirmVoteDelegation'; +export * from '@ui/components/ConfirmStakeVoteDelegation'; +export * from '@ui/components/ConfirmStakeRegistrationDelegation'; +export * from '@ui/components/ConfirmStakeVoteRegistrationDelegation'; +export * from '@ui/components/ConfirmVoteRegistrationDelegation'; +export * from '@ui/components/VotingProcedures'; +export * from '@ui/components/ProposalProcedures'; export * from '@ui/components/SharedWallet'; export * from '@ui/components/Account'; diff --git a/packages/core/src/ui/assets/icons/arrow-diagonal-down-outline.component.svg b/packages/core/src/ui/assets/icons/arrow-diagonal-down-outline.component.svg new file mode 100644 index 000000000..d0611e452 --- /dev/null +++ b/packages/core/src/ui/assets/icons/arrow-diagonal-down-outline.component.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/src/ui/assets/icons/arrow-diagonal-up-outline.component.svg b/packages/core/src/ui/assets/icons/arrow-diagonal-up-outline.component.svg new file mode 100644 index 000000000..cf7c2fe17 --- /dev/null +++ b/packages/core/src/ui/assets/icons/arrow-diagonal-up-outline.component.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/src/ui/assets/icons/badge-check-outline.component.svg b/packages/core/src/ui/assets/icons/badge-check-outline.component.svg new file mode 100644 index 000000000..b548d7cc7 --- /dev/null +++ b/packages/core/src/ui/assets/icons/badge-check-outline.component.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/src/ui/assets/icons/ban-outline.component.svg b/packages/core/src/ui/assets/icons/ban-outline.component.svg new file mode 100644 index 000000000..37df0adae --- /dev/null +++ b/packages/core/src/ui/assets/icons/ban-outline.component.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/src/ui/assets/icons/briefcase-back-icon.component.svg b/packages/core/src/ui/assets/icons/briefcase-back-icon.component.svg new file mode 100644 index 000000000..6223a4a1e --- /dev/null +++ b/packages/core/src/ui/assets/icons/briefcase-back-icon.component.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/core/src/ui/assets/icons/briefcase-outline.component.svg b/packages/core/src/ui/assets/icons/briefcase-outline.component.svg new file mode 100644 index 000000000..c998d3765 --- /dev/null +++ b/packages/core/src/ui/assets/icons/briefcase-outline.component.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/src/ui/assets/icons/clipboard-check-outline.component.svg b/packages/core/src/ui/assets/icons/clipboard-check-outline.component.svg new file mode 100644 index 000000000..c26f611c4 --- /dev/null +++ b/packages/core/src/ui/assets/icons/clipboard-check-outline.component.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/src/ui/assets/icons/clipboard-copy-outline.component.svg b/packages/core/src/ui/assets/icons/clipboard-copy-outline.component.svg new file mode 100644 index 000000000..a81655b75 --- /dev/null +++ b/packages/core/src/ui/assets/icons/clipboard-copy-outline.component.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/src/ui/assets/icons/clipboard-list-outline.component.svg b/packages/core/src/ui/assets/icons/clipboard-list-outline.component.svg new file mode 100644 index 000000000..e5c5f1f0b --- /dev/null +++ b/packages/core/src/ui/assets/icons/clipboard-list-outline.component.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/src/ui/assets/icons/clipboard-x-outline.component.svg b/packages/core/src/ui/assets/icons/clipboard-x-outline.component.svg new file mode 100644 index 000000000..b30137390 --- /dev/null +++ b/packages/core/src/ui/assets/icons/clipboard-x-outline.component.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/core/src/ui/assets/icons/delegation.component.svg b/packages/core/src/ui/assets/icons/delegation.component.svg deleted file mode 100644 index 0d0e38c15..000000000 --- a/packages/core/src/ui/assets/icons/delegation.component.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/core/src/ui/assets/icons/document-add-outline.component.svg b/packages/core/src/ui/assets/icons/document-add-outline.component.svg new file mode 100644 index 000000000..a5dc33e57 --- /dev/null +++ b/packages/core/src/ui/assets/icons/document-add-outline.component.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/src/ui/assets/icons/document-report-outline.component.svg b/packages/core/src/ui/assets/icons/document-report-outline.component.svg new file mode 100644 index 000000000..5fc6ced68 --- /dev/null +++ b/packages/core/src/ui/assets/icons/document-report-outline.component.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/src/ui/assets/icons/document-text-outline.component.svg b/packages/core/src/ui/assets/icons/document-text-outline.component.svg new file mode 100644 index 000000000..42dd9ac66 --- /dev/null +++ b/packages/core/src/ui/assets/icons/document-text-outline.component.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/src/ui/assets/icons/feather-outline.component.svg b/packages/core/src/ui/assets/icons/feather-outline.component.svg new file mode 100644 index 000000000..793654991 --- /dev/null +++ b/packages/core/src/ui/assets/icons/feather-outline.component.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/src/ui/assets/icons/gift-outline.component.svg b/packages/core/src/ui/assets/icons/gift-outline.component.svg new file mode 100644 index 000000000..7c91bda1c --- /dev/null +++ b/packages/core/src/ui/assets/icons/gift-outline.component.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/src/ui/assets/icons/identification-outline.component.svg b/packages/core/src/ui/assets/icons/identification-outline.component.svg new file mode 100644 index 000000000..c358c7964 --- /dev/null +++ b/packages/core/src/ui/assets/icons/identification-outline.component.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/src/ui/assets/icons/incoming.component.svg b/packages/core/src/ui/assets/icons/incoming.component.svg deleted file mode 100644 index 9b30f162c..000000000 --- a/packages/core/src/ui/assets/icons/incoming.component.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/core/src/ui/assets/icons/info-outline.component.svg b/packages/core/src/ui/assets/icons/info-outline.component.svg new file mode 100644 index 000000000..26f63e279 --- /dev/null +++ b/packages/core/src/ui/assets/icons/info-outline.component.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/src/ui/assets/icons/outgoing.component.svg b/packages/core/src/ui/assets/icons/outgoing.component.svg deleted file mode 100644 index d7a81aab4..000000000 --- a/packages/core/src/ui/assets/icons/outgoing.component.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/core/src/ui/assets/icons/receipt-right-outline.component.svg b/packages/core/src/ui/assets/icons/receipt-right-outline.component.svg new file mode 100644 index 000000000..a9a0500e6 --- /dev/null +++ b/packages/core/src/ui/assets/icons/receipt-right-outline.component.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/src/ui/assets/icons/refresh-outline.component.svg b/packages/core/src/ui/assets/icons/refresh-outline.component.svg new file mode 100644 index 000000000..c426d7d57 --- /dev/null +++ b/packages/core/src/ui/assets/icons/refresh-outline.component.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/src/ui/assets/icons/self-transaction.component.svg b/packages/core/src/ui/assets/icons/self-transaction.component.svg deleted file mode 100644 index 71df1564f..000000000 --- a/packages/core/src/ui/assets/icons/self-transaction.component.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/core/src/ui/assets/icons/terminal-outile.component.svg b/packages/core/src/ui/assets/icons/terminal-outile.component.svg new file mode 100644 index 000000000..53f81a73b --- /dev/null +++ b/packages/core/src/ui/assets/icons/terminal-outile.component.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/src/ui/assets/icons/ticket-arrow-right-outline.component.svg b/packages/core/src/ui/assets/icons/ticket-arrow-right-outline.component.svg new file mode 100644 index 000000000..858186e12 --- /dev/null +++ b/packages/core/src/ui/assets/icons/ticket-arrow-right-outline.component.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/src/ui/assets/icons/ticket-forward-icon.component.svg b/packages/core/src/ui/assets/icons/ticket-forward-icon.component.svg new file mode 100644 index 000000000..858186e12 --- /dev/null +++ b/packages/core/src/ui/assets/icons/ticket-forward-icon.component.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/core/src/ui/assets/icons/ticket-outline-icon.component.svg b/packages/core/src/ui/assets/icons/ticket-outline-icon.component.svg new file mode 100644 index 000000000..5a1413c4d --- /dev/null +++ b/packages/core/src/ui/assets/icons/ticket-outline-icon.component.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/core/src/ui/components/Activity/AssetActivityItem.tsx b/packages/core/src/ui/components/Activity/AssetActivityItem.tsx index 75cc0394c..1df59e254 100644 --- a/packages/core/src/ui/components/Activity/AssetActivityItem.tsx +++ b/packages/core/src/ui/components/Activity/AssetActivityItem.tsx @@ -1,3 +1,4 @@ +/* eslint-disable unicorn/consistent-function-scoping */ /* eslint-disable react/no-multi-comp */ import React, { useMemo, useRef, useEffect, useCallback } from 'react'; import debounce from 'lodash/debounce'; @@ -7,11 +8,13 @@ import Icon from '@ant-design/icons'; import { getTextWidth } from '@lace/common'; import { ReactComponent as PendingIcon } from '../../assets/icons/pending.component.svg'; import { ReactComponent as ErrorIcon } from '../../assets/icons/error.component.svg'; -import styles from './AssetActivityItem.module.scss'; import pluralize from 'pluralize'; import { txIconSize } from '@src/ui/utils/icon-size'; import { useTranslate } from '@src/ui/hooks'; -import { ActivityTypeIcon, ActivityType } from '../ActivityDetail'; +import { DelegationActivityType, TransactionActivityType } from '../ActivityDetail/types'; +import type { ActivityType } from '../ActivityDetail/types'; +import styles from './AssetActivityItem.module.scss'; +import { ActivityTypeIcon } from '../ActivityDetail/ActivityTypeIcon'; export type ActivityAssetInfo = { ticker: string }; export type ActivityAssetProp = { id: string; val: string; info?: ActivityAssetInfo }; @@ -62,11 +65,10 @@ export interface AssetActivityItemProps { assets?: ActivityAssetProp[]; } -const DelegationTransactionTypes = new Set(['delegation', 'delegationRegistration', 'delegationDeregistration']); const DELEGATION_ASSET_NUMBER = 1; interface ActivityStatusIconProps { - status: string; + status: ActivityStatus; type: ActivityType; } @@ -78,7 +80,7 @@ const ActivityStatusIcon = ({ status, type }: ActivityStatusIconProps) => { case ActivityStatus.SUCCESS: return ; case ActivityStatus.SPENDABLE: - return ; + return ; case ActivityStatus.PENDING: return ; case ActivityStatus.ERROR: @@ -87,17 +89,12 @@ const ActivityStatusIcon = ({ status, type }: ActivityStatusIconProps) => { } }; -const translationTypes = { - delegation: 'package.core.assetActivityItem.entry.name.delegation', - delegationDeregistration: 'package.core.assetActivityItem.entry.name.delegationDeregistration', - delegationRegistration: 'package.core.assetActivityItem.entry.name.delegationRegistration', - rewards: 'package.core.assetActivityItem.entry.name.rewards', - incoming: 'package.core.assetActivityItem.entry.name.incoming', - outgoing: 'package.core.assetActivityItem.entry.name.outgoing', - self: 'package.core.assetActivityItem.entry.name.self' -}; - -const negativeBalanceStyling: Set = new Set(['outgoing', 'delegationRegistration', 'self', 'delegation']); +const negativeBalanceStyling: Set> = new Set([ + TransactionActivityType.outgoing, + DelegationActivityType.delegationRegistration, + TransactionActivityType.self, + DelegationActivityType.delegation +]); // TODO: Handle pluralization and i18n of assetsNumber when we will have more than Ada. export const AssetActivityItem = ({ @@ -117,7 +114,7 @@ export const AssetActivityItem = ({ const getText = useCallback( (items: number): { text: string; suffix: string } => { - if (DelegationTransactionTypes.has(type) || type === 'self') return { text: amount, suffix: '' }; + if (type in DelegationActivityType || type === TransactionActivityType.self) return { text: amount, suffix: '' }; const assetsIdsText = assets ?.slice(0, items) @@ -162,15 +159,16 @@ export const AssetActivityItem = ({ const isPendingTx = status === ActivityStatus.PENDING; const assetsText = useMemo(() => getText(assetsToShow), [getText, assetsToShow]); - const assetAmountContent = DelegationTransactionTypes.has(type) ? ( -

- {DELEGATION_ASSET_NUMBER} {t('package.core.assetActivityItem.entry.token')} -

- ) : ( -

- {pluralize('package.core.assetActivityItem.entry.token', assetsNumber, true)} -

- ); + const assetAmountContent = + type in DelegationActivityType ? ( +

+ {DELEGATION_ASSET_NUMBER} {t('package.core.assetActivityItem.entry.token')} +

+ ) : ( +

+ {pluralize('package.core.assetActivityItem.entry.token', assetsNumber, true)} +

+ ); const descriptionContent = formattedTimestamp ? (

{formattedTimestamp} @@ -193,9 +191,9 @@ export const AssetActivityItem = ({

- {isPendingTx && type !== 'self' && !DelegationTransactionTypes.has(type) + {isPendingTx && type !== TransactionActivityType.self && !(type in DelegationActivityType) ? t('package.core.assetActivityItem.entry.name.sending') - : t(translationTypes[type])} + : t(`package.core.assetActivityItem.entry.name.${type}`)}
{descriptionContent}
diff --git a/packages/core/src/ui/components/Activity/AssetActivityList.stories.tsx b/packages/core/src/ui/components/Activity/AssetActivityList.stories.tsx new file mode 100644 index 000000000..2de6bd4e5 --- /dev/null +++ b/packages/core/src/ui/components/Activity/AssetActivityList.stories.tsx @@ -0,0 +1,58 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { AssetActivityList } from './AssetActivityList'; +import { ComponentProps } from 'react'; +import { + ConwayEraGovernanceActions, + Cip1694GovernanceActivityType, + ConwayEraCertificatesTypes +} from '../ActivityDetail'; +import { ActivityStatus } from './AssetActivityItem'; + +const meta: Meta = { + title: 'Sanchonet/ActivityHistory/AssetActivityList', + component: AssetActivityList, + parameters: { + layout: 'centered' + } +}; + +export default meta; +type Story = StoryObj; + +const activityTypes = [ + ConwayEraGovernanceActions.vote, + Cip1694GovernanceActivityType.HardForkInitiationAction, + Cip1694GovernanceActivityType.NewConstitution, + Cip1694GovernanceActivityType.NoConfidence, + Cip1694GovernanceActivityType.ParameterChangeAction, + Cip1694GovernanceActivityType.TreasuryWithdrawalsAction, + Cip1694GovernanceActivityType.UpdateCommittee, + ConwayEraCertificatesTypes.UpdateDelegateRepresentative, + Cip1694GovernanceActivityType.InfoAction, + ConwayEraCertificatesTypes.StakeVoteDelegation, + ConwayEraCertificatesTypes.StakeRegistrationDelegation, + ConwayEraCertificatesTypes.VoteRegistrationDelegation, + ConwayEraCertificatesTypes.StakeVoteRegistrationDelegation, + ConwayEraCertificatesTypes.ResignCommitteeCold, + ConwayEraCertificatesTypes.AuthorizeCommitteeHot, + ConwayEraCertificatesTypes.RegisterDelegateRepresentative, + ConwayEraCertificatesTypes.UnregisterDelegateRepresentative, + ConwayEraCertificatesTypes.VoteDelegation +]; + +const data: ComponentProps = { + items: activityTypes.map((type) => ({ + amount: '0.17 ADA', + fiatAmount: '0.04 USD', + type, + status: ActivityStatus.SUCCESS, + formattedTimestamp: '00:00:00' + })) +}; + +export const Overview: Story = { + args: { + ...data + } +}; diff --git a/packages/core/src/ui/components/Activity/__tests__/AssetActivityItem.test.tsx b/packages/core/src/ui/components/Activity/__tests__/AssetActivityItem.test.tsx index 1ced3b779..a8007f593 100644 --- a/packages/core/src/ui/components/Activity/__tests__/AssetActivityItem.test.tsx +++ b/packages/core/src/ui/components/Activity/__tests__/AssetActivityItem.test.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import { render, within, fireEvent, queryByTestId } from '@testing-library/react'; import '@testing-library/jest-dom'; import { AssetActivityItem, AssetActivityItemProps, ActivityStatus } from '../AssetActivityItem'; +import { TransactionActivityType } from '../../ActivityDetail/types'; import { ActivityType } from '../../ActivityDetail'; const assetsAmountTestId = 'asset-amount'; @@ -10,7 +11,7 @@ const assetsAmountTestId = 'asset-amount'; describe('Testing AssetActivityItem component', () => { const props: AssetActivityItemProps = { id: '1', - type: 'outgoing', + type: TransactionActivityType.outgoing, amount: '100', fiatAmount: '300 $', formattedTimestamp: 'Timestamp', @@ -23,7 +24,7 @@ describe('Testing AssetActivityItem component', () => { const assetActivityItemId = 'asset-activity-item'; test('should render an asset activity item with type incoming', async () => { - const { findByTestId } = render(); + const { findByTestId } = render(); const activityItem = await findByTestId(assetActivityItemId); const activityIcon = await findByTestId('asset-icon'); diff --git a/packages/core/src/ui/components/Activity/__tests__/AssetActivityList.test.tsx b/packages/core/src/ui/components/Activity/__tests__/AssetActivityList.test.tsx index bc29dd131..9c7b8e2e7 100644 --- a/packages/core/src/ui/components/Activity/__tests__/AssetActivityList.test.tsx +++ b/packages/core/src/ui/components/Activity/__tests__/AssetActivityList.test.tsx @@ -4,6 +4,7 @@ import { render, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; import { AssetActivityList, AssetActivityListProps } from '../AssetActivityList'; import { ActivityStatus } from '../AssetActivityItem'; +import { TransactionActivityType } from '../../ActivityDetail'; const activityTranslations = { asset: 'asset', @@ -22,7 +23,7 @@ describe('Testing AssetActivityList component', () => { const props: AssetActivityListProps = { onExpand: jest.fn(), items: Array.from({ length: 12 }, () => ({ - type: 'outgoing', + type: TransactionActivityType.outgoing, name: 'Sent', description: 'ADA', date: new Date('2021-01-01'), diff --git a/packages/core/src/ui/components/Activity/__tests__/GroupedAssetActivityList.test.tsx b/packages/core/src/ui/components/Activity/__tests__/GroupedAssetActivityList.test.tsx index 9d94a228f..a3655b908 100644 --- a/packages/core/src/ui/components/Activity/__tests__/GroupedAssetActivityList.test.tsx +++ b/packages/core/src/ui/components/Activity/__tests__/GroupedAssetActivityList.test.tsx @@ -4,9 +4,10 @@ import { render } from '@testing-library/react'; import '@testing-library/jest-dom'; import { GroupedAssetActivityList, GroupedAssetActivityListProps } from '../GroupedAssetActivityList'; import { AssetActivityItemProps } from '../AssetActivityItem'; +import { TransactionActivityType } from '../../ActivityDetail'; const activityItem: AssetActivityItemProps = { - type: 'outgoing', + type: TransactionActivityType.outgoing, amount: '100 ADA', fiatAmount: '300 $', formattedTimestamp: 'FormattedTimestamp', diff --git a/packages/core/src/ui/components/ActivityDetail/ActivityDetailHeader.module.scss b/packages/core/src/ui/components/ActivityDetail/ActivityDetailHeader.module.scss index c9da701fb..2efbf074c 100644 --- a/packages/core/src/ui/components/ActivityDetail/ActivityDetailHeader.module.scss +++ b/packages/core/src/ui/components/ActivityDetail/ActivityDetailHeader.module.scss @@ -7,7 +7,7 @@ gap: size_unit(1); .type { - @include text-heading; + @include text-subHeading-bold; color: var(--text-color-primary, #3d3b39); } .assets { diff --git a/packages/core/src/ui/components/ActivityDetail/ActivityTypeIcon.module.scss b/packages/core/src/ui/components/ActivityDetail/ActivityTypeIcon.module.scss new file mode 100644 index 000000000..8b94e7a1a --- /dev/null +++ b/packages/core/src/ui/components/ActivityDetail/ActivityTypeIcon.module.scss @@ -0,0 +1,34 @@ +@import '../../../../../common/src/ui/styles/abstracts/typography'; +@import '../../styles/theme.scss'; + +.iconWrapper { + width: size_unit(6); + height: size_unit(6); + border-radius: 100%; + background: rgba(#2cb67d, 0.1); + &.governance { + background: rgba(#3489f7, 0.1); + } + + @media (max-width: $breakpoint-popup) { + width: size_unit(5); + height: size_unit(5); + } +} + +.icon { + color: var(--data-green, #2cb67d) !important; + font-size: 22px !important; + + @media (max-width: $breakpoint-popup) { + font-size: 16px !important; + } + + &.governance { + color: var(--text-color-blue, #3489f7) !important; + } + + svg { + fill: none; + } +} diff --git a/packages/core/src/ui/components/ActivityDetail/ActivityTypeIcon.tsx b/packages/core/src/ui/components/ActivityDetail/ActivityTypeIcon.tsx index 8d69396df..6e20b47f2 100644 --- a/packages/core/src/ui/components/ActivityDetail/ActivityTypeIcon.tsx +++ b/packages/core/src/ui/components/ActivityDetail/ActivityTypeIcon.tsx @@ -1,43 +1,101 @@ +/* eslint-disable no-console */ import React from 'react'; -import { ReactComponent as OutgoingIcon } from '../../assets/icons/outgoing.component.svg'; -import { ReactComponent as IncomingIcon } from '../../assets/icons/incoming.component.svg'; -import { ReactComponent as SelfIcon } from '../../assets/icons/self-transaction.component.svg'; -import { ReactComponent as DelegationIcon } from '../../assets/icons/delegation.component.svg'; -import { ReactComponent as RegistrationIcon } from '../../assets/icons/registration.component.svg'; -import { ReactComponent as DeregistrationIcon } from '../../assets/icons/deregistration.component.svg'; -import { ReactComponent as RewardsIcon } from '../../assets/icons/rewards.component.svg'; +import cn from 'classnames'; import Icon, { QuestionOutlined } from '@ant-design/icons'; import { txIconSize } from '@src/ui/utils/icon-size'; +import { ReactComponent as OutgoingIcon } from '../../assets/icons/arrow-diagonal-up-outline.component.svg'; +import { ReactComponent as IncomingIcon } from '../../assets/icons/arrow-diagonal-down-outline.component.svg'; +import { ReactComponent as RefreshOutlinedIcon } from '../../assets/icons/refresh-outline.component.svg'; +import { ReactComponent as DelegationIcon } from '../../assets/icons/receipt-right-outline.component.svg'; +import { ReactComponent as ClipboardCheckOutlineIcon } from '../../assets/icons/clipboard-check-outline.component.svg'; +import { ReactComponent as ClipboardXOutlineComponentIcon } from '../../assets/icons/clipboard-x-outline.component.svg'; +import { ReactComponent as RewardsIcon } from '../../assets/icons/gift-outline.component.svg'; +import { ReactComponent as VoteIcon } from '../../assets/icons/ticket-outline-icon.component.svg'; +import { ReactComponent as HardForkInitiationActionIcon } from '../../assets/icons/terminal-outile.component.svg'; +import { ReactComponent as ParameterChangeActionIcon } from '../../assets/icons/clipboard-list-outline.component.svg'; +import { ReactComponent as TreasuryWithdrawalsActionIcon } from '../../assets/icons/clipboard-copy-outline.component.svg'; +import { ReactComponent as UpdateCommitteeIcon } from '../../assets/icons/document-add-outline.component.svg'; +import { ReactComponent as InfoActionIcon } from '../../assets/icons/info-outline.component.svg'; +import { ReactComponent as StakeVoteDelegationIcon } from '../../assets/icons/document-report-outline.component.svg'; +import { ReactComponent as StakeRegistrationDelegationIcon } from '../../assets/icons/badge-check-outline.component.svg'; +import { ReactComponent as VoteRegistrationDelegationIcon } from '../../assets/icons/identification-outline.component.svg'; +import { ReactComponent as StakeVoteRegistrationDelegationIcon } from '../../assets/icons/document-text-outline.component.svg'; +import { ReactComponent as ResignCommitteeColdIcon } from '../../assets/icons/ban-outline.component.svg'; +import { ReactComponent as AuthorizeCommitteeHotIcon } from '../../assets/icons/feather-outline.component.svg'; +import { ReactComponent as RegisterDelegateRepresentativeIcon } from '../../assets/icons/briefcase-outline.component.svg'; +import { ReactComponent as UnregisterDelegateRepresentativeIcon } from '../../assets/icons/briefcase-back-icon.component.svg'; +import { ReactComponent as VoteDelegationIcon } from '../../assets/icons/ticket-arrow-right-outline.component.svg'; -export type TransactionActivityType = - | 'outgoing' - | 'incoming' - | 'delegation' - | 'delegationRegistration' - | 'delegationDeregistration' - | 'self'; +import { + ActivityType, + ConwayEraCertificatesTypes, + ConwayEraGovernanceActions, + DelegationActivityType, + TransactionActivityType, + Cip1694GovernanceActivityType +} from './types'; -export type RewardsActivityType = 'rewards'; - -export type ActivityType = TransactionActivityType | RewardsActivityType; +import styles from './ActivityTypeIcon.module.scss'; +import { Flex } from '@lace/ui'; export interface ActivityTypeIconProps { type: ActivityType; } const activityTypeIcon: Record>> = { - outgoing: OutgoingIcon, - incoming: IncomingIcon, - self: SelfIcon, - delegation: DelegationIcon, - delegationRegistration: RegistrationIcon, - delegationDeregistration: DeregistrationIcon, - rewards: RewardsIcon + [ConwayEraGovernanceActions.vote]: VoteIcon, + [Cip1694GovernanceActivityType.HardForkInitiationAction]: HardForkInitiationActionIcon, + [Cip1694GovernanceActivityType.NewConstitution]: ClipboardCheckOutlineIcon, + [Cip1694GovernanceActivityType.NoConfidence]: ClipboardXOutlineComponentIcon, + [Cip1694GovernanceActivityType.ParameterChangeAction]: ParameterChangeActionIcon, + [Cip1694GovernanceActivityType.TreasuryWithdrawalsAction]: TreasuryWithdrawalsActionIcon, + [Cip1694GovernanceActivityType.UpdateCommittee]: UpdateCommitteeIcon, + [Cip1694GovernanceActivityType.InfoAction]: InfoActionIcon, + [ConwayEraCertificatesTypes.UpdateDelegateRepresentative]: RefreshOutlinedIcon, + [ConwayEraCertificatesTypes.StakeVoteDelegation]: StakeVoteDelegationIcon, + [ConwayEraCertificatesTypes.StakeRegistrationDelegation]: StakeRegistrationDelegationIcon, + [ConwayEraCertificatesTypes.VoteRegistrationDelegation]: VoteRegistrationDelegationIcon, + [ConwayEraCertificatesTypes.StakeVoteRegistrationDelegation]: StakeVoteRegistrationDelegationIcon, + [ConwayEraCertificatesTypes.ResignCommitteeCold]: ResignCommitteeColdIcon, + [ConwayEraCertificatesTypes.AuthorizeCommitteeHot]: AuthorizeCommitteeHotIcon, + [ConwayEraCertificatesTypes.RegisterDelegateRepresentative]: RegisterDelegateRepresentativeIcon, + [ConwayEraCertificatesTypes.UnregisterDelegateRepresentative]: UnregisterDelegateRepresentativeIcon, + [ConwayEraCertificatesTypes.VoteDelegation]: VoteDelegationIcon, + [TransactionActivityType.rewards]: RewardsIcon, + [TransactionActivityType.incoming]: IncomingIcon, + [TransactionActivityType.outgoing]: OutgoingIcon, + [TransactionActivityType.self]: RefreshOutlinedIcon, + [DelegationActivityType.delegation]: DelegationIcon, + [DelegationActivityType.delegationDeregistration]: ClipboardXOutlineComponentIcon, + [DelegationActivityType.delegationRegistration]: ClipboardCheckOutlineIcon }; export const ActivityTypeIcon = ({ type }: ActivityTypeIconProps): React.ReactElement => { - const icon = type && activityTypeIcon[type]; + const icon = (type && activityTypeIcon[type]) || RefreshOutlinedIcon; const iconStyle = { fontSize: txIconSize() }; - return icon ? : ; + const isGovernanceTx = + Object.values(ConwayEraCertificatesTypes).includes(type as unknown as ConwayEraCertificatesTypes) || + type in ConwayEraGovernanceActions || + type in Cip1694GovernanceActivityType; + + return ( + + {icon ? ( + + ) : ( + + )} + + ); }; diff --git a/packages/core/src/ui/components/ActivityDetail/TransactionDetails.module.scss b/packages/core/src/ui/components/ActivityDetail/TransactionDetails.module.scss index 73fdb278f..5e90ec99b 100644 --- a/packages/core/src/ui/components/ActivityDetail/TransactionDetails.module.scss +++ b/packages/core/src/ui/components/ActivityDetail/TransactionDetails.module.scss @@ -15,7 +15,7 @@ $border-bottom: 1px solid var(--light-mode-light-grey-plus, var(--dark-mode-mid- .header { color: var(--text-color-secondary, #878e9e); - @include text-bodyLarge-medium; + @include text-body-medium; margin-bottom: size_unit(9); @media (max-width: $breakpoint-popup) { @@ -26,10 +26,6 @@ $border-bottom: 1px solid var(--light-mode-light-grey-plus, var(--dark-mode-mid- } } - * { - margin: 0; - } - .block { border-bottom: $border-bottom; @@ -100,13 +96,6 @@ $border-bottom: 1px solid var(--light-mode-light-grey-plus, var(--dark-mode-mid- text-align: right; cursor: pointer; } - &.txLink { - color: var(--text-color-blue, #3489f7); - line-height: 17px; - @media (max-width: $breakpoint-popup) { - flex: 60%; - } - } &.poolId { color: var(--text-color-secondary); font-size: var(--bodySmall); @@ -185,7 +174,7 @@ $border-bottom: 1px solid var(--light-mode-light-grey-plus, var(--dark-mode-mid- } .hashLabel { - max-width: auto; + max-width: initial; @media (max-width: $breakpoint-popup) { max-width: 85px; overflow-wrap: break-word; @@ -279,3 +268,12 @@ $border-bottom: 1px solid var(--light-mode-light-grey-plus, var(--dark-mode-mid- color: var(--light-mode-dark-grey, var(--dark-mode-light-grey)); } } + +.txLink { + cursor: pointer !important; + color: var(--text-color-blue, #3489f7) !important; + line-height: 17px !important; + @media (max-width: $breakpoint-popup) { + flex: 60% !important; + } +} diff --git a/packages/core/src/ui/components/ActivityDetail/TransactionDetails.stories.tsx b/packages/core/src/ui/components/ActivityDetail/TransactionDetails.stories.tsx new file mode 100644 index 000000000..414ddf07b --- /dev/null +++ b/packages/core/src/ui/components/ActivityDetail/TransactionDetails.stories.tsx @@ -0,0 +1,918 @@ +/* eslint-disable no-console */ +/* eslint-disable sonarjs/no-duplicate-string */ +import type { Meta, StoryObj } from '@storybook/react'; + +import { TransactionDetails } from './TransactionDetails'; +import { ComponentProps } from 'react'; +import { ActivityStatus } from '../Activity/AssetActivityItem'; +import { Wallet } from '@lace/cardano'; +import { ConwayEraCertificatesTypes } from './types'; + +const meta: Meta = { + title: 'Sanchonet/ActivityDetail/TransactionDetails', + component: TransactionDetails, + parameters: { + layout: 'centered' + } +}; + +export default meta; +type Story = StoryObj; + +const adaPrice = 0.470_588_235_294_117_6; + +const data: ComponentProps = { + hash: '639a43144dc2c0ead16f2fb753360f4b4f536502dbdb8aa5e424b00abb7534ff', + name: 'Stake Vote Delegation Certificate', + status: ActivityStatus.SUCCESS, + includedDate: '00/00/0000', + includedTime: '00:00:00', + fee: '0.17', + addrInputs: [ + { + amount: '9975.13', + assetList: [], + addr: 'addr_test1qqwhys44c506gsyqnwx3cy6nrhmalajfanqsg0ult5aj4pg8unnmf7l2w7pwz6nej0qj463w7mpytey22ag0h64fs5gs8zw2jg' + } + ], + addrOutputs: [ + { + amount: '1.00', + assetList: [], + addr: 'addr_test1qqwhys44c506gsyqnwx3cy6nrhmalajfanqsg0ult5aj4pg8unnmf7l2w7pwz6nej0qj463w7mpytey22ag0h64fs5gs8zw2jg' + }, + { + amount: '9971.96', + assetList: [], + addr: 'addr_test1qqwhys44c506gsyqnwx3cy6nrhmalajfanqsg0ult5aj4pg8unnmf7l2w7pwz6nej0qj463w7mpytey22ag0h64fs5gs8zw2jg' + } + ], + txSummary: [], + coinSymbol: 'ADA', + addressToNameMap: new Map(), + isPopupView: false, + votingProcedures: [], + amountTransformer: (amount) => `${Number(amount) * adaPrice} USD`, + openExternalLink: (url) => window.open(url, '_blank', 'noopener,noreferrer'), + handleOpenExternalHashLink: () => { + console.log('handle on hash click', '639a43144dc2c0ead16f2fb753360f4b4f536502dbdb8aa5e424b00abb7534ff'); + } +}; + +const stakeVoteDelegationCertificate = [ + { + title: 'certificateType', + details: [ConwayEraCertificatesTypes.StakeVoteDelegation] + }, + { + title: 'stakeKey', + details: ['stake1u929x2y7nnfm797upl7v9rc39pqg87pk5cygvnn2edqmvuq6h48su'] + }, + { + title: 'poolId', + details: ['pool1k0ucs0fau2vhr3p7qh7mnpfgrllwwda7petxjz2gzzaxkyp8f88'] + }, + { + title: 'drep', + details: ['drep1cs234l5mtapethapx8cq97nkpa27xf84phruh5f6jqxa78ymlp4'] + } +]; +export const StakeVoteDelegationCertificate: Story = { + args: { + ...data, + certificates: [stakeVoteDelegationCertificate] + } +}; + +export const StakeVoteDelegationCertificates: Story = { + args: { + ...data, + certificates: [stakeVoteDelegationCertificate, stakeVoteDelegationCertificate] + } +}; + +const stakeRegistrationDelegationCertificate = [ + { + title: 'certificateType', + details: [ConwayEraCertificatesTypes.StakeRegistrationDelegation] + }, + { + title: 'stakeKey', + details: ['stake1u929x2y7nnfm797upl7v9rc39pqg87pk5cygvnn2edqmvuq6h48su'] + }, + { + title: 'poolId', + details: ['pool1k0ucs0fau2vhr3p7qh7mnpfgrllwwda7petxjz2gzzaxkyp8f88'] + }, + { + title: 'depositPaid', + info: 'depositPaidInfo', + details: [['2.00 ADA', '0.08 USD']] + } +]; + +export const StakeRegistrationDelegationCertificate: Story = { + args: { + ...data, + name: 'Stake Registration Delegation Certificate', + certificates: [stakeRegistrationDelegationCertificate] + } +}; + +const voteRegistrationDelegationCertificate = [ + { + title: 'certificateType', + details: [ConwayEraCertificatesTypes.VoteRegistrationDelegation] + }, + { + title: 'stakeKey', + details: ['stake1u929x2y7nnfm797upl7v9rc39pqg87pk5cygvnn2edqmvuq6h48su'] + }, + { + title: 'drepId', + details: ['drep1cs234l5mtapethapx8cq97nkpa27xf84phruh5f6jqxa78ymlp4'] + }, + { + title: 'depositPaid', + info: 'depositPaidInfo', + details: [['2.00 ADA', '0.08 USD']] + } +]; + +export const VoteRegistrationDelegationCertificate: Story = { + args: { + ...data, + name: 'Vote Registration Delegation Certificate', + certificates: [voteRegistrationDelegationCertificate] + } +}; + +const stakeVoteRegistrationDelegationCertificate = [ + { + title: 'certificateType', + details: [ConwayEraCertificatesTypes.StakeVoteRegistrationDelegation] + }, + { + title: 'stakeKey', + details: ['stake1u929x2y7nnfm797upl7v9rc39pqg87pk5cygvnn2edqmvuq6h48su'] + }, + { + title: 'poolId', + details: ['pool1k0ucs0fau2vhr3p7qh7mnpfgrllwwda7petxjz2gzzaxkyp8f88'] + }, + { + title: 'drepId', + details: ['drep1cs234l5mtapethapx8cq97nkpa27xf84phruh5f6jqxa78ymlp4'] + }, + { + title: 'depositPaid', + info: 'depositPaidInfo', + details: [['2.00 ADA', '0.08 USD']] + } +]; + +export const StakeVoteRegistrationDelegationCertificate: Story = { + args: { + ...data, + name: 'Stake Vote Registration Delegation Certificate', + certificates: [stakeVoteRegistrationDelegationCertificate] + } +}; + +const updateDRep = [ + { + title: 'certificateType', + details: [ConwayEraCertificatesTypes.UpdateDelegateRepresentative] + }, + { + title: 'drepId', + details: ['65ge6g54g5dd5'] + }, + { + title: 'anchorHash', + details: ['3d2a9d15382c14f5ca260a2f5bfb645fe148bfe10c1d0e1d305b7b1393e2bd97'] + }, + { + title: 'anchorURL', + details: [ + 'https://raw.githubusercontent.com/Ryun1/gov-metadata/main/governace-action/metadata.jsonldr1q99...uqvzlalu' + ] + } +]; + +export const UpdateDRep: Story = { + name: 'Update DRep', + args: { + ...data, + name: 'Update DRep', + certificates: [updateDRep] + } +}; + +const resignCommittee = [ + { + title: 'certificateType', + details: [ConwayEraCertificatesTypes.ResignCommitteeCold] + }, + { + title: 'coldCredential', + details: ['65ge6g54g5dd5'] + }, + { + title: 'anchorHash', + details: ['3d2a9d15382c14f5ca260a2f5bfb645fe148bfe10c1d0e1d305b7b1393e2bd97'] + }, + { + title: 'anchorURL', + details: [ + 'https://raw.githubusercontent.com/Ryun1/gov-metadata/main/governace-action/metadata.jsonldr1q99...uqvzlalu' + ] + } +]; + +export const ResignCommittee: Story = { + args: { + ...data, + name: 'Resign Committee', + certificates: [resignCommittee] + } +}; + +const authorizeCommittee = [ + { + title: 'certificateType', + details: [ConwayEraCertificatesTypes.AuthorizeCommitteeHot] + }, + { + title: 'coldCredential', + details: ['cc_cold 65ge6g54g5dd5'] + }, + { + title: 'hotCredential', + details: ['cc_hot stake1u929x2y7nnfm797upl7v9rc39pqg87pk5cygvnn2edqmvuq6h48su'] + } +]; + +export const AuthorizeCommittee: Story = { + args: { + ...data, + name: 'Authorize Committee', + certificates: [authorizeCommittee] + } +}; + +const dRepRegistration = [ + { + title: 'certificateType', + details: [ConwayEraCertificatesTypes.RegisterDelegateRepresentative] + }, + { + title: 'drepId', + details: ['drep170ef53apap7dadzemkcd7lujlzk5hyzvzzjj7f3sx89ecn3ft6u'] + }, + { + title: 'anchorHash', + details: ['3d2a9d15382c14f5ca260a2f5bfb645fe148bfe10c1d0e1d305b7b1393e2bd97'] + }, + { + title: 'anchorURL', + details: [ + 'https://raw.githubusercontent.com/Ryun1/gov-metadata/main/governace-action/metadata.jsonldr1q99...uqvzlalu' + ] + }, + { + title: 'depositPaid', + info: 'depositPaidInfo', + details: [['0.35 ADA', '0.08 USD']] + } +]; + +export const DRepRegistration: Story = { + name: 'DRep Registration', + args: { + ...data, + name: 'DRep Registration', + certificates: [dRepRegistration] + } +}; + +const dRepDeRegistration = [ + { + title: 'certificateType', + details: [ConwayEraCertificatesTypes.UnregisterDelegateRepresentative] + }, + { + title: 'drepId', + details: ['drep170ef53apap7dadzemkcd7lujlzk5hyzvzzjj7f3sx89ecn3ft6u'] + }, + { + title: 'depositReturned', + info: 'depositReturnedInfo', + details: [['0.35 ADA', '0.08 USD']] + } +]; + +export const DRepDeRegistration: Story = { + name: 'DRep De-Registration', + args: { + ...data, + name: 'DRep De-Registration', + certificates: [dRepDeRegistration] + } +}; + +const voteDelegation = [ + { + title: 'certificateType', + details: [ConwayEraCertificatesTypes.VoteDelegation] + }, + { + title: 'stakeKey', + details: ['stake1u929x2y7nnfm797upl7v9rc39pqg87pk5cygvnn2edqmvuq6h48su'] + }, + { + title: 'drepId', + details: ['drep1cs234l5mtapethapx8cq97nkpa27xf84phruh5f6jqxa78ymlp4'] + } +]; + +export const VoteDelegation: Story = { + args: { + ...data, + name: 'Vote Delegation', + certificates: [voteDelegation] + } +}; + +const confirmVote = [ + { + title: 'voterType', + details: ['drep'] + }, + { + title: 'voterCredential', + details: ['drep1cs234l5mtapethapx8cq97nkpa27xf84phruh5f6jqxa78ymlp4'] + }, + { + title: 'voteTypes', + details: ['yes'] + }, + { + title: 'anchorURL', + details: ['https://shorturl.at/eK145'] + }, + { + title: 'anchorHash', + details: ['9067f223838d88b83f660c05eedf7f6f65c45de31e522c1bcb6a1eb287b17e89'] + } +]; + +export const ConfirmVote: Story = { + args: { + ...data, + name: 'Confirm Vote', + votingProcedures: [confirmVote, confirmVote] + } +}; + +const parameterChangeAction = [ + { + title: 'type', + details: [Wallet.Cardano.GovernanceActionType.parameter_change_action] + }, + { + title: 'deposit', + details: ['stake1u89sa...css5vgr'] + }, + { + title: 'rewardAccount', + details: ['https://www.someurl.io'] + }, + { + title: 'anchorURL', + details: ['https://www.someurl.io'] + }, + { + title: 'anchorHash', + details: ['000000...0000'] + }, + { + header: 'maxTxExUnits', + details: [ + { + title: 'memory', + details: ['100000000'] + }, + { + title: 'step', + details: ['10000000000000'] + } + ] + }, + { + header: 'maxBlockExUnits', + details: [ + { + title: 'memory', + details: ['50000000'] + }, + { + title: 'step', + details: ['40000000000'] + } + ] + }, + { + header: 'networkGroup', + details: [ + { + title: 'maxBBSize', + details: ['65536'] + }, + { + title: 'maxTxSize', + details: ['16384'] + }, + { + title: 'maxBHSize', + details: ['1100'] + }, + { + title: 'maxValSize', + details: ['5000'] + }, + { + title: 'maxCollateralInputs', + details: ['3'] + } + ] + }, + { + header: 'economicGroup', + details: [ + { + title: 'minFeeA', + details: ['44'] + }, + { + title: 'minFeeB', + details: ['155381'] + }, + { + title: 'keyDeposit', + details: ['2000000'] + }, + { + title: 'poolDeposit', + details: ['500000000'] + }, + { + title: 'rho', + details: ['0.003'] + }, + { + title: 'tau', + details: ['0.2'] + }, + { + title: 'minPoolCost', + details: ['340000000'] + }, + { + title: 'coinsPerUTxOByte', + details: ['34482'] + } + ] + }, + { + header: 'costModels', + details: [ + { + title: 'PlutusV1', + details: ['197209'] + }, + { + title: 'PlutusV2', + details: ['197209'] + } + ] + }, + { + header: 'technicalGroup', + details: [ + { + title: 'a0', + details: ['0.3'] + }, + { + title: 'eMax', + details: ['18'] + }, + { + title: 'nOpt', + details: ['150'] + }, + { + title: 'collateralPercentage', + details: ['150'] + } + ] + }, + { + header: 'prices', + details: [ + { + title: 'memory', + details: ['0.0577'] + }, + { + title: 'step', + details: ['0.0000721'] + } + ] + }, + { + header: 'governanceGroup', + details: [ + { + title: 'govActionLifetime', + details: ['10'] + }, + { + title: 'govActionDeposit', + details: ['500 ADA'] + }, + { + title: 'drepDeposit', + details: ['1000 ADA'] + }, + { + title: 'drepActivity', + details: ['5'] + }, + { + title: 'ccMinSize', + details: ['7'] + }, + { + title: 'ccMaxTermLength', + details: ['25'] + } + ] + }, + { + header: 'dRepVotingThresholds', + details: [ + { + title: 'motionNoConfidence', + details: ['51%'] + }, + { + title: 'committeeNormal', + details: ['67%'] + }, + { + title: 'committeeNoConfidence', + details: ['80%'] + }, + { + title: 'updateConstitution', + details: ['75%'] + }, + { + title: 'hardForkInitiation', + details: ['90%'] + }, + { + title: 'ppNetworkGroup', + details: ['70%'] + }, + { + title: 'ppEconomicGroup', + details: ['70%'] + }, + { + title: 'ppTechnicalGroup', + details: ['70%'] + }, + { + title: 'ppGovernanceGroup', + details: ['70%'] + }, + { + title: 'treasuryWithdrawal', + details: ['51%'] + } + ] + } +]; + +export const ParameterChangeAction: Story = { + args: { + ...data, + name: 'Parameter Change Action', + proposalProcedures: [parameterChangeAction] + } +}; + +const hardForkInitiationAction = [ + { + title: 'type', + details: [Wallet.Cardano.GovernanceActionType.hard_fork_initiation_action] + }, + { + title: 'deposit', + info: 'deposit', + details: [['2.00 ADA', '0.18 USD']] + }, + { title: 'rewardAccount', details: ['23bcf2892e8182a68e3aac6f9f42ed3317d115ebad12a17232681175'] }, + { + title: 'anchorURL', + details: [ + 'https://raw.githubusercontent.com/Ryun1/gov-metadata/main/governace-action/metadata.jsonldr1q99...uqvzlalu' + ] + }, + { + title: 'anchorHash', + details: ['3d2a9d15382c14f5ca260a2f5bfb645fe148bfe10c1d0e1d305b7b1393e2bd97'] + }, + { + title: 'governanceActionID', + details: ['d0b1f7be72731a97e9728e0f1c358d576fd28aa9f290d53ce1ef803a1a753ba8'] + }, + { + title: 'actionIndex', + details: ['0'] + }, + { + title: 'protocolVersionMajor', + details: ['1'] + }, + { + title: 'protocolVersionMinor', + details: ['2'] + }, + { + title: 'protocolVersionPatch', + details: ['3'] + } +]; + +export const HardForkInitiationAction: Story = { + args: { + ...data, + name: 'Hard Fork Initiation Action', + proposalProcedures: [hardForkInitiationAction] + } +}; + +const infoAction = [ + { + title: 'type', + details: [Wallet.Cardano.GovernanceActionType.info_action] + }, + { + title: 'deposit', + info: 'deposit', + details: [['2.00 ADA', '0.18 USD']] + }, + { + title: 'anchorURL', + details: [ + 'https://raw.githubusercontent.com/Ryun1/gov-metadata/main/governace-action/metadata.jsonldr1q99...uqvzlalu' + ] + }, + { + title: 'anchorHash', + details: ['3d2a9d15382c14f5ca260a2f5bfb645fe148bfe10c1d0e1d305b7b1393e2bd97'] + }, + { + title: 'governanceActionID', + details: ['d0b1f7be72731a97e9728e0f1c358d576fd28aa9f290d53ce1ef803a1a753ba8'] + }, + { + title: 'actionIndex', + details: ['0'] + } +]; + +export const InfoAction: Story = { + args: { + ...data, + name: 'Info Action', + proposalProcedures: [infoAction] + } +}; + +const newConstitutionAction = [ + { + title: 'type', + details: [Wallet.Cardano.GovernanceActionType.new_constitution] + }, + { + title: 'anchorURL', + details: [ + 'https://raw.githubusercontent.com/Ryun1/gov-metadata/main/governace-action/metadata.jsonldr1q99...uqvzlalu' + ] + }, + { + title: 'anchorHash', + details: ['3d2a9d15382c14f5ca260a2f5bfb645fe148bfe10c1d0e1d305b7b1393e2bd97'] + }, + { + title: 'governanceActionID', + details: ['d0b1f7be72731a97e9728e0f1c358d576fd28aa9f290d53ce1ef803a1a753ba8'] + }, + { + title: 'actionIndex', + details: ['0'] + }, + { + title: 'constitutionAnchorURL', + details: ['https://www.someurl.io'] + }, + { + title: 'constitutionScriptHash', + details: ['cb0ec2692497b458e46812c8a5bfa2931d1a2d965a99893828ec810f'] + } +]; + +export const NewConstitutionAction: Story = { + args: { + ...data, + name: 'New Constitution Action', + proposalProcedures: [newConstitutionAction] + } +}; + +const noConfidenceAction = [ + { + title: 'type', + details: [Wallet.Cardano.GovernanceActionType.no_confidence] + }, + { + title: 'deposit', + info: 'deposit', + details: [['2.00 ADA', '0.18 USD']] + }, + { title: 'rewardAccount', details: ['23bcf2892e8182a68e3aac6f9f42ed3317d115ebad12a17232681175'] }, + { + title: 'anchorURL', + details: [ + 'https://raw.githubusercontent.com/Ryun1/gov-metadata/main/governace-action/metadata.jsonldr1q99...uqvzlalu' + ] + }, + { + title: 'anchorHash', + details: ['3d2a9d15382c14f5ca260a2f5bfb645fe148bfe10c1d0e1d305b7b1393e2bd97'] + }, + { + title: 'governanceActionID', + details: ['d0b1f7be72731a97e9728e0f1c358d576fd28aa9f290d53ce1ef803a1a753ba8'] + }, + { + title: 'actionIndex', + details: ['0'] + } +]; + +export const NoConfidenceAction: Story = { + args: { + ...data, + name: 'No Confidence Action', + proposalProcedures: [noConfidenceAction] + } +}; + +const updateCommitteeAction = [ + { + title: 'type', + details: [Wallet.Cardano.GovernanceActionType.update_committee] + }, + { + title: 'deposit', + info: 'deposit', + details: [['2.00 ADA', '0.18 USD']] + }, + { title: 'rewardAccount', details: ['23bcf2892e8182a68e3aac6f9f42ed3317d115ebad12a17232681175'] }, + { + title: 'anchorURL', + details: [ + 'https://raw.githubusercontent.com/Ryun1/gov-metadata/main/governace-action/metadata.jsonldr1q99...uqvzlalu' + ] + }, + { + title: 'anchorHash', + details: ['3d2a9d15382c14f5ca260a2f5bfb645fe148bfe10c1d0e1d305b7b1393e2bd97'] + }, + { + title: 'governanceActionID', + details: ['d0b1f7be72731a97e9728e0f1c358d576fd28aa9f290d53ce1ef803a1a753ba8'] + }, + { + title: 'actionIndex', + details: ['0'] + }, + { + header: 'membersToBeAdded', + details: [ + { + title: 'coldCredentialHash', + details: ['30000000000000000000000000000000000000000000000000000000'] + }, + { + title: 'epoch', + details: ['1'] + }, + { + title: 'coldCredentialHash', + details: ['40000000000000000000000000000000000000000000000000000000'] + }, + { + title: 'epoch', + details: ['2'] + } + ] + }, + { + header: 'membersToBeRemoved', + details: [ + { + title: 'hash', + details: ['00000000000000000000000000000000000000000000000000000000'] + } + ] + }, + { + title: 'newQuorumThreshold', + details: ['0.4%'] + } +]; + +export const UpdateCommitteeAction: Story = { + args: { + ...data, + name: 'Update Committee Action', + proposalProcedures: [updateCommitteeAction] + } +}; + +const treasuryWithdrawalsAction = [ + { + title: 'type', + details: [Wallet.Cardano.GovernanceActionType.treasury_withdrawals_action] + }, + { + title: 'deposit', + info: 'deposit', + details: [['2.00 ADA', '0.18 USD']] + }, + { title: 'rewardAccount', details: ['23bcf2892e8182a68e3aac6f9f42ed3317d115ebad12a17232681175'] }, + { + title: 'anchorURL', + details: [ + 'https://raw.githubusercontent.com/Ryun1/gov-metadata/main/governace-action/metadata.jsonldr1q99...uqvzlalu' + ] + }, + { + title: 'anchorHash', + details: ['3d2a9d15382c14f5ca260a2f5bfb645fe148bfe10c1d0e1d305b7b1393e2bd97'] + }, + { + title: 'governanceActionID', + details: ['d0b1f7be72731a97e9728e0f1c358d576fd28aa9f290d53ce1ef803a1a753ba8'] + }, + { + title: 'actionIndex', + details: ['0'] + }, + { + header: 'withdrawal', + details: [ + { + title: 'withdrawalRewardAccount', + details: ['23bcf2892e8182a68e3aac6f9f42ed3317d115ebad12a17232681175'] + }, + { + title: 'withdrawalAmount', + details: ['1030939916423 ADA'] + }, + { + title: 'withdrawalRewardAccount', + details: ['23bcf2892e8182a68e3aac6f9f42ed3317d115ebad12a17232681175'] + }, + { + title: 'withdrawalAmount', + details: ['1030939916423 ADA'] + } + ] + } +]; + +export const TreasuryWithdrawalsAction: Story = { + args: { + ...data, + name: 'Treasury Withdrawals Action', + proposalProcedures: [treasuryWithdrawalsAction] + } +}; diff --git a/packages/core/src/ui/components/ActivityDetail/TransactionDetails.tsx b/packages/core/src/ui/components/ActivityDetail/TransactionDetails.tsx index 1fa563df5..ecd094a3c 100644 --- a/packages/core/src/ui/components/ActivityDetail/TransactionDetails.tsx +++ b/packages/core/src/ui/components/ActivityDetail/TransactionDetails.tsx @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ /* eslint-disable no-magic-numbers */ import React from 'react'; import cn from 'classnames'; @@ -6,11 +7,19 @@ import { Ellipsis, toast } from '@lace/common'; import { Box } from '@lace/ui'; import { useTranslate } from '@src/ui/hooks'; import CopyToClipboard from 'react-copy-to-clipboard'; -import type { ActivityStatus } from '../Activity'; +import { ActivityStatus } from '../Activity'; import styles from './TransactionDetails.module.scss'; import { TransactionInputOutput } from './TransactionInputOutput'; import { TransactionFee } from './TransactionFee'; import { ActivityDetailHeader } from './ActivityDetailHeader'; +import { TxDetailList } from './TxDetailsList'; +import { + TxDetailsVotingProceduresTitles, + TxDetailsProposalProceduresTitles, + TxDetailsCertificateTitles, + TxDetails, + TxDetail +} from './types'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const displayMetadataMsg = (value: any[]): string => value?.find((val: any) => val.hasOwnProperty('msg'))?.msg || ''; @@ -64,9 +73,13 @@ export interface TransactionDetailsProps { tooltipContent?: string; addressToNameMap: Map; isPopupView?: boolean; - openExternalLink?: () => void; + openExternalLink?: (url: string) => void; + handleOpenExternalHashLink?: () => void; sendAnalyticsInputs?: () => void; sendAnalyticsOutputs?: () => void; + votingProcedures?: TxDetails[]; + proposalProcedures?: TxDetails[]; + certificates?: TxDetails[]; } const TOAST_DEFAULT_DURATION = 3; @@ -107,12 +120,104 @@ export const TransactionDetails = ({ addressToNameMap, isPopupView, openExternalLink, + handleOpenExternalHashLink, sendAnalyticsInputs, - sendAnalyticsOutputs + sendAnalyticsOutputs, + proposalProcedures, + votingProcedures, + certificates }: TransactionDetailsProps): React.ReactElement => { const { t } = useTranslate(); - const isSending = status === 'sending'; - const isSuccess = status === 'success'; + + const isSending = status === ActivityStatus.PENDING; + const isSuccess = status === ActivityStatus.SUCCESS; + + const renderAnchorHashDetails = (url: string) => ( +
openExternalLink(url)}> + {url} +
+ ); + + // Translate certificate typenames + // TODO: refactor this one + const translatedCertificates = certificates?.map((certificate) => + certificate?.map( + (detail) => + ({ + ...detail, + ...('title' in detail && + detail.title === 'certificateType' && { + details: [t(`package.core.assetActivityItem.entry.name.${detail.details[0]}`)] + }), + ...('title' in detail && + detail.title === 'anchorURL' && { + details: [renderAnchorHashDetails(detail.details[0] as string)] + }) + } as unknown as TxDetail) + ) + ); + + // Translate governance proposal typenames + // TODO: find a way to translate in the mappers + // TODO: refactor this one + const translatedProposalProcedures = proposalProcedures?.map((proposal) => + proposal?.map( + (p) => + ({ + ...p, + ...('title' in p && + p.title === 'type' && { + details: [t(`package.core.activityDetails.governanceActions.${p.details[0]}`)] + }), + ...('title' in p && + p.title === 'anchorURL' && { + details: [renderAnchorHashDetails(p.details[0] as string)] + }), + ...('header' in p && { + details: p.details.map((detail) => ({ + ...detail, + ...('title' in detail && + ['govActionLifetime', 'drepActivity', 'ccMaxTermLength'].includes(detail.title) && { + details: [ + ` + ${Number(detail.details[0])} + ${t( + // eslint-disable-next-line sonarjs/no-nested-template-literals + `package.core.activityDetails.${ + Number(detail.details[0]) === 0 || Number(detail.details[0]) > 1 ? 'epochs' : 'epoch' + }` + )} + ` + ] + }), + ...('title' in detail && + detail.title === 'anchorURL' && { + details: [renderAnchorHashDetails(detail.details[0] as string)] + }) + })) + }) + } as unknown as TxDetail) + ) + ); + + // Translate voting procedure typenames + // TODO: refactor this one + const translatedVotingProcedures = votingProcedures?.map((proposal) => + proposal?.map( + (p) => + ({ + ...p, + ...('title' in p && + ['voterType', 'voteTypes'].includes(p.title) && { + details: [t(`package.core.activityDetails.${p.title}.${p.details[0]}`)] + }), + ...('title' in p && + p.title === 'anchorURL' && { + details: [renderAnchorHashDetails(p.details[0] as string)] + }) + } as unknown as TxDetail) + ) + ); const renderDepositValueSection = ({ value, label }: { value: string; label: string }) => (
@@ -136,14 +241,15 @@ export const TransactionDetails = ({
-
{t('package.core.activityDetails.transactionHash')}
+
{t('package.core.activityDetails.transactionID')}
{isSending ? ( @@ -277,6 +383,198 @@ export const TransactionDetails = ({ })}
+ {votingProcedures?.length > 0 && ( + + testId="voting-procedures" + title={t('package.core.activityDetails.votingProcedures')} + subTitle={t('package.core.activityDetails.votingProcedure')} + lists={translatedVotingProcedures} + translations={{ + voterType: t('package.core.activityDetails.votingProcedureTitles.voterType'), + drepId: t('package.core.activityDetails.votingProcedureTitles.drepId'), + voterCredential: t('package.core.activityDetails.votingProcedureTitles.voterCredential'), + voteTypes: t('package.core.activityDetails.votingProcedureTitles.voteTypes'), + anchorHash: t('package.core.activityDetails.votingProcedureTitles.anchorHash'), + anchorURL: t('package.core.activityDetails.votingProcedureTitles.anchorURL') + }} + withSeparatorLine + /> + )} + {proposalProcedures?.length > 0 && ( + + testId="proposal-procedures" + title={t('package.core.activityDetails.proposalProcedures')} + subTitle={t('package.core.activityDetails.proposalProcedure')} + lists={translatedProposalProcedures} + withSeparatorLine + translations={{ + type: t('package.core.activityDetails.proposalProcedureTitles.type'), + deposit: t('package.core.activityDetails.proposalProcedureTitles.deposit'), + rewardAccount: t('package.core.activityDetails.proposalProcedureTitles.rewardAccount'), + anchorHash: t('package.core.activityDetails.proposalProcedureTitles.anchorHash'), + anchorURL: t('package.core.activityDetails.proposalProcedureTitles.anchorURL'), + governanceActionID: t('package.core.activityDetails.proposalProcedureTitles.governanceActionID'), + actionIndex: t('package.core.activityDetails.proposalProcedureTitles.actionIndex'), + newQuorumThreshold: t('package.core.activityDetails.proposalProcedureTitles.newQuorumThreshold'), + withdrawal: t('package.core.activityDetails.proposalProcedureTitles.withdrawal'), + withdrawalRewardAccount: t( + 'package.core.activityDetails.proposalProcedureTitles.withdrawalRewardAccount' + ), + withdrawalAmount: t('package.core.activityDetails.proposalProcedureTitles.withdrawalAmount'), + constitutionAnchorURL: t('package.core.activityDetails.proposalProcedureTitles.constitutionAnchorURL'), + constitutionScriptHash: t('package.core.activityDetails.proposalProcedureTitles.constitutionScriptHash'), + coldCredentialHash: t('package.core.activityDetails.proposalProcedureTitles.coldCredentialHash'), + epoch: t('package.core.activityDetails.proposalProcedureTitles.epoch'), + membersToBeAdded: t('package.core.activityDetails.proposalProcedureTitles.membersToBeAdded'), + hash: t('package.core.activityDetails.proposalProcedureTitles.hash'), + membersToBeRemoved: t('package.core.activityDetails.proposalProcedureTitles.membersToBeRemoved'), + protocolVersionMajor: t('package.core.activityDetails.proposalProcedureTitles.protocolVersionMajor'), + protocolVersionMinor: t('package.core.activityDetails.proposalProcedureTitles.protocolVersionMinor'), + protocolVersionPatch: t('package.core.activityDetails.proposalProcedureTitles.protocolVersionPatch'), + maxTxExUnits: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.maxTxExUnits' + ), + maxBlockExUnits: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.maxBlockExUnits' + ), + networkGroup: t('package.core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.title'), + economicGroup: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.title' + ), + technicalGroup: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.technicalGroup.title' + ), + costModels: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.technicalGroup.costModels' + ), + PlutusV1: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.technicalGroup.PlutusV1' + ), + PlutusV2: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.technicalGroup.PlutusV2' + ), + governanceGroup: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.governanceGroup.title' + ), + dRepVotingThresholds: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.governanceGroup.dRepVotingThresholds.title' + ), + memory: t('package.core.ProposalProcedure.governanceAction.protocolParamUpdate.memory'), + step: t('package.core.ProposalProcedure.governanceAction.protocolParamUpdate.step'), + maxBBSize: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.maxBBSize' + ), + maxTxSize: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.maxTxSize' + ), + maxBHSize: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.maxBHSize' + ), + maxValSize: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.maxValSize' + ), + maxCollateralInputs: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.networkGroup.maxCollateralInputs' + ), + minFeeA: t('package.core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.minFeeA'), + minFeeB: t('package.core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.minFeeB'), + keyDeposit: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.keyDeposit' + ), + poolDeposit: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.poolDeposit' + ), + rho: t('package.core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.rho'), + tau: t('package.core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.tau'), + minPoolCost: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.minPoolCost' + ), + coinsPerUTxOByte: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.coinsPerUTxOByte' + ), + a0: t('package.core.ProposalProcedure.governanceAction.protocolParamUpdate.technicalGroup.a0'), + eMax: t('package.core.ProposalProcedure.governanceAction.protocolParamUpdate.technicalGroup.eMax'), + nOpt: t('package.core.ProposalProcedure.governanceAction.protocolParamUpdate.technicalGroup.nOpt'), + collateralPercentage: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.technicalGroup.collateralPercentage' + ), + prices: t('package.core.ProposalProcedure.governanceAction.protocolParamUpdate.economicGroup.prices'), + govActionLifetime: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.governanceGroup.govActionLifetime' + ), + govActionDeposit: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.governanceGroup.govActionDeposit' + ), + drepDeposit: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.governanceGroup.drepDeposit' + ), + drepActivity: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.governanceGroup.drepActivity' + ), + ccMinSize: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.governanceGroup.ccMinSize' + ), + ccMaxTermLength: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.governanceGroup.ccMaxTermLength' + ), + motionNoConfidence: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.governanceGroup.dRepVotingThresholds.motionNoConfidence' + ), + committeeNormal: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.governanceGroup.dRepVotingThresholds.committeeNormal' + ), + committeeNoConfidence: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.governanceGroup.dRepVotingThresholds.committeeNoConfidence' + ), + updateConstitution: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.governanceGroup.dRepVotingThresholds.updateConstitution' + ), + hardForkInitiation: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.governanceGroup.dRepVotingThresholds.hardForkInitiation' + ), + ppNetworkGroup: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.governanceGroup.dRepVotingThresholds.ppNetworkGroup' + ), + ppEconomicGroup: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.governanceGroup.dRepVotingThresholds.ppEconomicGroup' + ), + ppTechnicalGroup: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.governanceGroup.dRepVotingThresholds.ppTechnicalGroup' + ), + ppGovernanceGroup: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.governanceGroup.dRepVotingThresholds.ppGovernanceGroup' + ), + treasuryWithdrawal: t( + 'package.core.ProposalProcedure.governanceAction.protocolParamUpdate.governanceGroup.dRepVotingThresholds.treasuryWithdrawal' + ) + }} + /> + )} + {certificates?.length > 0 && ( + + title={t('package.core.activityDetails.certificates')} + subTitle={t('package.core.activityDetails.certificate')} + testId="certificates" + lists={translatedCertificates} + withSeparatorLine + translations={{ + certificate: t('package.core.activityDetails.certificateTitles.certificate'), + certificateType: t('package.core.activityDetails.certificateTitles.certificateType'), + coldCredential: t('package.core.activityDetails.certificateTitles.coldCredential'), + hotCredential: t('package.core.activityDetails.certificateTitles.hotCredential'), + stakeKey: t('package.core.activityDetails.certificateTitles.stakeKey'), + drepId: t('package.core.activityDetails.certificateTitles.drepId'), + anchorURL: t('package.core.activityDetails.certificateTitles.anchorURL'), + anchorHash: t('package.core.activityDetails.certificateTitles.anchorHash'), + poolId: t('package.core.activityDetails.certificateTitles.poolId'), + drep: t('package.core.activityDetails.certificateTitles.drep'), + depositPaid: t('package.core.activityDetails.certificateTitles.depositPaid'), + depositPaidInfo: t('package.core.activityDetails.certificateTitles.depositPaidInfo'), + depositReturned: t('package.core.activityDetails.certificateTitles.depositReturned'), + depositReturnedInfo: t('package.core.activityDetails.certificateTitles.depositReturnedInfo') + }} + /> + )} {addrInputs?.length > 0 && ( { + testId: string; + title: string; + subTitle: string; + lists: TxDetails[]; + translations: TranslationsFor; + tooltipContent?: React.ReactNode; + withSeparatorLine?: boolean; +} + +export const TxDetailList = ({ + testId, + title, + subTitle, + lists, + tooltipContent, + withSeparatorLine, + translations +}: TxDetailListProps): React.ReactElement => { + const [isVisible, setIsVisible] = useState(); + + const Icon = BracketDown ? ( + + ) : ( + + ); + + return ( +
+
+
+ {title} + {tooltipContent && ( + + + + )} +
+ +
+ {isVisible && ( +
+ {lists.map((list, idx) => ( + +
0 })} /> + {lists.length > 1 && ( +
+
{`${subTitle} ${idx + 1}`}
+
+ )} + translations={translations} testId={testId} list={list} /> + + ))} +
+ )} +
+ ); +}; diff --git a/packages/core/src/ui/components/ActivityDetail/components/DetailRow.module.scss b/packages/core/src/ui/components/ActivityDetail/components/DetailRow.module.scss new file mode 100644 index 000000000..83b9354ec --- /dev/null +++ b/packages/core/src/ui/components/ActivityDetail/components/DetailRow.module.scss @@ -0,0 +1,45 @@ +@import '../../../styles/theme.scss'; +@import '../../../../../../common/src/ui/styles/abstracts/typography'; + +.details { + color: var(--text-color-primary, #ffffff); + display: flex; + justify-content: space-between; + margin-bottom: size_unit(4); + width: 100%; + + .title { + align-items: center; + gap: 8px; + display: flex; + flex: 0 0 50%; + align-self: baseline; + color: var(--text-color-primary, #ffffff); + @include text-body-semi-bold; + } + + .detail { + align-items: end; + display: flex; + flex-direction: column; + gap: size_unit(2); + color: var(--text-color-primary, #ffffff); + text-align: right; + word-break: break-all; + @include text-body-medium; + + @media (max-width: $breakpoint-popup) { + flex-direction: column; + } + + .subitems { + display: flex; + flex-direction: column; + width: 100%; + align-items: flex-end; + .subitem { + color: var(--text-color-secondary, #878e9e); + } + } + } +} diff --git a/packages/core/src/ui/components/ActivityDetail/components/DetailRow.tsx b/packages/core/src/ui/components/ActivityDetail/components/DetailRow.tsx new file mode 100644 index 000000000..c70d7c85b --- /dev/null +++ b/packages/core/src/ui/components/ActivityDetail/components/DetailRow.tsx @@ -0,0 +1,31 @@ +/* eslint-disable no-magic-numbers */ +import React from 'react'; +import styles from './DetailRow.module.scss'; +import { DetailRowSubitems } from './DetailRowSubitems'; +import { InfoItem } from './InfoItem'; + +type DetailsRowProps = { + title: string; + info?: string; + dataTestId?: string; + details: (string | [string, string])[]; +}; + +// TODO: add proper data mappers, eg: strings, urls, elements, arrays etc +export const DetailRow = ({ title, info, details, dataTestId }: DetailsRowProps): React.ReactElement => ( +
+
+ {title} + {info && } +
+
+ {details.map((detail, idx) => ( + + {typeof detail === 'string' && detail} + {typeof detail === 'object' && + (detail.length === 2 ? : detail)} + + ))} +
+
+); diff --git a/packages/core/src/ui/components/ActivityDetail/components/DetailRowSubitems.tsx b/packages/core/src/ui/components/ActivityDetail/components/DetailRowSubitems.tsx new file mode 100644 index 000000000..3adf72383 --- /dev/null +++ b/packages/core/src/ui/components/ActivityDetail/components/DetailRowSubitems.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import styles from './DetailRow.module.scss'; + +export interface DetailRowSubitems { + item: string; + subitem: string; + dataTestId?: string; +} +export const DetailRowSubitems = ({ item, subitem, dataTestId }: DetailRowSubitems): React.ReactElement => ( +
+ {item} + + {subitem} + +
+); diff --git a/packages/core/src/ui/components/ActivityDetail/components/DetailRows.tsx b/packages/core/src/ui/components/ActivityDetail/components/DetailRows.tsx new file mode 100644 index 000000000..8292e4158 --- /dev/null +++ b/packages/core/src/ui/components/ActivityDetail/components/DetailRows.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import cn from 'classnames'; +import { DetailRow } from './DetailRow'; +import { TxDetails } from '../types'; +import { TranslationsFor } from '@src/ui/utils/types'; +import styles from '../TransactionInputOutput.module.scss'; + +type DetailRowsProps = { + list: TxDetails; + testId: string; + translations: TranslationsFor; +}; + +export const DetailRows = function DetailRows({ + list, + testId, + translations +}: DetailRowsProps): React.ReactElement { + return ( + <> + {list?.map((item, index) => + 'title' in item ? ( + + ) : ( + <> +
0 })} + > +
{translations[item.header]}
+
+ + + ) + )} + + ); +}; diff --git a/packages/core/src/ui/components/ActivityDetail/components/InfoItem.tsx b/packages/core/src/ui/components/ActivityDetail/components/InfoItem.tsx new file mode 100644 index 000000000..143510eab --- /dev/null +++ b/packages/core/src/ui/components/ActivityDetail/components/InfoItem.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { InfoCircleOutlined } from '@ant-design/icons'; +import { Tooltip } from 'antd'; + +import { ReactComponent as Info } from '../../../assets/icons/info-icon.component.svg'; + +export interface InfoItemProps { + title: string; + dataTestId?: string; +} +export const InfoItem = ({ title, dataTestId = '' }: InfoItemProps): React.ReactElement => ( + + {Info ? ( + + ) : ( + + )} + +); diff --git a/packages/core/src/ui/components/ActivityDetail/components/index.ts b/packages/core/src/ui/components/ActivityDetail/components/index.ts new file mode 100644 index 000000000..c470084f1 --- /dev/null +++ b/packages/core/src/ui/components/ActivityDetail/components/index.ts @@ -0,0 +1 @@ +export * from './DetailRows'; diff --git a/packages/core/src/ui/components/ActivityDetail/index.ts b/packages/core/src/ui/components/ActivityDetail/index.ts index c2ee72854..b694c44a0 100644 --- a/packages/core/src/ui/components/ActivityDetail/index.ts +++ b/packages/core/src/ui/components/ActivityDetail/index.ts @@ -1,6 +1,7 @@ export * from './TransactionDetails'; export * from './RewardsDetails'; export * from './ActivityTypeIcon'; +export * from './types'; export * from './TransactionDetailAsset'; export * from './TransactionInputOutput'; export * from './TransactionFee'; diff --git a/packages/core/src/ui/components/ActivityDetail/types.ts b/packages/core/src/ui/components/ActivityDetail/types.ts new file mode 100644 index 000000000..7a8203337 --- /dev/null +++ b/packages/core/src/ui/components/ActivityDetail/types.ts @@ -0,0 +1,155 @@ +import { Wallet } from '@lace/cardano'; + +// supported certificates actions +export enum ConwayEraCertificatesTypes { + 'AuthorizeCommitteeHot' = Wallet.Cardano.CertificateType.AuthorizeCommitteeHot, + 'RegisterDelegateRepresentative' = Wallet.Cardano.CertificateType.RegisterDelegateRepresentative, + 'ResignCommitteeCold' = Wallet.Cardano.CertificateType.ResignCommitteeCold, + 'VoteRegistrationDelegation' = Wallet.Cardano.CertificateType.VoteRegistrationDelegation, + 'VoteDelegation' = Wallet.Cardano.CertificateType.VoteDelegation, + 'UpdateDelegateRepresentative' = Wallet.Cardano.CertificateType.UpdateDelegateRepresentative, + 'UnregisterDelegateRepresentative' = Wallet.Cardano.CertificateType.UnregisterDelegateRepresentative, + 'StakeVoteRegistrationDelegation' = Wallet.Cardano.CertificateType.StakeVoteRegistrationDelegation, + 'StakeVoteDelegation' = Wallet.Cardano.CertificateType.StakeVoteDelegation, + 'StakeRegistrationDelegation' = Wallet.Cardano.CertificateType.StakeRegistrationDelegation +} + +// cip 1694 governance actions +export enum Cip1694GovernanceActivityType { + ParameterChangeAction = 'ParameterChangeAction', + HardForkInitiationAction = 'HardForkInitiationAction', + TreasuryWithdrawalsAction = 'TreasuryWithdrawalsAction', + NoConfidence = 'NoConfidence', + UpdateCommittee = 'UpdateCommittee', + NewConstitution = 'NewConstitution', + InfoAction = 'InfoAction' +} + +export enum ConwayEraGovernanceActions { + 'vote' = 'vote' +} + +export enum DelegationActivityType { + 'delegation' = 'delegation', + 'delegationRegistration' = 'delegationRegistration', + 'delegationDeregistration' = 'delegationDeregistration' +} + +export enum TransactionActivityType { + 'outgoing' = 'outgoing', + 'incoming' = 'incoming', + 'self' = 'self', + 'rewards' = 'rewards' +} + +export type TxDetailsCertificateTitles = + | 'certificateType' + | 'coldCredential' + | 'hotCredential' + | 'stakeKey' + | 'drepId' + | 'anchorURL' + | 'anchorHash' + | 'poolId' + | 'drep' + | 'depositPaid' + | 'depositPaidInfo' + | 'depositReturned' + | 'depositReturnedInfo' + | 'certificate'; + +export type TxDetailsProposalProceduresTitles = + | 'type' + | 'deposit' + | 'rewardAccount' + | 'anchorHash' + | 'anchorURL' + | 'governanceActionID' + | 'actionIndex' + | 'newQuorumThreshold' + | 'withdrawal' + | 'withdrawalRewardAccount' + | 'withdrawalAmount' + | 'constitutionAnchorURL' + | 'constitutionScriptHash' + | 'coldCredentialHash' + | 'epoch' + | 'membersToBeAdded' + | 'hash' + | 'membersToBeRemoved' + | 'protocolVersionMajor' + | 'protocolVersionMinor' + | 'protocolVersionPatch' + | 'maxTxExUnits' + | 'maxBlockExUnits' + | 'networkGroup' + | 'economicGroup' + | 'technicalGroup' + | 'costModels' + | 'governanceGroup' + | 'dRepVotingThresholds' + | 'memory' + | 'step' + | 'maxBBSize' + | 'maxTxSize' + | 'maxBHSize' + | 'maxValSize' + | 'maxCollateralInputs' + | 'minFeeA' + | 'minFeeB' + | 'keyDeposit' + | 'poolDeposit' + | 'rho' + | 'tau' + | 'minPoolCost' + | 'coinsPerUTxOByte' + | 'a0' + | 'eMax' + | 'nOpt' + | 'collateralPercentage' + | 'prices' + | 'PlutusV1' + | 'PlutusV2' + | 'govActionLifetime' + | 'govActionDeposit' + | 'drepDeposit' + | 'drepActivity' + | 'ccMinSize' + | 'ccMaxTermLength' + | 'motionNoConfidence' + | 'committeeNormal' + | 'committeeNoConfidence' + | 'updateConstitution' + | 'hardForkInitiation' + | 'ppNetworkGroup' + | 'ppEconomicGroup' + | 'ppTechnicalGroup' + | 'ppGovernanceGroup' + | 'treasuryWithdrawal'; + +export type TxDetailsVotingProceduresTitles = + | 'voterType' + | 'voterCredential' + | 'voteTypes' + | 'drepId' + | 'anchorHash' + | 'anchorURL'; + +export type TxDetail = { + title: T; + info?: T; + details: (string | [string, string])[]; +}; + +export type TxDetaisList = { + header: T; + details: TxDetail[]; +}; + +export type TxDetails = (TxDetail | TxDetaisList)[]; + +export type GovernanceTransactionTypes = + | ConwayEraCertificatesTypes + | ConwayEraGovernanceActions + | Cip1694GovernanceActivityType; +export type ActivityType = DelegationActivityType | TransactionActivityType | GovernanceTransactionTypes; diff --git a/packages/core/src/ui/components/ConfirmDRepRegistration/ConfirmDRepRegistration.stories.tsx b/packages/core/src/ui/components/ConfirmDRepRegistration/ConfirmDRepRegistration.stories.tsx new file mode 100644 index 000000000..94e3848d4 --- /dev/null +++ b/packages/core/src/ui/components/ConfirmDRepRegistration/ConfirmDRepRegistration.stories.tsx @@ -0,0 +1,64 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { ConfirmDRepRegistration } from './ConfirmDRepRegistration'; +import { ComponentProps } from 'react'; + +const customViewports = { + popup: { + name: 'Popup', + styles: { + width: '360px', + height: '600' + } + } +}; + +const meta: Meta = { + title: 'Sanchonet/Certificates/ConfirmDRepRegistration', + component: ConfirmDRepRegistration, + parameters: { + layout: 'centered', + viewport: { + viewports: customViewports, + defaultViewport: 'popup' + } + } +}; + +export default meta; +type Story = StoryObj; + +const data: ComponentProps = { + dappInfo: { + logo: 'https://cdn.mint.handle.me/favicon.png', + name: 'Mint', + url: 'https://preprod.mint.handle.me' + }, + translations: { + labels: { + depositPaid: 'Deposit paid', + drepId: 'DRep ID', + hash: 'Hash', + url: 'URL' + }, + metadata: 'Metadata' + }, + metadata: { + depositPaid: '0.35 ADA', + drepId: '65ge6g54g5dd5', + hash: '9bba8233cdd086f0325daba465d568a88970d42536f9e71e92a80d5922ded885', + url: 'https://raw.githubusercontent.com/Ryun1/gov-metadata/main/governace-action/metadata.jsonldr1q99...uqvzlalu' + } +}; + +export const Overview: Story = { + args: { + ...data + } +}; +export const WithError: Story = { + args: { + ...data, + errorMessage: 'Something went wrong' + } +}; diff --git a/packages/core/src/ui/components/ConfirmDRepRegistration/ConfirmDRepRegistration.tsx b/packages/core/src/ui/components/ConfirmDRepRegistration/ConfirmDRepRegistration.tsx new file mode 100644 index 000000000..644b457fe --- /dev/null +++ b/packages/core/src/ui/components/ConfirmDRepRegistration/ConfirmDRepRegistration.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { Box, Cell, Grid, TransactionSummary, Flex } from '@lace/ui'; +import { DappInfo, DappInfoProps } from '../DappInfo'; +import { ErrorPane } from '@lace/common'; +interface Props { + dappInfo: Omit; + errorMessage?: string; + translations: { + labels: { + url: string; + hash: string; + drepId: string; + depositPaid: string; + }; + metadata: string; + }; + metadata: { + url: string; + hash: string; + drepId: string; + depositPaid: string; + }; +} + +export const ConfirmDRepRegistration = ({ dappInfo, errorMessage, translations, metadata }: Props): JSX.Element => ( + + + + + {errorMessage && ( + + + + )} + + + + + {metadata.url && ( + + + + )} + {metadata.hash && ( + + + + )} + + + + + + + + +); diff --git a/packages/core/src/ui/components/ConfirmDRepRegistration/index.ts b/packages/core/src/ui/components/ConfirmDRepRegistration/index.ts new file mode 100644 index 000000000..cf5bded0e --- /dev/null +++ b/packages/core/src/ui/components/ConfirmDRepRegistration/index.ts @@ -0,0 +1 @@ +export { ConfirmDRepRegistration } from './ConfirmDRepRegistration'; diff --git a/packages/core/src/ui/components/ConfirmDRepRetirement/ConfirmDRepRetirement.stories.tsx b/packages/core/src/ui/components/ConfirmDRepRetirement/ConfirmDRepRetirement.stories.tsx new file mode 100644 index 000000000..594a6fcc5 --- /dev/null +++ b/packages/core/src/ui/components/ConfirmDRepRetirement/ConfirmDRepRetirement.stories.tsx @@ -0,0 +1,60 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { ConfirmDRepRetirement } from './ConfirmDRepRetirement'; +import { ComponentProps } from 'react'; + +const customViewports = { + popup: { + name: 'Popup', + styles: { + width: '360px', + height: '600' + } + } +}; + +const meta: Meta = { + title: 'Sanchonet/Certificates/ConfirmDRepRetirement', + component: ConfirmDRepRetirement, + parameters: { + layout: 'centered', + viewport: { + viewports: customViewports, + defaultViewport: 'popup' + } + } +}; + +export default meta; +type Story = StoryObj; + +const data: ComponentProps = { + dappInfo: { + logo: 'https://cdn.mint.handle.me/favicon.png', + name: 'Mint', + url: 'https://preprod.mint.handle.me' + }, + translations: { + labels: { + depositReturned: 'Deposit paid', + drepId: 'DRep ID' + }, + metadata: 'Metadata' + }, + metadata: { + depositReturned: '0.35 ADA', + drepId: '65ge6g54g5dd5' + } +}; + +export const Overview: Story = { + args: { + ...data + } +}; +export const WithError: Story = { + args: { + ...data, + errorMessage: 'Something went wrong' + } +}; diff --git a/packages/core/src/ui/components/ConfirmDRepRetirement/ConfirmDRepRetirement.tsx b/packages/core/src/ui/components/ConfirmDRepRetirement/ConfirmDRepRetirement.tsx new file mode 100644 index 000000000..59219813a --- /dev/null +++ b/packages/core/src/ui/components/ConfirmDRepRetirement/ConfirmDRepRetirement.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { Box, Cell, Grid, TransactionSummary, Flex } from '@lace/ui'; +import { DappInfo, DappInfoProps } from '../DappInfo'; +import { ErrorPane } from '@lace/common'; + +interface Props { + dappInfo: Omit; + errorMessage?: string; + translations: { + labels: { + drepId: string; + depositReturned: string; + }; + metadata: string; + }; + metadata: { + drepId: string; + depositReturned: string; + }; +} + +export const ConfirmDRepRetirement = ({ dappInfo, errorMessage, translations, metadata }: Props): JSX.Element => ( + + + + + {errorMessage && ( + + + + )} + + + + + + + + + + + + +); diff --git a/packages/core/src/ui/components/ConfirmDRepRetirement/index.ts b/packages/core/src/ui/components/ConfirmDRepRetirement/index.ts new file mode 100644 index 000000000..cb0e8d0d1 --- /dev/null +++ b/packages/core/src/ui/components/ConfirmDRepRetirement/index.ts @@ -0,0 +1 @@ +export { ConfirmDRepRetirement } from './ConfirmDRepRetirement'; diff --git a/packages/core/src/ui/components/ConfirmDRepUpdate/ConfirmDRepUpdate.stories.tsx b/packages/core/src/ui/components/ConfirmDRepUpdate/ConfirmDRepUpdate.stories.tsx new file mode 100644 index 000000000..5a72d541f --- /dev/null +++ b/packages/core/src/ui/components/ConfirmDRepUpdate/ConfirmDRepUpdate.stories.tsx @@ -0,0 +1,72 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { ConfirmDRepUpdate } from './ConfirmDRepUpdate'; +import { ComponentProps } from 'react'; + +const customViewports = { + popup: { + name: 'Popup', + styles: { + width: '360px', + height: '600' + } + } +}; + +const meta: Meta = { + title: 'Sanchonet/Certificates/ConfirmDRepUpdate', + component: ConfirmDRepUpdate, + parameters: { + layout: 'centered', + viewport: { + viewports: customViewports, + defaultViewport: 'popup' + } + } +}; + +export default meta; +type Story = StoryObj; + +const data: ComponentProps = { + dappInfo: { + logo: 'https://cdn.mint.handle.me/favicon.png', + name: 'Mint', + url: 'https://preprod.mint.handle.me' + }, + translations: { + labels: { + drepId: 'DRep ID', + hash: 'Hash', + url: 'URL' + }, + metadata: 'Metadata' + }, + metadata: { + drepId: '65ge6g54g5dd5', + hash: '9bba8233cdd086f0325daba465d568a88970d42536f9e71e92a80d5922ded885', + url: 'https://raw.githubusercontent.com/Ryun1/gov-metadata/main/governace-action/metadata.jsonldr1q99...uqvzlalu' + } +}; + +export const Overview: Story = { + args: { + ...data + } +}; + +export const Empty: Story = { + args: { + ...data, + metadata: { + drepId: '65ge6g54g5dd5' + } + } +}; + +export const WithError: Story = { + args: { + ...data, + errorMessage: 'Something went wrong' + } +}; diff --git a/packages/core/src/ui/components/ConfirmDRepUpdate/ConfirmDRepUpdate.tsx b/packages/core/src/ui/components/ConfirmDRepUpdate/ConfirmDRepUpdate.tsx new file mode 100644 index 000000000..e86ee2b77 --- /dev/null +++ b/packages/core/src/ui/components/ConfirmDRepUpdate/ConfirmDRepUpdate.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { Box, Cell, Grid, TransactionSummary, Flex } from '@lace/ui'; +import { DappInfo, DappInfoProps } from '../DappInfo'; +import { ErrorPane } from '@lace/common'; + +interface Props { + dappInfo: Omit; + errorMessage?: string; + translations: { + labels: { + url: string; + hash: string; + drepId: string; + }; + metadata: string; + }; + metadata: { + url?: string; + hash?: string; + drepId: string; + }; +} + +export const ConfirmDRepUpdate = ({ dappInfo, errorMessage, translations, metadata }: Props): JSX.Element => ( + + + + + {errorMessage && ( + + + + )} + + + + + {metadata.url && ( + + + + )} + {metadata.hash && ( + + + + )} + + + + + +); diff --git a/packages/core/src/ui/components/ConfirmDRepUpdate/index.ts b/packages/core/src/ui/components/ConfirmDRepUpdate/index.ts new file mode 100644 index 000000000..1017f7880 --- /dev/null +++ b/packages/core/src/ui/components/ConfirmDRepUpdate/index.ts @@ -0,0 +1 @@ +export { ConfirmDRepUpdate } from './ConfirmDRepUpdate'; diff --git a/packages/core/src/ui/components/ConfirmStakeRegistrationDelegation/ConfirmStakeRegistrationDelegation.stories.tsx b/packages/core/src/ui/components/ConfirmStakeRegistrationDelegation/ConfirmStakeRegistrationDelegation.stories.tsx new file mode 100644 index 000000000..68e9b704b --- /dev/null +++ b/packages/core/src/ui/components/ConfirmStakeRegistrationDelegation/ConfirmStakeRegistrationDelegation.stories.tsx @@ -0,0 +1,62 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { ConfirmStakeRegistrationDelegation } from './ConfirmStakeRegistrationDelegation'; +import { ComponentProps } from 'react'; + +const customViewports = { + popup: { + name: 'Popup', + styles: { + width: '360px', + height: '600' + } + } +}; + +const meta: Meta = { + title: 'Sanchonet/Certificates/ConfirmStakeRegistrationDelegation', + component: ConfirmStakeRegistrationDelegation, + parameters: { + layout: 'centered', + viewport: { + viewports: customViewports, + defaultViewport: 'popup' + } + } +}; + +export default meta; +type Story = StoryObj; + +const data: ComponentProps = { + dappInfo: { + logo: 'https://cdn.mint.handle.me/favicon.png', + name: 'Mint', + url: 'https://preprod.mint.handle.me' + }, + translations: { + labels: { + stakeKeyHash: 'Stake key hash', + poolId: 'Pool Id', + depositPaid: 'Deposit paid' + }, + metadata: 'Metadata' + }, + metadata: { + stakeKeyHash: '13cf55d175ea848b87deb3e914febd7e028e2bf6534475d52fb9c3d0', + poolId: 'pool1zuevzm3xlrhmwjw87ec38mzs02tlkwec9wxpgafcaykmwg7efhh', + depositPaid: '0.35 ADA' + } +}; + +export const Overview: Story = { + args: { + ...data + } +}; +export const WithError: Story = { + args: { + ...data, + errorMessage: 'Something went wrong' + } +}; diff --git a/packages/core/src/ui/components/ConfirmStakeRegistrationDelegation/ConfirmStakeRegistrationDelegation.tsx b/packages/core/src/ui/components/ConfirmStakeRegistrationDelegation/ConfirmStakeRegistrationDelegation.tsx new file mode 100644 index 000000000..ada22bfaf --- /dev/null +++ b/packages/core/src/ui/components/ConfirmStakeRegistrationDelegation/ConfirmStakeRegistrationDelegation.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { Box, Cell, Grid, TransactionSummary, Flex } from '@lace/ui'; +import { DappInfo, DappInfoProps } from '../DappInfo'; +import { ErrorPane } from '@lace/common'; + +interface Props { + dappInfo: Omit; + errorMessage?: string; + translations: { + labels: { + poolId: string; + stakeKeyHash: string; + depositPaid: string; + }; + metadata: string; + }; + metadata: { + poolId: string; + stakeKeyHash: string; + depositPaid: string; + }; +} + +export const ConfirmStakeRegistrationDelegation = ({ + dappInfo, + errorMessage, + translations, + metadata +}: Props): JSX.Element => ( + + + + + {errorMessage && ( + + + + )} + + + + + + + + + + + + + + + +); diff --git a/packages/core/src/ui/components/ConfirmStakeRegistrationDelegation/index.ts b/packages/core/src/ui/components/ConfirmStakeRegistrationDelegation/index.ts new file mode 100644 index 000000000..78be311a9 --- /dev/null +++ b/packages/core/src/ui/components/ConfirmStakeRegistrationDelegation/index.ts @@ -0,0 +1 @@ +export { ConfirmStakeRegistrationDelegation } from './ConfirmStakeRegistrationDelegation'; diff --git a/packages/core/src/ui/components/ConfirmStakeVoteDelegation/ConfirmStakeVoteDelegation.stories.tsx b/packages/core/src/ui/components/ConfirmStakeVoteDelegation/ConfirmStakeVoteDelegation.stories.tsx new file mode 100644 index 000000000..c044156a4 --- /dev/null +++ b/packages/core/src/ui/components/ConfirmStakeVoteDelegation/ConfirmStakeVoteDelegation.stories.tsx @@ -0,0 +1,91 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { ConfirmStakeVoteDelegation } from './ConfirmStakeVoteDelegation'; +import { ComponentProps } from 'react'; + +const customViewports = { + popup: { + name: 'Popup', + styles: { + width: '360px', + height: '600' + } + } +}; + +const meta: Meta = { + title: 'Sanchonet/Certificates/ConfirmStakeVoteDelegation', + component: ConfirmStakeVoteDelegation, + parameters: { + layout: 'centered', + viewport: { + viewports: customViewports, + defaultViewport: 'popup' + } + } +}; + +export default meta; +type Story = StoryObj; + +const data: ComponentProps = { + dappInfo: { + logo: 'https://cdn.mint.handle.me/favicon.png', + name: 'Mint', + url: 'https://preprod.mint.handle.me' + }, + translations: { + labels: { + drepId: 'DRep ID', + alwaysAbstain: 'Abstain', + alwaysNoConfidence: 'No Confidence', + stakeKeyHash: 'Stake key hash', + poolId: 'Pool Id' + }, + option: 'Yes', + metadata: 'Metadata' + }, + metadata: { + drepId: 'drep1ruvgm0auzdplfn7g2jf3kcnpnw5mlhwxaxj8crag8h6t2ye9y9g', + alwaysAbstain: false, + alwaysNoConfidence: false, + stakeKeyHash: '13cf55d175ea848b87deb3e914febd7e028e2bf6534475d52fb9c3d0', + poolId: 'pool1zuevzm3xlrhmwjw87ec38mzs02tlkwec9wxpgafcaykmwg7efhh' + } +}; + +export const Overview: Story = { + args: { + ...data + } +}; +export const WithError: Story = { + args: { + ...data, + errorMessage: 'Something went wrong' + } +}; + +export const WithAbstain: Story = { + args: { + ...data, + metadata: { + ...data.metadata, + drepId: undefined, + alwaysAbstain: true, + alwaysNoConfidence: false + } + } +}; + +export const WithNoConfidence: Story = { + args: { + ...data, + metadata: { + ...data.metadata, + drepId: undefined, + alwaysAbstain: false, + alwaysNoConfidence: true + } + } +}; diff --git a/packages/core/src/ui/components/ConfirmStakeVoteDelegation/ConfirmStakeVoteDelegation.tsx b/packages/core/src/ui/components/ConfirmStakeVoteDelegation/ConfirmStakeVoteDelegation.tsx new file mode 100644 index 000000000..77cfd3243 --- /dev/null +++ b/packages/core/src/ui/components/ConfirmStakeVoteDelegation/ConfirmStakeVoteDelegation.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { Box, Cell, Grid, TransactionSummary, Flex } from '@lace/ui'; +import { DappInfo, DappInfoProps } from '../DappInfo'; +import { ErrorPane } from '@lace/common'; + +interface Props { + dappInfo: Omit; + errorMessage?: string; + translations: { + labels: { + poolId: string; + stakeKeyHash: string; + drepId: string; + alwaysAbstain: string; + alwaysNoConfidence: string; + }; + option: string; + metadata: string; + }; + metadata: { + poolId: string; + stakeKeyHash: string; + drepId?: string; + alwaysAbstain: boolean; + alwaysNoConfidence: boolean; + }; +} + +export const ConfirmStakeVoteDelegation = ({ dappInfo, errorMessage, translations, metadata }: Props): JSX.Element => ( + + + + + {errorMessage && ( + + + + )} + + + + + {metadata.drepId && ( + + + + )} + {metadata.alwaysAbstain && ( + + + + )} + {metadata.alwaysNoConfidence && ( + + + + )} + + + + + + + + +); diff --git a/packages/core/src/ui/components/ConfirmStakeVoteDelegation/index.ts b/packages/core/src/ui/components/ConfirmStakeVoteDelegation/index.ts new file mode 100644 index 000000000..d532c9b44 --- /dev/null +++ b/packages/core/src/ui/components/ConfirmStakeVoteDelegation/index.ts @@ -0,0 +1 @@ +export { ConfirmStakeVoteDelegation } from './ConfirmStakeVoteDelegation'; diff --git a/packages/core/src/ui/components/ConfirmStakeVoteRegistrationDelegation/ConfirmStakeVoteRegistrationDelegation.stories.tsx b/packages/core/src/ui/components/ConfirmStakeVoteRegistrationDelegation/ConfirmStakeVoteRegistrationDelegation.stories.tsx new file mode 100644 index 000000000..c5f8e313b --- /dev/null +++ b/packages/core/src/ui/components/ConfirmStakeVoteRegistrationDelegation/ConfirmStakeVoteRegistrationDelegation.stories.tsx @@ -0,0 +1,93 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { ConfirmStakeVoteRegistrationDelegation } from './ConfirmStakeVoteRegistrationDelegation'; +import { ComponentProps } from 'react'; + +const customViewports = { + popup: { + name: 'Popup', + styles: { + width: '360px', + height: '600' + } + } +}; + +const meta: Meta = { + title: 'Sanchonet/Certificates/ConfirmStakeVoteRegistrationDelegation', + component: ConfirmStakeVoteRegistrationDelegation, + parameters: { + layout: 'centered', + viewport: { + viewports: customViewports, + defaultViewport: 'popup' + } + } +}; + +export default meta; +type Story = StoryObj; + +const data: ComponentProps = { + dappInfo: { + logo: 'https://cdn.mint.handle.me/favicon.png', + name: 'Mint', + url: 'https://preprod.mint.handle.me' + }, + translations: { + labels: { + drepId: 'DRep ID', + alwaysAbstain: 'Abstain', + alwaysNoConfidence: 'No Confidence', + stakeKeyHash: 'Stake key hash', + poolId: 'Pool Id', + depositPaid: 'Deposit paid' + }, + option: 'Yes', + metadata: 'Metadata' + }, + metadata: { + drepId: 'drep1ruvgm0auzdplfn7g2jf3kcnpnw5mlhwxaxj8crag8h6t2ye9y9g', + alwaysAbstain: false, + alwaysNoConfidence: false, + stakeKeyHash: '13cf55d175ea848b87deb3e914febd7e028e2bf6534475d52fb9c3d0', + poolId: 'pool1zuevzm3xlrhmwjw87ec38mzs02tlkwec9wxpgafcaykmwg7efhh', + depositPaid: '0.35 ADA' + } +}; + +export const Overview: Story = { + args: { + ...data + } +}; +export const WithError: Story = { + args: { + ...data, + errorMessage: 'Something went wrong' + } +}; + +export const WithAbstain: Story = { + args: { + ...data, + metadata: { + ...data.metadata, + drepId: undefined, + alwaysAbstain: true, + alwaysNoConfidence: false + } + } +}; + +export const WithNoConfidence: Story = { + args: { + ...data, + metadata: { + ...data.metadata, + drepId: undefined, + alwaysAbstain: false, + alwaysNoConfidence: true + } + } +}; diff --git a/packages/core/src/ui/components/ConfirmStakeVoteRegistrationDelegation/ConfirmStakeVoteRegistrationDelegation.tsx b/packages/core/src/ui/components/ConfirmStakeVoteRegistrationDelegation/ConfirmStakeVoteRegistrationDelegation.tsx new file mode 100644 index 000000000..a8e0789e4 --- /dev/null +++ b/packages/core/src/ui/components/ConfirmStakeVoteRegistrationDelegation/ConfirmStakeVoteRegistrationDelegation.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { Box, Cell, Grid, TransactionSummary, Flex } from '@lace/ui'; +import { DappInfo, DappInfoProps } from '../DappInfo'; +import { ErrorPane } from '@lace/common'; + +interface Props { + dappInfo: Omit; + errorMessage?: string; + translations: { + labels: { + poolId: string; + stakeKeyHash: string; + drepId: string; + alwaysAbstain: string; + alwaysNoConfidence: string; + depositPaid: string; + }; + option: string; + metadata: string; + }; + metadata: { + poolId: string; + stakeKeyHash: string; + drepId?: string; + alwaysAbstain: boolean; + alwaysNoConfidence: boolean; + depositPaid: string; + }; +} + +export const ConfirmStakeVoteRegistrationDelegation = ({ + dappInfo, + errorMessage, + translations, + metadata +}: Props): JSX.Element => ( + + + + + {errorMessage && ( + + + + )} + + + + + {metadata.drepId && ( + + + + )} + {metadata.alwaysAbstain && ( + + + + )} + {metadata.alwaysNoConfidence && ( + + + + )} + + + + + + + + + + + +); diff --git a/packages/core/src/ui/components/ConfirmStakeVoteRegistrationDelegation/index.ts b/packages/core/src/ui/components/ConfirmStakeVoteRegistrationDelegation/index.ts new file mode 100644 index 000000000..6e5483c0d --- /dev/null +++ b/packages/core/src/ui/components/ConfirmStakeVoteRegistrationDelegation/index.ts @@ -0,0 +1 @@ +export { ConfirmStakeVoteRegistrationDelegation } from './ConfirmStakeVoteRegistrationDelegation'; diff --git a/packages/core/src/ui/components/ConfirmVoteDelegation/ConfirmVoteDelegation.stories.tsx b/packages/core/src/ui/components/ConfirmVoteDelegation/ConfirmVoteDelegation.stories.tsx new file mode 100644 index 000000000..485675692 --- /dev/null +++ b/packages/core/src/ui/components/ConfirmVoteDelegation/ConfirmVoteDelegation.stories.tsx @@ -0,0 +1,83 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { ConfirmVoteDelegation } from './ConfirmVoteDelegation'; +import { ComponentProps } from 'react'; + +const customViewports = { + popup: { + name: 'Popup', + styles: { + width: '360px', + height: '600' + } + } +}; + +const meta: Meta = { + title: 'Sanchonet/Certificates/ConfirmVoteDelegation', + component: ConfirmVoteDelegation, + parameters: { + layout: 'centered', + viewport: { + viewports: customViewports, + defaultViewport: 'popup' + } + } +}; + +export default meta; +type Story = StoryObj; + +const data: ComponentProps = { + dappInfo: { + logo: 'https://cdn.mint.handle.me/favicon.png', + name: 'Mint', + url: 'https://preprod.mint.handle.me' + }, + translations: { + labels: { + drepId: 'DRep ID', + alwaysAbstain: 'Abstain', + alwaysNoConfidence: 'No Confidence' + }, + option: 'Yes', + metadata: 'Metadata' + }, + metadata: { + drepId: 'drep1ruvgm0auzdplfn7g2jf3kcnpnw5mlhwxaxj8crag8h6t2ye9y9g', + alwaysAbstain: false, + alwaysNoConfidence: false + } +}; + +export const Overview: Story = { + args: { + ...data + } +}; +export const WithError: Story = { + args: { + ...data, + errorMessage: 'Something went wrong' + } +}; + +export const WithAbstain: Story = { + args: { + ...data, + metadata: { + alwaysAbstain: true, + alwaysNoConfidence: false + } + } +}; + +export const WithNoConfidence: Story = { + args: { + ...data, + metadata: { + alwaysAbstain: false, + alwaysNoConfidence: true + } + } +}; diff --git a/packages/core/src/ui/components/ConfirmVoteDelegation/ConfirmVoteDelegation.tsx b/packages/core/src/ui/components/ConfirmVoteDelegation/ConfirmVoteDelegation.tsx new file mode 100644 index 000000000..4eae2a274 --- /dev/null +++ b/packages/core/src/ui/components/ConfirmVoteDelegation/ConfirmVoteDelegation.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { Box, Cell, Grid, TransactionSummary, Flex } from '@lace/ui'; +import { DappInfo, DappInfoProps } from '../DappInfo'; +import { ErrorPane } from '@lace/common'; + +interface Props { + dappInfo: Omit; + errorMessage?: string; + translations: { + labels: { + drepId: string; + alwaysAbstain: string; + alwaysNoConfidence: string; + }; + option: string; + metadata: string; + }; + metadata: { + drepId?: string; + alwaysAbstain: boolean; + alwaysNoConfidence: boolean; + }; +} + +export const ConfirmVoteDelegation = ({ dappInfo, errorMessage, translations, metadata }: Props): JSX.Element => ( + + + + + {errorMessage && ( + + + + )} + + + + + {metadata.drepId && ( + + + + )} + {metadata.alwaysAbstain && ( + + + + )} + {metadata.alwaysNoConfidence && ( + + + + )} + + +); diff --git a/packages/core/src/ui/components/ConfirmVoteDelegation/index.ts b/packages/core/src/ui/components/ConfirmVoteDelegation/index.ts new file mode 100644 index 000000000..33e8e6755 --- /dev/null +++ b/packages/core/src/ui/components/ConfirmVoteDelegation/index.ts @@ -0,0 +1 @@ +export { ConfirmVoteDelegation } from './ConfirmVoteDelegation'; diff --git a/packages/core/src/ui/components/ConfirmVoteRegistrationDelegation/ConfirmVoteRegistrationDelegation.stories.tsx b/packages/core/src/ui/components/ConfirmVoteRegistrationDelegation/ConfirmVoteRegistrationDelegation.stories.tsx new file mode 100644 index 000000000..03427a18c --- /dev/null +++ b/packages/core/src/ui/components/ConfirmVoteRegistrationDelegation/ConfirmVoteRegistrationDelegation.stories.tsx @@ -0,0 +1,91 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { ConfirmVoteRegistrationDelegation } from './ConfirmVoteRegistrationDelegation'; +import { ComponentProps } from 'react'; + +const customViewports = { + popup: { + name: 'Popup', + styles: { + width: '360px', + height: '600' + } + } +}; + +const meta: Meta = { + title: 'Sanchonet/Certificates/ConfirmVoteRegistrationDelegation', + component: ConfirmVoteRegistrationDelegation, + parameters: { + layout: 'centered', + viewport: { + viewports: customViewports, + defaultViewport: 'popup' + } + } +}; + +export default meta; +type Story = StoryObj; + +const data: ComponentProps = { + dappInfo: { + logo: 'https://cdn.mint.handle.me/favicon.png', + name: 'Mint', + url: 'https://preprod.mint.handle.me' + }, + translations: { + labels: { + drepId: 'DRep ID', + alwaysAbstain: 'Abstain', + alwaysNoConfidence: 'No Confidence', + depositPaid: 'Deposit paid', + stakeKeyHash: 'Stake key hash' + }, + option: 'Yes', + metadata: 'Metadata' + }, + metadata: { + drepId: 'drep1ruvgm0auzdplfn7g2jf3kcnpnw5mlhwxaxj8crag8h6t2ye9y9g', + alwaysAbstain: false, + alwaysNoConfidence: false, + depositPaid: '0.35 ADA', + stakeKeyHash: '13cf55d175ea848b87deb3e914febd7e028e2bf6534475d52fb9c3d0' + } +}; + +export const Overview: Story = { + args: { + ...data + } +}; +export const WithError: Story = { + args: { + ...data, + errorMessage: 'Something went wrong' + } +}; + +export const WithAbstain: Story = { + args: { + ...data, + metadata: { + ...data.metadata, + drepId: undefined, + alwaysAbstain: true, + alwaysNoConfidence: false + } + } +}; + +export const WithNoConfidence: Story = { + args: { + ...data, + metadata: { + ...data.metadata, + drepId: undefined, + alwaysAbstain: false, + alwaysNoConfidence: true + } + } +}; diff --git a/packages/core/src/ui/components/ConfirmVoteRegistrationDelegation/ConfirmVoteRegistrationDelegation.tsx b/packages/core/src/ui/components/ConfirmVoteRegistrationDelegation/ConfirmVoteRegistrationDelegation.tsx new file mode 100644 index 000000000..39f184e26 --- /dev/null +++ b/packages/core/src/ui/components/ConfirmVoteRegistrationDelegation/ConfirmVoteRegistrationDelegation.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { Box, Cell, Grid, TransactionSummary, Flex } from '@lace/ui'; +import { DappInfo, DappInfoProps } from '../DappInfo'; +import { ErrorPane } from '@lace/common'; + +interface Props { + dappInfo: Omit; + errorMessage?: string; + translations: { + labels: { + drepId: string; + alwaysAbstain: string; + alwaysNoConfidence: string; + stakeKeyHash: string; + depositPaid: string; + }; + option: string; + metadata: string; + }; + metadata: { + drepId?: string; + alwaysAbstain: boolean; + alwaysNoConfidence: boolean; + stakeKeyHash: string; + depositPaid: string; + }; +} + +export const ConfirmVoteRegistrationDelegation = ({ + dappInfo, + errorMessage, + translations, + metadata +}: Props): JSX.Element => ( + + + + + {errorMessage && ( + + + + )} + + + + + {metadata.drepId && ( + + + + )} + {metadata.alwaysAbstain && ( + + + + )} + {metadata.alwaysNoConfidence && ( + + + + )} + + + + + + + + +); diff --git a/packages/core/src/ui/components/ConfirmVoteRegistrationDelegation/index.ts b/packages/core/src/ui/components/ConfirmVoteRegistrationDelegation/index.ts new file mode 100644 index 000000000..fbcf38853 --- /dev/null +++ b/packages/core/src/ui/components/ConfirmVoteRegistrationDelegation/index.ts @@ -0,0 +1 @@ +export { ConfirmVoteRegistrationDelegation } from './ConfirmVoteRegistrationDelegation'; diff --git a/packages/core/src/ui/components/DappTransaction/DappTransaction.stories.tsx b/packages/core/src/ui/components/DappTransaction/DappTransaction.stories.tsx new file mode 100644 index 000000000..07525eac9 --- /dev/null +++ b/packages/core/src/ui/components/DappTransaction/DappTransaction.stories.tsx @@ -0,0 +1,54 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { DappTransaction } from './DappTransaction'; +import { ComponentProps } from 'react'; +import { Wallet } from '@lace/cardano'; + +const meta: Meta = { + title: 'DappTransaction', + component: DappTransaction, + parameters: { + layout: 'centered' + } +}; + +export default meta; +type Story = StoryObj; + +const data: ComponentProps = { + dappInfo: { + logo: 'https://cdn.mint.handle.me/favicon.png', + name: 'Mint', + url: 'https://preprod.mint.handle.me' + }, + transaction: { + fee: '0.17', + outputs: [ + { + coins: '1', + recipient: + 'addr_test1qrl0s3nqfljv8dfckn7c4wkzu5rl6wn4hakkddcz2mczt3szlqss933x0aag07qcgspcaglmay6ufl4y4lalmlpe02mqhl0fx2' + } + ], + type: Wallet.Cip30TxType.Mint + } +}; + +export const Overview: Story = { + args: { + ...data + } +}; + +export const WithInsufficientFunds: Story = { + args: { + ...data + } +}; + +export const WithError: Story = { + args: { + ...data, + errorMessage: 'Something went wrong' + } +}; diff --git a/packages/core/src/ui/components/DappTransaction/DappTransaction.tsx b/packages/core/src/ui/components/DappTransaction/DappTransaction.tsx index 1bde1147a..8a70fb94c 100644 --- a/packages/core/src/ui/components/DappTransaction/DappTransaction.tsx +++ b/packages/core/src/ui/components/DappTransaction/DappTransaction.tsx @@ -4,27 +4,18 @@ import { ErrorPane } from '@lace/common'; import { Wallet } from '@lace/cardano'; import { DappInfo, DappInfoProps } from '../DappInfo'; import { DappTxHeader } from './DappTxHeader/DappTxHeader'; -import { DappTxAsset, DappTxAssetProps } from './DappTxAsset/DappTxAsset'; -import { DappTxOutput, DappTxOutputProps } from './DappTxOutput/DappTxOutput'; +import { DappTxAsset } from './DappTxAsset/DappTxAsset'; +import { DappTxOutput } from './DappTxOutput/DappTxOutput'; import styles from './DappTransaction.module.scss'; import { useTranslate } from '@src/ui/hooks'; import { TransactionFee, Collateral } from '@ui/components/ActivityDetail'; -type TransactionDetails = { - fee: string; - outputs: DappTxOutputProps[]; - type: 'Send' | 'Mint'; - mintedAssets?: DappTxAssetProps[]; - burnedAssets?: DappTxAssetProps[]; - collateral?: string; -}; - const amountTransformer = (fiat: { price: number; code: string }) => (ada: string) => `${Wallet.util.convertAdaToFiat({ ada, fiat: fiat.price })} ${fiat.code}`; export interface DappTransactionProps { /** Transaction details such as type, amount, fee and address */ - transaction: TransactionDetails; + transaction: Wallet.Cip30SignTxSummary; /** dApp information such as logo, name and url */ dappInfo: Omit; /** Optional error message */ @@ -48,7 +39,7 @@ export const DappTransaction = ({ {errorMessage && }
- {type === 'Mint' && mintedAssets?.length > 0 && ( + {type === Wallet.Cip30TxType.Mint && mintedAssets?.length > 0 && ( <> )} - {type === 'Mint' && burnedAssets?.length > 0 && ( + {type === Wallet.Cip30TxType.Mint && burnedAssets?.length > 0 && ( <> 0 ? undefined : t('package.core.dappTransaction.transaction')} + title={burnedAssets?.length > 0 ? undefined : t('package.core.dappTransaction.transaction')} subtitle={t('package.core.dappTransaction.burn')} /> {burnedAssets.map((asset) => ( @@ -70,7 +61,7 @@ export const DappTransaction = ({ ))} )} - {type === 'Send' && ( + {type === Wallet.Cip30TxType.Send && ( <> { +export const DappTxAsset = ({ amount, name, ticker }: Wallet.Cip30SignTxAssetItem): React.ReactElement => { const { t } = useTranslate(); return (
diff --git a/packages/core/src/ui/components/DappTransaction/DappTxAsset/index.ts b/packages/core/src/ui/components/DappTransaction/DappTxAsset/index.ts new file mode 100644 index 000000000..3418661f1 --- /dev/null +++ b/packages/core/src/ui/components/DappTransaction/DappTxAsset/index.ts @@ -0,0 +1 @@ +export { DappTxAsset } from './DappTxAsset'; diff --git a/packages/core/src/ui/components/DappTransaction/DappTxOutput/DappTxOutput.tsx b/packages/core/src/ui/components/DappTransaction/DappTxOutput/DappTxOutput.tsx index d60892916..9eccd4e0e 100644 --- a/packages/core/src/ui/components/DappTransaction/DappTxOutput/DappTxOutput.tsx +++ b/packages/core/src/ui/components/DappTransaction/DappTxOutput/DappTxOutput.tsx @@ -1,13 +1,13 @@ import React from 'react'; import { Ellipsis } from '@lace/common'; -import { DappTxAssetProps } from '../DappTxAsset/DappTxAsset'; import styles from './DappTxOutput.module.scss'; import { useTranslate } from '@src/ui/hooks'; +import { Wallet } from '@lace/cardano'; export interface DappTxOutputProps { coins: string; recipient: string; - assets?: DappTxAssetProps[]; + assets?: Wallet.Cip30SignTxAssetItem[]; } export const DappTxOutput = ({ recipient, coins, assets }: DappTxOutputProps): React.ReactElement => { diff --git a/packages/core/src/ui/components/InsufficientFundsWarning/InsufficientFundsWarning.module.scss b/packages/core/src/ui/components/InsufficientFundsWarning/InsufficientFundsWarning.module.scss new file mode 100644 index 000000000..303cbebf6 --- /dev/null +++ b/packages/core/src/ui/components/InsufficientFundsWarning/InsufficientFundsWarning.module.scss @@ -0,0 +1,23 @@ +@import '../../styles/theme.scss'; +@import '../../../../../common/src/ui/styles/abstracts/_typography.scss'; + +.warningAlert { + flex-direction: row; + background: var(--lace-cream); + display: flex; + align-items: center; + border-radius: size_unit(2); + padding: size_unit(2) size_unit(3); + gap: size_unit(3); + + svg { + height: size_unit(3); + width: size_unit(3); + color: var(--data-orange); + } + + p { + @include text-body-semi-bold; + margin: 0; + } +} diff --git a/packages/core/src/ui/components/InsufficientFundsWarning/InsufficientFundsWarning.tsx b/packages/core/src/ui/components/InsufficientFundsWarning/InsufficientFundsWarning.tsx new file mode 100644 index 000000000..7823ca8d4 --- /dev/null +++ b/packages/core/src/ui/components/InsufficientFundsWarning/InsufficientFundsWarning.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import styles from './InsufficientFundsWarning.module.scss'; +import { ReactComponent as WarningIcon } from '../../assets/icons/warning-icon.component.svg'; +import Icon from '@ant-design/icons'; + +export interface DappTransactionProps { + translations: string; +} + +export const InsufficientFundsWarning = ({ translations }: DappTransactionProps): React.ReactElement => ( +
+ +

{translations}

+
+); diff --git a/packages/core/src/ui/components/InsufficientFundsWarning/index.ts b/packages/core/src/ui/components/InsufficientFundsWarning/index.ts new file mode 100644 index 000000000..fdd126c9d --- /dev/null +++ b/packages/core/src/ui/components/InsufficientFundsWarning/index.ts @@ -0,0 +1 @@ +export * from './InsufficientFundsWarning'; diff --git a/packages/core/src/ui/components/ProposalProcedures/HardForkInitiationAction/HardForkInitiationAction.stories.tsx b/packages/core/src/ui/components/ProposalProcedures/HardForkInitiationAction/HardForkInitiationAction.stories.tsx new file mode 100644 index 000000000..0ff20971f --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/HardForkInitiationAction/HardForkInitiationAction.stories.tsx @@ -0,0 +1,98 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { HardForkInitiationAction } from './HardForkInitiationAction'; +import { ComponentProps } from 'react'; + +const customViewports = { + popup: { + name: 'Popup', + styles: { + width: '360px', + height: '600' + } + } +}; + +const meta: Meta = { + title: 'Sanchonet/Proposal Procedures/HardForkInitiationAction', + component: HardForkInitiationAction, + parameters: { + layout: 'centered', + viewport: { + viewports: customViewports, + defaultViewport: 'popup' + } + } +}; + +export default meta; +type Story = StoryObj; + +const data: ComponentProps = { + dappInfo: { + logo: 'https://cdn.mint.handle.me/favicon.png', + name: 'Mint', + url: 'https://preprod.mint.handle.me' + }, + data: { + txDetails: { + txType: 'Hard Fork Initiation', + deposit: '2000', + rewardAccount: 'stake1u89sasnfyjtmgk8ydqfv3fdl52f36x3djedfnzfc9rkgzrcss5vgr' + }, + procedure: { + anchor: { + hash: '26bfdcc75a7f4d0cd8c71f0189bc5ca5ad2f4a3db6240c82b5a0edac7f9203e0', + url: 'https://www.someurl.io', + txHashUrl: 'https://www.someurl.io' + } + }, + actionId: { + index: '0', + id: '26bfdcc75a7f4d0cd8c71f0189bc5ca5ad2f4a3db6240c82b5a0edac7f9203e0' + }, + protocolVersion: { + major: '5', + minor: '1', + patch: '1' + } + }, + translations: { + txDetails: { + title: 'Transaction Details', + txType: 'Transaction Type', + deposit: 'Deposit', + rewardAccount: 'Reward Account' + }, + procedure: { + anchor: { + hash: 'Anchor Hash', + url: 'Anchor URL' + }, + title: 'Procedure' + }, + actionId: { + title: 'Action ID', + index: 'Index', + txId: 'TX ID' + }, + protocolVersion: { + major: 'Protocol Version Major', + minor: 'Protocol Version Minor', + patch: 'Protocol Version Patch' + } + } +}; + +export const Overview: Story = { + args: { + ...data + } +}; + +export const WithError: Story = { + args: { + ...data, + errorMessage: 'Something went wrong' + } +}; diff --git a/packages/core/src/ui/components/ProposalProcedures/HardForkInitiationAction/HardForkInitiationAction.tsx b/packages/core/src/ui/components/ProposalProcedures/HardForkInitiationAction/HardForkInitiationAction.tsx new file mode 100644 index 000000000..6a722f101 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/HardForkInitiationAction/HardForkInitiationAction.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { Box, Grid, Flex, Divider, Metadata, Cell } from '@lace/ui'; +import { DappInfo, DappInfoProps } from '../../DappInfo'; +import { ErrorPane } from '@lace/common'; +import * as Types from './HardForkInitiationActionTypes'; +import { TransactionDetails } from '../components/TransactionDetails'; +import { Procedure } from '../components/Procedure'; +import { ActionId } from '../components/ActionId'; + +export interface HardForkInitiationActionProps { + dappInfo: Omit; + errorMessage?: string; + data: Types.Data; + translations: Types.Translations; +} + +export const HardForkInitiationAction = ({ + dappInfo, + errorMessage, + data: { procedure, txDetails, actionId, protocolVersion }, + translations +}: HardForkInitiationActionProps): JSX.Element => ( + + + + + {errorMessage && ( + + + + )} + + {/* tx details section */} + + + + + {/* procedure section */} + + + + + + + + {protocolVersion.patch && ( + + + + )} + {/* action id section*/} + {actionId && ( + <> + + + + + + )} + + +); diff --git a/packages/core/src/ui/components/ProposalProcedures/HardForkInitiationAction/HardForkInitiationActionTypes.ts b/packages/core/src/ui/components/ProposalProcedures/HardForkInitiationAction/HardForkInitiationActionTypes.ts new file mode 100644 index 000000000..bd8ae61b7 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/HardForkInitiationAction/HardForkInitiationActionTypes.ts @@ -0,0 +1,25 @@ +import * as ProcedureTypes from '../components/ProcedureTypes'; +import * as ActionIdTypes from '../components/ActionIdTypes'; +import * as TxDetailsTypes from '../components/TransactionDetailsTypes'; + +export interface Data { + txDetails: TxDetailsTypes.TxDetails; + procedure: ProcedureTypes.Procedure; + actionId?: ActionIdTypes.Data; + protocolVersion: { + major: string; + minor: string; + patch?: string; + }; +} + +export interface Translations { + txDetails: TxDetailsTypes.Translations; + procedure: ProcedureTypes.Translations; + actionId?: ActionIdTypes.Translations; + protocolVersion: { + major: string; + minor: string; + patch: string; + }; +} diff --git a/packages/core/src/ui/components/ProposalProcedures/HardForkInitiationAction/index.ts b/packages/core/src/ui/components/ProposalProcedures/HardForkInitiationAction/index.ts new file mode 100644 index 000000000..94f3dd74c --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/HardForkInitiationAction/index.ts @@ -0,0 +1 @@ +export { HardForkInitiationAction } from './HardForkInitiationAction'; diff --git a/packages/core/src/ui/components/ProposalProcedures/InfoAction/InfoAction.stories.tsx b/packages/core/src/ui/components/ProposalProcedures/InfoAction/InfoAction.stories.tsx new file mode 100644 index 000000000..12ee1f7ec --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/InfoAction/InfoAction.stories.tsx @@ -0,0 +1,75 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { InfoAction } from './InfoAction'; +import { ComponentProps } from 'react'; + +const customViewports = { + popup: { + name: 'Popup', + styles: { + width: '360px', + height: '600' + } + } +}; + +const meta: Meta = { + title: 'Sanchonet/Proposal Procedures/InfoAction', + component: InfoAction, + parameters: { + layout: 'centered', + viewport: { + viewports: customViewports, + defaultViewport: 'popup' + } + } +}; + +export default meta; +type Story = StoryObj; + +const data: ComponentProps = { + dappInfo: { + logo: 'https://cdn.mint.handle.me/favicon.png', + name: 'Mint', + url: 'https://preprod.mint.handle.me' + }, + data: { + txDetails: { + txType: 'Info action' + }, + procedure: { + anchor: { + hash: '26bfdcc75a7f4d0cd8c71f0189bc5ca5ad2f4a3db6240c82b5a0edac7f9203e0', + url: 'https://www.someurl.io', + txHashUrl: 'https://www.someurl.io' + } + } + }, + translations: { + txDetails: { + title: 'Transaction Details', + txType: 'Transaction Type' + }, + procedure: { + anchor: { + hash: 'Anchor Hash', + url: 'Anchor URL' + }, + title: 'Procedure' + } + } +}; + +export const Overview: Story = { + args: { + ...data + } +}; + +export const WithError: Story = { + args: { + ...data, + errorMessage: 'Something went wrong' + } +}; diff --git a/packages/core/src/ui/components/ProposalProcedures/InfoAction/InfoAction.tsx b/packages/core/src/ui/components/ProposalProcedures/InfoAction/InfoAction.tsx new file mode 100644 index 000000000..b84260bd9 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/InfoAction/InfoAction.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Box, Grid, Flex, Divider, Cell } from '@lace/ui'; +import { DappInfo, DappInfoProps } from '../../DappInfo'; +import { ErrorPane } from '@lace/common'; +import * as Types from './InfoActionTypes'; +import { TransactionDetails } from '../components/TransactionDetails'; +import { Procedure } from '../components/Procedure'; + +export interface InfoActionProps { + dappInfo: Omit; + errorMessage?: string; + data: Types.Data; + translations: Types.Translations; +} + +export const InfoAction = ({ + dappInfo, + errorMessage, + data: { procedure, txDetails }, + translations +}: InfoActionProps): JSX.Element => ( + + + + + {errorMessage && ( + + + + )} + + + + + + {/* procedure section */} + + + +); diff --git a/packages/core/src/ui/components/ProposalProcedures/InfoAction/InfoActionTypes.ts b/packages/core/src/ui/components/ProposalProcedures/InfoAction/InfoActionTypes.ts new file mode 100644 index 000000000..2f8853991 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/InfoAction/InfoActionTypes.ts @@ -0,0 +1,11 @@ +import * as ProcedureTypes from '../components/ProcedureTypes'; +import * as TxDetailsTypes from '../components/TransactionDetailsTypes'; +export interface Data { + txDetails: TxDetailsTypes.TxDetails; + procedure: ProcedureTypes.Procedure; +} + +export interface Translations { + txDetails: TxDetailsTypes.Translations; + procedure: ProcedureTypes.Translations; +} diff --git a/packages/core/src/ui/components/ProposalProcedures/InfoAction/index.ts b/packages/core/src/ui/components/ProposalProcedures/InfoAction/index.ts new file mode 100644 index 000000000..f6fae454f --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/InfoAction/index.ts @@ -0,0 +1 @@ +export { InfoAction } from './InfoAction'; diff --git a/packages/core/src/ui/components/ProposalProcedures/NewConstitutionAction/NewConstitutionAction.stories.tsx b/packages/core/src/ui/components/ProposalProcedures/NewConstitutionAction/NewConstitutionAction.stories.tsx new file mode 100644 index 000000000..dd52ad210 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/NewConstitutionAction/NewConstitutionAction.stories.tsx @@ -0,0 +1,103 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { NewConstitutionAction } from './NewConstitutionAction'; +import { ComponentProps } from 'react'; + +const customViewports = { + popup: { + name: 'Popup', + styles: { + width: '360px', + height: '600' + } + } +}; + +const meta: Meta = { + title: 'Sanchonet/Proposal Procedures/NewConstitutionAction', + component: NewConstitutionAction, + parameters: { + layout: 'centered', + viewport: { + viewports: customViewports, + defaultViewport: 'popup' + } + } +}; + +export default meta; +type Story = StoryObj; + +const data: ComponentProps = { + dappInfo: { + logo: 'https://cdn.mint.handle.me/favicon.png', + name: 'Mint', + url: 'https://preprod.mint.handle.me' + }, + data: { + txDetails: { + txType: 'New Constitution', + deposit: '2000', + rewardAccount: 'stake1u89sasnfyjtmgk8ydqfv3fdl52f36x3djedfnzfc9rkgzrcss5vgr' + }, + procedure: { + anchor: { + hash: '0000000000000000000000000000000000000000000000000000000000000000', + url: 'https://www.someurl.io', + txHashUrl: 'https://www.someurl.io/' + } + }, + actionId: { + index: '0', + id: '26bfdcc75a7f4d0cd8c71f0189bc5ca5ad2f4a3db6240c82b5a0edac7f9203e0' + }, + constitution: { + anchor: { + dataHash: '0000000000000000000000000000000000000000000000000000000000000000', + url: 'https://www.someurl.io' + }, + scriptHash: 'cb0ec2692497b458e46812c8a5bfa2931d1a2d965a99893828ec810f' + } + }, + translations: { + txDetails: { + title: 'Transaction Details', + txType: 'Transaction Type', + deposit: 'Deposit', + rewardAccount: 'Reward Account' + }, + procedure: { + anchor: { + hash: 'Anchor Hash', + url: 'Anchor URL' + }, + title: 'Procedure' + }, + actionId: { + title: 'Action ID', + index: 'Index', + txId: 'TX ID' + }, + constitution: { + title: 'Constitution Details', + anchor: { + dataHash: 'Anchor Data Hash', + url: 'Anchor URL' + }, + scriptHash: 'Script Hash' + } + } +}; + +export const Overview: Story = { + args: { + ...data + } +}; + +export const WithError: Story = { + args: { + ...data, + errorMessage: 'Something went wrong' + } +}; diff --git a/packages/core/src/ui/components/ProposalProcedures/NewConstitutionAction/NewConstitutionAction.tsx b/packages/core/src/ui/components/ProposalProcedures/NewConstitutionAction/NewConstitutionAction.tsx new file mode 100644 index 000000000..7fb710ab0 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/NewConstitutionAction/NewConstitutionAction.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { Box, Grid, Flex, Divider, Metadata, MetadataLink, Cell } from '@lace/ui'; +import { DappInfo, DappInfoProps } from '../../DappInfo'; +import { ErrorPane } from '@lace/common'; +import * as Types from './NewConstitutionActionTypes'; +import { TransactionDetails } from '../components/TransactionDetails'; +import { Procedure } from '../components/Procedure'; +import { ActionId } from '../components/ActionId'; + +export interface NewConstitutionActionProps { + dappInfo: Omit; + errorMessage?: string; + data: Types.Data; + translations: Types.Translations; +} + +export const NewConstitutionAction = ({ + dappInfo, + errorMessage, + data: { txDetails, procedure, constitution, actionId }, + translations +}: NewConstitutionActionProps): JSX.Element => ( + + + + + {errorMessage && ( + + + + )} + + {/* txDetails section */} + + + + + {/* procedure section */} + + + + + {constitution.scriptHash && ( + + + + )} + {/* action id section*/} + {actionId && ( + <> + + + + + + )} + + +); diff --git a/packages/core/src/ui/components/ProposalProcedures/NewConstitutionAction/NewConstitutionActionTypes.ts b/packages/core/src/ui/components/ProposalProcedures/NewConstitutionAction/NewConstitutionActionTypes.ts new file mode 100644 index 000000000..4d53001a2 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/NewConstitutionAction/NewConstitutionActionTypes.ts @@ -0,0 +1,30 @@ +import * as ProcedureTypes from '../components/ProcedureTypes'; +import * as ActionIdTypes from '../components/ActionIdTypes'; +import * as TxDetailsTypes from '../components/TransactionDetailsTypes'; + +export interface Data { + procedure: ProcedureTypes.Procedure; + actionId?: ActionIdTypes.Data; + txDetails: TxDetailsTypes.TxDetails; + constitution: { + anchor: { + dataHash: string; + url: string; + }; + scriptHash: string; + }; +} + +export interface Translations { + procedure: ProcedureTypes.Translations; + actionId?: ActionIdTypes.Translations; + txDetails: TxDetailsTypes.Translations; + constitution: { + title: string; + anchor: { + dataHash: string; + url: string; + }; + scriptHash: string; + }; +} diff --git a/packages/core/src/ui/components/ProposalProcedures/NewConstitutionAction/index.ts b/packages/core/src/ui/components/ProposalProcedures/NewConstitutionAction/index.ts new file mode 100644 index 000000000..2e50ba16e --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/NewConstitutionAction/index.ts @@ -0,0 +1 @@ +export { NewConstitutionAction } from './NewConstitutionAction'; diff --git a/packages/core/src/ui/components/ProposalProcedures/NoConfidenceAction/NoConfidenceAction.stories.tsx b/packages/core/src/ui/components/ProposalProcedures/NoConfidenceAction/NoConfidenceAction.stories.tsx new file mode 100644 index 000000000..dd0d0cd33 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/NoConfidenceAction/NoConfidenceAction.stories.tsx @@ -0,0 +1,88 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { NoConfidenceAction } from './NoConfidenceAction'; +import { ComponentProps } from 'react'; + +const customViewports = { + popup: { + name: 'Popup', + styles: { + width: '360px', + height: '600' + } + } +}; + +const meta: Meta = { + title: 'Sanchonet/Proposal Procedures/NoConfidenceAction', + component: NoConfidenceAction, + parameters: { + layout: 'centered', + viewport: { + viewports: customViewports, + defaultViewport: 'popup' + } + } +}; + +export default meta; +type Story = StoryObj; + +const data: ComponentProps = { + dappInfo: { + logo: 'https://cdn.mint.handle.me/favicon.png', + name: 'Mint', + url: 'https://preprod.mint.handle.me' + }, + data: { + txDetails: { + txType: 'No Confidence', + deposit: '2000', + rewardAccount: 'stake1u89sasnfyjtmgk8ydqfv3fdl52f36x3djedfnzfc9rkgzrcss5vgr' + }, + procedure: { + anchor: { + hash: '0000000000000000000000000000000000000000000000000000000000000000', + url: 'https://www.someurl.io', + txHashUrl: 'https://www.someurl.io/' + } + }, + actionId: { + index: '0', + id: '26bfdcc75a7f4d0cd8c71f0189bc5ca5ad2f4a3db6240c82b5a0edac7f9203e0' + } + }, + translations: { + txDetails: { + title: 'Transaction Details', + txType: 'Transaction Type', + deposit: 'Deposit', + rewardAccount: 'Reward Account' + }, + procedure: { + anchor: { + hash: 'Anchor Hash', + url: 'Anchor URL' + }, + title: 'Procedure' + }, + actionId: { + title: 'Action ID', + index: 'Index', + txId: 'TX ID' + } + } +}; + +export const Overview: Story = { + args: { + ...data + } +}; + +export const WithError: Story = { + args: { + ...data, + errorMessage: 'Something went wrong' + } +}; diff --git a/packages/core/src/ui/components/ProposalProcedures/NoConfidenceAction/NoConfidenceAction.tsx b/packages/core/src/ui/components/ProposalProcedures/NoConfidenceAction/NoConfidenceAction.tsx new file mode 100644 index 000000000..73034a6fe --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/NoConfidenceAction/NoConfidenceAction.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Box, Grid, Flex, Divider, Cell } from '@lace/ui'; +import { DappInfo, DappInfoProps } from '../../DappInfo'; +import { ErrorPane } from '@lace/common'; +import * as Types from './NoConfidenceActionTypes'; +import { TransactionDetails } from '../components/TransactionDetails'; +import { Procedure } from '../components/Procedure'; +import { ActionId } from '../components/ActionId'; + +export interface NoConfidenceActionProps { + dappInfo: Omit; + errorMessage?: string; + data: Types.Data; + translations: Types.Translations; +} + +export const NoConfidenceAction = ({ + dappInfo, + errorMessage, + data: { procedure, txDetails, actionId }, + translations +}: NoConfidenceActionProps): JSX.Element => ( + + + + + {errorMessage && ( + + + + )} + + {/* tx details section */} + + + + + {/* procedure section */} + + {/* action id section*/} + {actionId && ( + <> + + + + + + )} + + +); diff --git a/packages/core/src/ui/components/ProposalProcedures/NoConfidenceAction/NoConfidenceActionTypes.ts b/packages/core/src/ui/components/ProposalProcedures/NoConfidenceAction/NoConfidenceActionTypes.ts new file mode 100644 index 000000000..0cdfcea7d --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/NoConfidenceAction/NoConfidenceActionTypes.ts @@ -0,0 +1,15 @@ +import * as ProcedureTypes from '../components/ProcedureTypes'; +import * as ActionIdTypes from '../components/ActionIdTypes'; +import * as TxDetailsTypes from '../components/TransactionDetailsTypes'; + +export interface Data { + procedure: ProcedureTypes.Procedure; + actionId?: ActionIdTypes.Data; + txDetails: TxDetailsTypes.TxDetails; +} + +export interface Translations { + procedure: ProcedureTypes.Translations; + actionId?: ActionIdTypes.Translations; + txDetails: TxDetailsTypes.Translations; +} diff --git a/packages/core/src/ui/components/ProposalProcedures/NoConfidenceAction/index.ts b/packages/core/src/ui/components/ProposalProcedures/NoConfidenceAction/index.ts new file mode 100644 index 000000000..2c9c805b7 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/NoConfidenceAction/index.ts @@ -0,0 +1 @@ +export { NoConfidenceAction } from './NoConfidenceAction'; diff --git a/packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/EconomicGroup.tsx b/packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/EconomicGroup.tsx new file mode 100644 index 000000000..6738bfa0a --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/EconomicGroup.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { Metadata, Text, sx, Cell } from '@lace/ui'; +import { Card } from '../components/Card'; +import * as Types from './ParameterChangeActionTypes'; + +interface Props { + economicGroup: Types.EconomicGroup; + translations: Types.Translations['economicGroup'] & { + memory: Types.Translations['memory']; + step: Types.Translations['step']; + }; +} + +export const EconomicGroup = ({ economicGroup, translations }: Props): JSX.Element => { + const textCss = sx({ + color: '$text_primary' + }); + + return ( + <> + + + {translations.title} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/GovernanceGroup.tsx b/packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/GovernanceGroup.tsx new file mode 100644 index 000000000..e0aa03098 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/GovernanceGroup.tsx @@ -0,0 +1,144 @@ +import React from 'react'; +import { Metadata, Text, sx, Divider, Cell } from '@lace/ui'; +import * as Types from './ParameterChangeActionTypes'; + +interface Props { + governanceGroup: Types.GovernanceGroup; + translations: Types.Translations['governanceGroup']; +} + +export const GovernanceGroup = ({ governanceGroup, translations }: Props): JSX.Element => { + const textCss = sx({ + color: '$text_primary' + }); + + return ( + <> + + + {translations.title} + + + + + + + + + + + + + + + + + + + + + + + + + + {translations.dRepVotingThresholds.title} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/NetworkGroup.tsx b/packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/NetworkGroup.tsx new file mode 100644 index 000000000..88605cbc7 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/NetworkGroup.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { Metadata, Text, sx, Cell } from '@lace/ui'; +import * as Types from './ParameterChangeActionTypes'; + +interface Props { + networkGroup: Types.NetworkGroup; + translations: Types.Translations['networkGroup']; +} + +export const NetworkGroup = ({ networkGroup, translations }: Props): JSX.Element => { + const textCss = sx({ + color: '$text_primary' + }); + + return ( + <> + + + {translations.title} + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/ParameterChangeAction.stories.tsx b/packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/ParameterChangeAction.stories.tsx new file mode 100644 index 000000000..f3963851a --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/ParameterChangeAction.stories.tsx @@ -0,0 +1,243 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { ParameterChangeAction } from './ParameterChangeAction'; +import { ComponentProps } from 'react'; + +const customViewports = { + popup: { + name: 'Popup', + styles: { + width: '360px', + height: '600' + } + } +}; + +const meta: Meta = { + title: 'Sanchonet/Proposal Procedures/ParameterChangeAction', + component: ParameterChangeAction, + parameters: { + layout: 'centered', + viewport: { + viewports: customViewports, + defaultViewport: 'popup' + } + } +}; + +export default meta; +type Story = StoryObj; + +const data: ComponentProps = { + dappInfo: { + logo: 'https://cdn.mint.handle.me/favicon.png', + name: 'Mint', + url: 'https://preprod.mint.handle.me' + }, + data: { + txDetails: { + txType: 'Protocol Parameter Update', + deposit: '2000', + rewardAccount: 'stake1u89sasnfyjtmgk8ydqfv3fdl52f36x3djedfnzfc9rkgzrcss5vgr' + }, + anchor: { + hash: '0000000000000000000000000000000000000000000000000000000000000000', + url: 'https://www.someurl.io', + txHashUrl: 'https://www.someurl.io/' + }, + protocolParamUpdate: { + maxBlockExUnits: { + memory: '50000000', + step: '4000000000' + }, + maxTxExUnits: { + memory: '10000000', + step: '10000000000' + }, + networkGroup: { + maxBBSize: '65536', + maxBHSize: '1100', + maxTxSize: '16384', + maxCollateralInputs: '3', + maxValSize: '5000' + }, + economicGroup: { + minFeeA: '44', + minFeeB: '155381', + keyDeposit: '2000000', + poolDeposit: '500000000', + minPoolCost: '340000000', + coinsPerUTxOByte: '34482', + price: { + memory: '0.0577', + step: '0.0000721' + }, + rho: '0.003', + tau: '0.2' + }, + technicalGroup: { + a0: '0.3', + nOpt: '150', + collateralPercentage: '150', + costModels: { + PlutusV1: { + 'addInteger-cpu-arguments-intercept': '197_209', + 'addInteger-cpu-arguments-slope': '0' + }, + PlutusV2: { + 'addInteger-cpu-arguments-intercept': '197_209', + 'addInteger-cpu-arguments-slope': '0' + } + }, + eMax: '18' + }, + governanceGroup: { + govActionLifetime: '14', + govActionDeposit: '0', + ccMaxTermLength: '60', + ccMinSize: '0', + drepActivity: '0', + drepDeposit: '0', + dRepVotingThresholds: { + motionNoConfidence: '0.51', + committeeNormal: '0.51', + committeeNoConfidence: '0.51', + updateToConstitution: '0.51', + hardForkInitiation: '0.51', + ppNetworkGroup: '0.51', + ppEconomicGroup: '0.51', + ppTechnicalGroup: '0.51', + ppGovGroup: '0.51', + treasuryWithdrawal: '0.51' + } + } + } + }, + translations: { + txDetails: { + title: 'Transaction Details', + txType: 'Transaction Type', + deposit: 'Deposit', + rewardAccount: 'Reward Account' + }, + memory: 'Memory', + step: 'Step', + anchor: { + hash: 'Anchor Hash', + url: 'Anchor URL' + }, + networkGroup: { + title: 'Network Group', + maxBBSize: 'Max BB Size', + maxTxSize: 'Max Tx Size', + maxBHSize: 'Max BH Size', + maxValSize: 'Max Val Size', + maxTxExUnits: 'Max TX Ex Units', + maxBlockExUnits: 'Max BLK Ex Units', + maxCollateralInputs: 'Max Coll Inputs', + tooltip: { + maxBBSize: 'Max block body size', + maxTxSize: 'Max transaction size', + maxBHSize: 'Max block header size', + maxValSize: 'Max size of a serialized asset value', + maxTxExUnits: 'Max script execution units in a single transaction', + maxBlockExUnits: 'Max script execution units in a single block', + maxCollateralInputs: 'Max number of collateral inputs' + } + }, + economicGroup: { + title: 'Economic Group', + minFeeA: 'Min Fee A', + minFeeB: 'Min Fee B', + keyDeposit: 'Key Deposit', + poolDeposit: 'Pool Deposit', + rho: 'Rho', + tau: 'Tau', + minPoolCost: 'Min Pool Cost', + coinsPerUTxOByte: 'Coins/UTxO Byte', + prices: 'Price', + tooltip: { + minFeeA: 'Min fee coefficient', + minFeeB: 'Min fee constant', + keyDeposit: 'Delegation key Lovelace deposit', + poolDeposit: 'Pool registration Lovelace deposit', + rho: 'Monetary expansion', + tau: 'Treasury expansion', + minPoolCost: 'Min fixed rewards cut for pools', + coinsPerUTxOByte: 'Min Lovelace deposit per byte of serialized UTxO', + prices: 'Prices of Plutus execution units' + } + }, + technicalGroup: { + title: 'Technical Group', + a0: 'A0', + eMax: 'EMax', + nOpt: 'NOpt', + costModels: 'Cost Models', + collateralPercentage: 'Coll Percentage', + tooltip: { + a0: 'Pool pledge influence', + eMax: 'Pool retirement maximum epoch', + nOpt: 'Desired number of pools', + costModels: 'Plutus execution cost models', + collateralPercentage: 'Proportion of collateral needed for scripts' + } + }, + governanceGroup: { + title: 'Governance Group', + govActionLifetime: 'Gov Act Lifetime', + govActionDeposit: 'Gov Act Deposit', + drepDeposit: 'DRep Deposit', + drepActivity: 'DRep Activity', + ccMinSize: 'CC Min Size', + ccMaxTermLength: 'CC Max Term Length', + dRepVotingThresholds: { + title: 'Governance voting thresholds', + motionNoConfidence: 'Motion No Conf', + committeeNormal: 'Comm Normal', + committeeNoConfidence: 'Comm No Conf', + updateConstitution: 'Update Const', + hardForkInitiation: 'Hard Fork Init', + ppNetworkGroup: 'PP Network Grp', + ppEconomicGroup: 'PP Economic Grp', + ppTechnicalGroup: 'PP Technical Grp', + ppGovernanceGroup: 'PP Governance Grp', + treasuryWithdrawal: 'Treasury Withdraw' + }, + tooltip: { + govActionLifetime: 'governance action maximum lifetime in epochs', + govActionDeposit: 'governance action deposit', + drepDeposit: 'DRep deposit amount', + drepActivity: 'DRep activity period in epochs', + ccMinSize: 'Min constitutional committee size', + ccMaxTermLength: 'Max term length (in epochs) for the constitutional committee members', + dRepVotingThresholds: { + title: 'DRep voting thresholds', + motionNoConfidence: '1. Motion of no-confidence', + committeeNormal: '2a. New committee/threshold (normal state)', + committeeNoConfidence: '2b. New committee/threshold (state of no-confidence)', + updateConstitution: '3. Update to the Constitution or proposal policy', + hardForkInitiation: '4. Hard-fork initiation', + ppNetworkGroup: '5a. Protocol parameter changes, network group', + ppEconomicGroup: '5b. Protocol parameter changes, economic group', + ppTechnicalGroup: '5c. Protocol parameter changes, technical group', + ppGovernanceGroup: '5d. Protocol parameter changes, governance group', + treasuryWithdrawal: '6. Treasury withdrawal' + } + } + } + } +}; + +export const Overview: Story = { + args: { + ...data + } +}; + +export const WithError: Story = { + args: { + ...data, + errorMessage: 'Something went wrong' + } +}; diff --git a/packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/ParameterChangeAction.tsx b/packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/ParameterChangeAction.tsx new file mode 100644 index 000000000..13ae37171 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/ParameterChangeAction.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import { Box, Grid, Flex, Divider, Metadata, MetadataLink, Cell } from '@lace/ui'; +import { DappInfo, DappInfoProps } from '../../DappInfo'; +import { ErrorPane } from '@lace/common'; +import { TransactionDetails } from '../components/TransactionDetails'; +import * as Types from './ParameterChangeActionTypes'; +import { EconomicGroup } from './EconomicGroup'; +import { NetworkGroup } from './NetworkGroup'; +import { TechnicalGroup } from './TechnicalGroup'; +import { GovernanceGroup } from './GovernanceGroup'; +import { Card } from '../components/Card'; + +interface ParameterChangeActionProps { + dappInfo: Omit; + errorMessage?: string; + data: Types.Data; + translations: Types.Translations; +} + +export const ParameterChangeAction = ({ + dappInfo, + errorMessage, + data: { txDetails, protocolParamUpdate, anchor }, + translations +}: ParameterChangeActionProps): JSX.Element => { + const { economicGroup, governanceGroup, networkGroup, technicalGroup, maxTxExUnits, maxBlockExUnits } = + protocolParamUpdate; + + return ( + + + + + {errorMessage && ( + + + + )} + + {/* tx details section */} + + <> + + + + + {anchor.txHashUrl ? ( + + ) : ( + + )} + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/ParameterChangeActionTypes.ts b/packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/ParameterChangeActionTypes.ts new file mode 100644 index 000000000..90efec01d --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/ParameterChangeActionTypes.ts @@ -0,0 +1,185 @@ +import * as ProcedureTypes from '../components/ProcedureTypes'; +import * as TxDetailsTypes from '../components/TransactionDetailsTypes'; + +export interface Data { + protocolParamUpdate: ProtocolParamUpdate; + txDetails: TxDetailsTypes.TxDetails; + anchor: ProcedureTypes.Procedure['anchor']; +} + +export interface NetworkGroup { + maxBBSize: string; + maxTxSize: string; + maxBHSize: string; + maxValSize: string; + maxCollateralInputs: string; +} + +export interface EconomicGroup { + minFeeA: string; + minFeeB: string; + keyDeposit: string; + poolDeposit: string; + rho: string; + tau: string; + minPoolCost: string; + coinsPerUTxOByte: string; + price: { + memory: string; + step: string; + }; +} + +export interface TechnicalGroup { + a0: string; + eMax: string; + nOpt: string; + costModels: { + PlutusV1: Record; + PlutusV2: Record; + }; + collateralPercentage: string; +} + +export interface GovernanceGroup { + govActionLifetime: string; + govActionDeposit: string; + drepDeposit: string; + drepActivity: string; + ccMinSize: string; + ccMaxTermLength: string; + dRepVotingThresholds: { + motionNoConfidence: string; + committeeNormal: string; + committeeNoConfidence: string; + updateToConstitution: string; + hardForkInitiation: string; + ppNetworkGroup: string; + ppEconomicGroup: string; + ppTechnicalGroup: string; + ppGovGroup: string; + treasuryWithdrawal: string; + }; +} + +interface ProtocolParamUpdate { + maxTxExUnits: { + memory: string; + step: string; + }; + maxBlockExUnits: { + memory: string; + step: string; + }; + networkGroup: NetworkGroup; + economicGroup: EconomicGroup; + technicalGroup: TechnicalGroup; + governanceGroup: GovernanceGroup; +} + +export interface Translations { + txDetails: TxDetailsTypes.Translations; + anchor: ProcedureTypes.Translations['anchor']; + memory: string; + step: string; + networkGroup: { + title: string; + maxBBSize: string; + maxTxSize: string; + maxBHSize: string; + maxValSize: string; + maxTxExUnits: string; + maxBlockExUnits: string; + maxCollateralInputs: string; + tooltip: { + maxBBSize: string; + maxTxSize: string; + maxBHSize: string; + maxValSize: string; + maxTxExUnits: string; + maxBlockExUnits: string; + maxCollateralInputs: string; + }; + }; + economicGroup: { + title: string; + minFeeA: string; + minFeeB: string; + keyDeposit: string; + poolDeposit: string; + rho: string; + tau: string; + minPoolCost: string; + coinsPerUTxOByte: string; + prices: string; + tooltip: { + minFeeA: string; + minFeeB: string; + keyDeposit: string; + poolDeposit: string; + rho: string; + tau: string; + minPoolCost: string; + coinsPerUTxOByte: string; + prices: string; + }; + }; + technicalGroup: { + title: string; + a0: string; + eMax: string; + nOpt: string; + costModels: string; + collateralPercentage: string; + tooltip: { + a0: string; + eMax: string; + nOpt: string; + costModels: string; + collateralPercentage: string; + }; + }; + governanceGroup: { + title: string; + govActionLifetime: string; + govActionDeposit: string; + drepDeposit: string; + drepActivity: string; + ccMinSize: string; + ccMaxTermLength: string; + dRepVotingThresholds: { + title: string; + motionNoConfidence: string; + committeeNormal: string; + committeeNoConfidence: string; + updateConstitution: string; + hardForkInitiation: string; + ppNetworkGroup: string; + ppEconomicGroup: string; + ppTechnicalGroup: string; + ppGovernanceGroup: string; + treasuryWithdrawal: string; + }; + tooltip: { + govActionLifetime: string; + govActionDeposit: string; + drepDeposit: string; + drepActivity: string; + ccMinSize: string; + ccMaxTermLength: string; + dRepVotingThresholds: { + title: string; + motionNoConfidence: string; + committeeNormal: string; + committeeNoConfidence: string; + updateConstitution: string; + hardForkInitiation: string; + ppNetworkGroup: string; + ppEconomicGroup: string; + ppTechnicalGroup: string; + ppGovernanceGroup: string; + treasuryWithdrawal: string; + }; + }; + }; +} diff --git a/packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/TechnicalGroup.tsx b/packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/TechnicalGroup.tsx new file mode 100644 index 000000000..fd3776d21 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/TechnicalGroup.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { Metadata, Text, sx, Cell } from '@lace/ui'; +import * as Types from './ParameterChangeActionTypes'; + +interface Props { + technicalGroup: Types.TechnicalGroup; + translations: Types.Translations['technicalGroup']; +} + +export const TechnicalGroup = ({ technicalGroup, translations }: Props): JSX.Element => { + const textCss = sx({ + color: '$text_primary' + }); + + // TODO: review cost model syntax/display + // const costModels = Object.entries(technicalGroup.costModels).map(([key, value]) => ({ + // title: key, + // fields: Object.entries(value).map(([cKey, cValue]) => ({ + // label: cKey, + // value: cValue + // })) + // })); + + return ( + <> + + + {translations.title} + + + + + + + + + + + + + + + {/* TODO: review cost model syntax/display */} + {/* + + {translations.costModels} + + + + {costModels.map(({ title, fields }, idx) => ( + 0 ? '$24' : '$0'} mb={costModels.length === idx - 1 ? '$18' : '$0'} key={title}> + + + ))} + */} + + ); +}; diff --git a/packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/index.ts b/packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/index.ts new file mode 100644 index 000000000..63caf5ba1 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/ParameterChangeAction/index.ts @@ -0,0 +1 @@ +export { ParameterChangeAction } from './ParameterChangeAction'; diff --git a/packages/core/src/ui/components/ProposalProcedures/TreasuryWithdrawalsAction/TreasuryWithdrawalsAction.stories.tsx b/packages/core/src/ui/components/ProposalProcedures/TreasuryWithdrawalsAction/TreasuryWithdrawalsAction.stories.tsx new file mode 100644 index 000000000..2e6b44532 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/TreasuryWithdrawalsAction/TreasuryWithdrawalsAction.stories.tsx @@ -0,0 +1,99 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { TreasuryWithdrawalsAction } from './TreasuryWithdrawalsAction'; +import { ComponentProps } from 'react'; + +const customViewports = { + popup: { + name: 'Popup', + styles: { + width: '360px', + height: '600' + } + } +}; + +const meta: Meta = { + title: 'Sanchonet/Proposal Procedures/TreasuryWithdrawalsAction', + component: TreasuryWithdrawalsAction, + parameters: { + layout: 'centered', + viewport: { + viewports: customViewports, + defaultViewport: 'popup' + } + } +}; + +export default meta; +type Story = StoryObj; + +const data: ComponentProps = { + dappInfo: { + logo: 'https://cdn.mint.handle.me/favicon.png', + name: 'Mint', + url: 'https://preprod.mint.handle.me' + }, + data: { + txDetails: { + txType: 'Treasury Withdrawals', + deposit: '2000', + rewardAccount: 'stake1u89sasnfyjtmgk8ydqfv3fdl52f36x3djedfnzfc9rkgzrcss5vgr' + }, + procedure: { + anchor: { + hash: '26bfdcc75a7f4d0cd8c71f0189bc5ca5ad2f4a3db6240c82b5a0edac7f9203e0', + url: 'https://www.someurl.io', + txHashUrl: 'https://www.someurl.io' + } + }, + withdrawals: [ + { + rewardAccount: 'stake1u89sasnfyjtmgk8ydqfv3fdl52f36x3djedfnzfc9rkgzrcss5vgr', + lovelace: '1030939916423' + } + ], + actionId: { + index: '0', + id: '26bfdcc75a7f4d0cd8c71f0189bc5ca5ad2f4a3db6240c82b5a0edac7f9203e0' + } + }, + translations: { + txDetails: { + title: 'Transaction Details', + txType: 'Transaction Type', + deposit: 'Deposit', + rewardAccount: 'Reward Account' + }, + procedure: { + anchor: { + hash: 'Anchor Hash', + url: 'Anchor URL' + }, + title: 'Procedure' + }, + actionId: { + title: 'Action ID', + index: 'Index', + txId: 'TX ID' + }, + withdrawals: { + title: 'Withdrawal Details', + lovelace: 'Lovelace Withdrawn', + rewardAccount: 'Reward Account' + } + } +}; + +export const Overview: Story = { + args: { + ...data + } +}; + +export const WithError: Story = { + args: { + ...data, + errorMessage: 'Something went wrong' + } +}; diff --git a/packages/core/src/ui/components/ProposalProcedures/TreasuryWithdrawalsAction/TreasuryWithdrawalsAction.tsx b/packages/core/src/ui/components/ProposalProcedures/TreasuryWithdrawalsAction/TreasuryWithdrawalsAction.tsx new file mode 100644 index 000000000..38a8d2196 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/TreasuryWithdrawalsAction/TreasuryWithdrawalsAction.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { Box, Grid, Flex, Divider, sx, Text, Metadata, Cell } from '@lace/ui'; +import { DappInfo, DappInfoProps } from '../../DappInfo'; +import { ErrorPane } from '@lace/common'; +import * as Types from './TreasuryWithdrawalsActionTypes'; +import { TransactionDetails } from '../components/TransactionDetails'; +import { ActionId } from '../components/ActionId'; +import { Procedure } from '../components/Procedure'; + +interface TreasuryWithdrawalsActionProps { + dappInfo: Omit; + errorMessage?: string; + data: Types.Data; + translations: Types.Translations; +} + +export const TreasuryWithdrawalsAction = ({ + dappInfo, + errorMessage, + data: { txDetails, procedure, withdrawals, actionId }, + translations +}: TreasuryWithdrawalsActionProps): JSX.Element => { + const textCss = sx({ + color: '$text_primary' + }); + + return ( + + + + + {errorMessage && ( + + + + )} + + {/* tx details section */} + + + + + {/* procedure section */} + + + + {translations.withdrawals.title} + + + {withdrawals.map((withdrawal) => ( + + + + + + + + + ))} + {actionId && ( + <> + + + + + + )} + + + ); +}; diff --git a/packages/core/src/ui/components/ProposalProcedures/TreasuryWithdrawalsAction/TreasuryWithdrawalsActionTypes.ts b/packages/core/src/ui/components/ProposalProcedures/TreasuryWithdrawalsAction/TreasuryWithdrawalsActionTypes.ts new file mode 100644 index 000000000..7de7a0fc0 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/TreasuryWithdrawalsAction/TreasuryWithdrawalsActionTypes.ts @@ -0,0 +1,24 @@ +import * as ProcedureTypes from '../components/ProcedureTypes'; +import * as ActionIdTypes from '../components/ActionIdTypes'; +import * as TxDetailsTypes from '../components/TransactionDetailsTypes'; + +export interface Data { + actionId?: ActionIdTypes.Data; + txDetails: TxDetailsTypes.TxDetails; + procedure: ProcedureTypes.Procedure; + withdrawals: Array<{ + rewardAccount: string; + lovelace: string; + }>; +} + +export interface Translations { + txDetails: TxDetailsTypes.Translations; + actionId?: ActionIdTypes.Translations; + procedure: ProcedureTypes.Translations; + withdrawals: { + title: string; + rewardAccount: string; + lovelace: string; + }; +} diff --git a/packages/core/src/ui/components/ProposalProcedures/TreasuryWithdrawalsAction/index.ts b/packages/core/src/ui/components/ProposalProcedures/TreasuryWithdrawalsAction/index.ts new file mode 100644 index 000000000..2b3226708 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/TreasuryWithdrawalsAction/index.ts @@ -0,0 +1 @@ +export { TreasuryWithdrawalsAction } from './TreasuryWithdrawalsAction'; diff --git a/packages/core/src/ui/components/ProposalProcedures/UpdateCommitteeAction/UpdateCommitteeActionAction.stories.tsx b/packages/core/src/ui/components/ProposalProcedures/UpdateCommitteeAction/UpdateCommitteeActionAction.stories.tsx new file mode 100644 index 000000000..2df1c9075 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/UpdateCommitteeAction/UpdateCommitteeActionAction.stories.tsx @@ -0,0 +1,130 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { UpdateCommitteeAction } from './UpdateCommitteeActionAction'; +import { ComponentProps } from 'react'; + +const customViewports = { + popup: { + name: 'Popup', + styles: { + width: '360px', + height: '600' + } + } +}; + +const meta: Meta = { + title: 'Sanchonet/Proposal Procedures/UpdateCommitteeAction', + component: UpdateCommitteeAction, + parameters: { + layout: 'centered', + viewport: { + viewports: customViewports, + defaultViewport: 'popup' + } + } +}; + +export default meta; +type Story = StoryObj; + +const data: ComponentProps = { + dappInfo: { + logo: 'https://cdn.mint.handle.me/favicon.png', + name: 'Mint', + url: 'https://preprod.mint.handle.me' + }, + data: { + txDetails: { + txType: 'Hard Fork Initiation', + deposit: '2000', + rewardAccount: 'stake1u89sasnfyjtmgk8ydqfv3fdl52f36x3djedfnzfc9rkgzrcss5vgr' + }, + procedure: { + anchor: { + hash: '26bfdcc75a7f4d0cd8c71f0189bc5ca5ad2f4a3db6240c82b5a0edac7f9203e0', + url: 'https://www.someurl.io', + txHashUrl: 'https://www.someurl.io' + } + }, + actionId: { + index: '0', + id: '26bfdcc75a7f4d0cd8c71f0189bc5ca5ad2f4a3db6240c82b5a0edac7f9203e0' + }, + membersToBeAdded: [ + { + coldCredential: { + hash: '30000000000000000000000000000000000000000000000000000000' + }, + epoch: '1' + }, + { + coldCredential: { + hash: '40000000000000000000000000000000000000000000000000000000' + }, + epoch: '2' + } + ], + membersToBeRemoved: [ + { + hash: '00000000000000000000000000000000000000000000000000000000' + }, + { + hash: '20000000000000000000000000000000000000000000000000000000' + } + ], + newQuorumThreshold: { + denominator: '5', + numerator: '1' + } + }, + translations: { + txDetails: { + title: 'Transaction Details', + txType: 'Transaction Type', + deposit: 'Deposit', + rewardAccount: 'Reward Account' + }, + procedure: { + anchor: { + hash: 'Anchor Hash', + url: 'Anchor URL' + }, + title: 'Procedure' + }, + actionId: { + title: 'Action ID', + index: 'Index', + txId: 'TX ID' + }, + membersToBeAdded: { + title: 'Members To Be Added', + coldCredential: { + hash: 'Cold Credential Hash', + epoch: 'Epoch' + } + }, + membersToBeRemoved: { + title: 'Members To Be Removed', + hash: 'Hash' + }, + newQuorumThreshold: { + title: 'New Quorum Threshold', + denominator: 'Denominator', + numerator: 'Numerator' + } + } +}; + +export const Overview: Story = { + args: { + ...data + } +}; + +export const WithError: Story = { + args: { + ...data, + errorMessage: 'Something went wrong' + } +}; diff --git a/packages/core/src/ui/components/ProposalProcedures/UpdateCommitteeAction/UpdateCommitteeActionAction.tsx b/packages/core/src/ui/components/ProposalProcedures/UpdateCommitteeAction/UpdateCommitteeActionAction.tsx new file mode 100644 index 000000000..97237316d --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/UpdateCommitteeAction/UpdateCommitteeActionAction.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { Box, Grid, Flex, Divider, sx, Text, Metadata, Cell } from '@lace/ui'; +import { DappInfo, DappInfoProps } from '../../DappInfo'; +import { ErrorPane } from '@lace/common'; +import * as Types from './UpdateCommitteeActionTypes'; +import { Procedure } from '../components/Procedure'; +import { TransactionDetails } from '../components/TransactionDetails'; +import { ActionId } from '../components/ActionId'; + +interface UpdateCommitteeActionProps { + dappInfo: Omit; + errorMessage?: string; + data: Types.Data; + translations: Types.Translations; +} + +export const UpdateCommitteeAction = ({ + dappInfo, + errorMessage, + data: { procedure, txDetails, membersToBeAdded, membersToBeRemoved, actionId }, + translations +}: UpdateCommitteeActionProps): JSX.Element => { + const textCss = sx({ + color: '$text_primary' + }); + + return ( + + + + + {errorMessage && ( + + + + )} + + {/* tx details section */} + + + + + {/* procedure section */} + + + + + {membersToBeAdded.length > 0 && ( + <> + + + {translations.membersToBeAdded.title} + + + {membersToBeAdded.map(({ coldCredential, epoch }) => ( + + + + + + + + + ))} + + )} + {membersToBeRemoved.length > 0 && ( + <> + + + {translations.membersToBeRemoved.title} + + + {membersToBeRemoved.map(({ hash }) => ( + + + + + + + + + ))} + + )} + {actionId && ( + <> + + + + + + )} + + + ); +}; diff --git a/packages/core/src/ui/components/ProposalProcedures/UpdateCommitteeAction/UpdateCommitteeActionTypes.ts b/packages/core/src/ui/components/ProposalProcedures/UpdateCommitteeAction/UpdateCommitteeActionTypes.ts new file mode 100644 index 000000000..ce8b90745 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/UpdateCommitteeAction/UpdateCommitteeActionTypes.ts @@ -0,0 +1,50 @@ +import * as ProcedureTypes from '../components/ProcedureTypes'; +import * as ActionIdTypes from '../components/ActionIdTypes'; +import * as TxDetailsTypes from '../components/TransactionDetailsTypes'; + +interface MembersToBeAdded { + coldCredential: { + hash: string; + }; + epoch: string; +} + +interface MembersToBeRemoved { + hash: string; +} + +interface NewQuorumThreshold { + denominator: string; + numerator: string; +} + +export interface Data { + actionId?: ActionIdTypes.Data; + txDetails: TxDetailsTypes.TxDetails; + procedure: ProcedureTypes.Procedure; + membersToBeAdded: MembersToBeAdded[]; + membersToBeRemoved: MembersToBeRemoved[]; + newQuorumThreshold?: NewQuorumThreshold; +} + +export interface Translations { + procedure: ProcedureTypes.Translations; + actionId?: ActionIdTypes.Translations; + txDetails: TxDetailsTypes.Translations; + membersToBeAdded: { + title: string; + coldCredential: { + hash: string; + epoch: string; + }; + }; + membersToBeRemoved: { + title: string; + hash: string; + }; + newQuorumThreshold?: { + title: string; + denominator: string; + numerator: string; + }; +} diff --git a/packages/core/src/ui/components/ProposalProcedures/UpdateCommitteeAction/index.ts b/packages/core/src/ui/components/ProposalProcedures/UpdateCommitteeAction/index.ts new file mode 100644 index 000000000..b2b57cd25 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/UpdateCommitteeAction/index.ts @@ -0,0 +1 @@ +export { UpdateCommitteeAction } from './UpdateCommitteeActionAction'; diff --git a/packages/core/src/ui/components/ProposalProcedures/components/ActionId.tsx b/packages/core/src/ui/components/ProposalProcedures/components/ActionId.tsx new file mode 100644 index 000000000..27112d1a5 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/components/ActionId.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Cell, sx, Metadata, Text } from '@lace/ui'; +import * as Types from './ActionIdTypes'; + +interface Props { + data: Types.Data; + translations: Types.Translations; +} + +export const ActionId = ({ data, translations }: Props): JSX.Element => { + const textCss = sx({ + color: '$text_primary' + }); + + return ( + <> + + + {translations.title} + + + + + + + + + + ); +}; diff --git a/packages/core/src/ui/components/ProposalProcedures/components/ActionIdTypes.ts b/packages/core/src/ui/components/ProposalProcedures/components/ActionIdTypes.ts new file mode 100644 index 000000000..3dbb0d882 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/components/ActionIdTypes.ts @@ -0,0 +1,10 @@ +export interface Data { + index: string; + id: string; +} + +export interface Translations { + title?: string; + index: string; + txId: string; +} diff --git a/packages/core/src/ui/components/ProposalProcedures/components/Card.module.scss b/packages/core/src/ui/components/ProposalProcedures/components/Card.module.scss new file mode 100644 index 000000000..bf8fa31ca --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/components/Card.module.scss @@ -0,0 +1,3 @@ +.text { + word-break: break-all; +} diff --git a/packages/core/src/ui/components/ProposalProcedures/components/Card.tsx b/packages/core/src/ui/components/ProposalProcedures/components/Card.tsx new file mode 100644 index 000000000..503ff0ad6 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/components/Card.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { Grid, Flex, Card as UICard, Box, sx, Text, TextLink, Tooltip, Cell } from '@lace/ui'; +import styles from './Card.module.scss'; + +interface Item { + label: string; + value: string; + tooltip?: string; + url?: string; +} + +interface Props { + title?: string; + tooltip?: string; + data: Item[]; +} + +export const Card = ({ title, tooltip, data }: Props): JSX.Element => { + const textCss = sx({ + color: '$text_primary' + }); + + const renderRow = (props: Item) => ( + + + + {props.tooltip ? ( + + + {props.label} + + + ) : ( + + {props.label} + + )} + + + + + {props.url ? ( + + + + ) : ( + + {props.value} + + )} + + + + ); + + const renderTitle = () => { + if (!title) return <>; + + if (tooltip) { + return ( + + + + {title} + + + + ); + } + + return ( + + + {title} + + + ); + }; + + return ( + + + + + {renderTitle()} + + {data.map((props) => renderRow(props))} + + + + + + ); +}; diff --git a/packages/core/src/ui/components/ProposalProcedures/components/Procedure.tsx b/packages/core/src/ui/components/ProposalProcedures/components/Procedure.tsx new file mode 100644 index 000000000..682fcc21f --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/components/Procedure.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { Metadata, MetadataLink, Text, sx, Cell } from '@lace/ui'; +import * as Types from './ProcedureTypes'; + +interface Props { + data: Types.Procedure; + translations: Types.Translations; +} + +export const Procedure = ({ data, translations }: Props): JSX.Element => { + const textCss = sx({ + color: '$text_primary' + }); + + return ( + <> + + + {translations.title} + + + <> + + + + + + + + + ); +}; diff --git a/packages/core/src/ui/components/ProposalProcedures/components/ProcedureTypes.ts b/packages/core/src/ui/components/ProposalProcedures/components/ProcedureTypes.ts new file mode 100644 index 000000000..a1ec2c00a --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/components/ProcedureTypes.ts @@ -0,0 +1,15 @@ +export interface Procedure { + anchor: { + url: string; + hash: string; + txHashUrl: string; + }; +} + +export interface Translations { + title: string; + anchor: { + url: string; + hash: string; + }; +} diff --git a/packages/core/src/ui/components/ProposalProcedures/components/TransactionDetails.tsx b/packages/core/src/ui/components/ProposalProcedures/components/TransactionDetails.tsx new file mode 100644 index 000000000..8a21c6b00 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/components/TransactionDetails.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { Metadata, Text, sx, Cell } from '@lace/ui'; +import * as Types from './TransactionDetailsTypes'; + +interface Props { + data: Types.TxDetails; + translations: Types.Translations; +} + +export const TransactionDetails = ({ data, translations }: Props): JSX.Element => { + const textCss = sx({ + color: '$text_primary' + }); + + return ( + <> + + + {translations.title} + + + + + + {data.rewardAccount && ( + + + + )} + {data.deposit && ( + + + + )} + + ); +}; diff --git a/packages/core/src/ui/components/ProposalProcedures/components/TransactionDetailsTypes.ts b/packages/core/src/ui/components/ProposalProcedures/components/TransactionDetailsTypes.ts new file mode 100644 index 000000000..744b83200 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/components/TransactionDetailsTypes.ts @@ -0,0 +1,12 @@ +export interface TxDetails { + txType: string; + deposit?: string; + rewardAccount?: string; +} + +export interface Translations { + title: string; + txType: string; + deposit?: string; + rewardAccount?: string; +} diff --git a/packages/core/src/ui/components/ProposalProcedures/index.ts b/packages/core/src/ui/components/ProposalProcedures/index.ts new file mode 100644 index 000000000..d8d0bc400 --- /dev/null +++ b/packages/core/src/ui/components/ProposalProcedures/index.ts @@ -0,0 +1,7 @@ +export * from './HardForkInitiationAction'; +export * from './InfoAction'; +export * from './NewConstitutionAction'; +export * from './NoConfidenceAction'; +export * from './ParameterChangeAction'; +export * from './TreasuryWithdrawalsAction'; +export * from './UpdateCommitteeAction'; diff --git a/packages/core/src/ui/components/VotingProcedures/VotingProcedures.stories.tsx b/packages/core/src/ui/components/VotingProcedures/VotingProcedures.stories.tsx new file mode 100644 index 000000000..e2b6308b9 --- /dev/null +++ b/packages/core/src/ui/components/VotingProcedures/VotingProcedures.stories.tsx @@ -0,0 +1,121 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { VotingProcedures } from './VotingProcedures'; +import { ComponentProps } from 'react'; + +const customViewports = { + popup: { + name: 'Popup', + styles: { + width: '360px', + height: '600' + } + } +}; + +const meta: Meta = { + title: 'Sanchonet/Voting/Procedures', + component: VotingProcedures, + parameters: { + layout: 'centered', + viewport: { + viewports: customViewports, + defaultViewport: 'popup' + } + } +}; + +export default meta; +type Story = StoryObj; + +const data: ComponentProps = { + dappInfo: { + logo: 'https://cdn.mint.handle.me/favicon.png', + name: 'Mint', + url: 'https://preprod.mint.handle.me' + }, + data: [ + { + voter: { + type: 'DRep', + dRepId: 'drep1cs234l5mtapethapx8cq97nkpa27xf84phruh5f6jqxa78ymlp4' + }, + votes: [ + { + actionId: { + index: 0, + txHash: '26bfdcc75a7f4d0cd8c71f0189bc5ca5ad2f4a3db6240c82b5a0edac7f9203e0', + txHashUrl: + 'https://cexplorer.io/address/addr1q9wlvfl74g9h8txw5v0lfew2gjsw9z56d5kj8mmv5d8tudcx9eh8zefr3cxuje02lu6tgy083xkl39rr5xkj483vvd6q8nlapq' + }, + votingProcedure: { + anchor: { + hash: '9067f223838d88b83f660c05eedf7f6f65c45de31e522c1bcb6a1eb287b17e89', + url: 'https://shorturl.at/eK145' + }, + vote: 'Yes' + } + } + ] + } + ], + translations: { + voterType: 'Voter Type', + procedureTitle: 'Procedure', + actionIdTitle: 'Action ID', + vote: 'Vote', + actionId: { + index: 'Index', + txHash: 'TX Hash' + }, + anchor: { + hash: 'Anchor Hash', + url: 'Anchor URL' + }, + dRepId: 'DRep ID' + } +}; + +export const Overview: Story = { + args: { + ...data + } +}; +export const WithError: Story = { + args: { + ...data, + errorMessage: 'Something went wrong' + } +}; + +export const MultipleVotes: Story = { + args: { + ...data, + data: [ + ...data.data, + { + voter: { + type: 'DRep', + dRepId: 'drep1cs234l5mtapethapx8cq97nkpa27xf84phruh5f6jqxa78ymlp5' + }, + votes: [ + { + actionId: { + index: 0, + txHash: '26bfdcc75a7f4d0cd8c71f0189bc5ca5ad2f4a3db6240c82b5a0edac7f9203e0', + txHashUrl: + 'https://cexplorer.io/address/addr1q9wlvfl74g9h8txw5v0lfew2gjsw9z56d5kj8mmv5d8tudcx9eh8zefr3cxuje02lu6tgy083xkl39rr5xkj483vvd6q8nlapq' + }, + votingProcedure: { + anchor: { + hash: '9067f223838d88b83f660c05eedf7f6f65c45de31e522c1bcb6a1eb287b17e89', + url: 'https://shorturl.at/eK145' + }, + vote: 'Yes' + } + } + ] + } + ] + } +}; diff --git a/packages/core/src/ui/components/VotingProcedures/VotingProcedures.tsx b/packages/core/src/ui/components/VotingProcedures/VotingProcedures.tsx new file mode 100644 index 000000000..dc40824e7 --- /dev/null +++ b/packages/core/src/ui/components/VotingProcedures/VotingProcedures.tsx @@ -0,0 +1,135 @@ +import React, { Fragment } from 'react'; +import { Box, Cell, Grid, Flex, Metadata, MetadataLink, Text, Divider, sx } from '@lace/ui'; +import { DappInfo, DappInfoProps } from '../DappInfo'; +import { ErrorPane } from '@lace/common'; + +type VotingProcedure = { + voter: { + type: string; + dRepId?: string; + }; + votes: { + actionId: { + index: number; + txHash: string; + txHashUrl?: string; // Dependent on having an explorer to link + }; + votingProcedure: { + vote: string; + anchor: { + url: string; + hash: string; + } | null; + }; + }[]; +}; + +interface Props { + dappInfo: Omit; + errorMessage?: string; + data: VotingProcedure[]; + translations: { + actionIdTitle: string; + actionId: { + index: string; + txHash: string; + }; + anchor: { + url: string; + hash: string; + } | null; + dRepId: string; + procedureTitle: string; + vote: string; + voterType: string; + }; +} + +const indexCounter = (text: string, idx: number, length: number): string => (length > 1 ? `${text} ${idx + 1}` : text); + +export const VotingProcedures = ({ dappInfo, errorMessage, data, translations }: Props): JSX.Element => { + const textCss = sx({ + color: '$text_primary' + }); + + return ( + + + + + {errorMessage && ( + + + + )} + {data.map(({ voter, votes }, idx) => ( + 0 ? '$40' : '$0'}> + + + + {indexCounter(translations.vote, idx, data.length)} + + + + + + {voter.dRepId && ( + + + + )} + + + + {votes.map(({ actionId, votingProcedure }) => ( + + + + {indexCounter(translations.procedureTitle, idx, votes.length)} + + + + + + {votingProcedure.anchor && ( + <> + + + + + + + + )} + + + + + + {indexCounter(translations.actionIdTitle, idx, votes.length)} + + + {actionId.txHashUrl && ( + + + + )} + + + + + ))} + + + ))} + + ); +}; diff --git a/packages/core/src/ui/components/VotingProcedures/index.ts b/packages/core/src/ui/components/VotingProcedures/index.ts new file mode 100644 index 000000000..0bc283f6b --- /dev/null +++ b/packages/core/src/ui/components/VotingProcedures/index.ts @@ -0,0 +1 @@ +export { VotingProcedures } from './VotingProcedures'; diff --git a/packages/core/src/ui/lib/translations/en.json b/packages/core/src/ui/lib/translations/en.json index 7336752ff..5a8c48b60 100644 --- a/packages/core/src/ui/lib/translations/en.json +++ b/packages/core/src/ui/lib/translations/en.json @@ -17,7 +17,40 @@ "incoming": "Received", "outgoing": "Sent", "sending": "Sending", - "self": "Self Transaction" + "self": "Self Transaction", + "vote": "Vote Signing", + "HardForkInitiationAction": "Hard Fork Initiation Action", + "NewConstitution": "New Constitution Action", + "NoConfidence": "No Confidence Action", + "ParameterChangeAction": "Parameter Change Action", + "TreasuryWithdrawalsAction": "Treasury Withdrawals Action", + "UpdateCommittee": "Update Committee Action", + "InfoAction": "Info Action", + "UpdateDelegateRepresentativeCertificate": "Update DRep", + "StakeVoteDelegationCertificate": "Stake Vote Delegation Certificate", + "StakeRegistrationDelegationCertificate": "Stake Registration Delegation Certificate", + "VoteRegistrationDelegationCertificate": "Vote Registration Delegation Certificate", + "StakeVoteRegistrationDelegationCertificate": "Stake Vote Registration Delegation Certificate", + "ResignCommitteeColdCertificate": "Resign Committee", + "AuthorizeCommitteeHotCertificate": "Authorize Committee", + "RegisterDelegateRepresentativeCertificate": "DRep Registration", + "UnregisterDelegateRepresentativeCertificate": "DRep De-Registration", + "VoteDelegationCertificate": "Vote Delegation", + "StakeRegistrationDelegateCertificate": "Stake Registration Delegation Certificate", + "StakeVoteRegistrationDelegateCertificate": "Stake Vote Registration Delegation Certificate", + "VoteRegistrationDelegateCertificate": "Vote Registration Delegation Certificate" + }, + "certificates": { + "headings": { + "certificates": "Certificates", + "typename": "Certificate Type", + "anchor": "Anchor", + "drep": "DRep", + "depositPaid": "Deposit paid", + "coldCredential": "Cold credential", + "hotCredential": "Hot credential", + "drepCredential": "DRep credential" + } } } }, @@ -59,7 +92,7 @@ "sent": "Sent", "sending": "Sending", "header": "Summary of your activity", - "transactionHash": "Transaction Hash", + "transactionID": "Transaction ID", "status": "Status", "timestamp": "Timestamp", "inputs": "Inputs", @@ -78,9 +111,283 @@ "poolId": "Pool ID", "rewards": "Rewards", "rewardsDescription": "When available, your rewards will be withdrawn automatically every time you send Tokens.", + "summary": "Summary", "copiedToClipboard": "Copied to clipboard", "pools": "Pool(s)", - "epoch": "Epoch" + "self": "Self Transaction", + "RegisterDelegateRepresentativeCertificate": "DRep Registration", + "UnregisterDelegateRepresentativeCertificate": "DRep De-Registration", + "UpdateDelegateRepresentativeCertificate": "DRep Update", + "ResignCommitteeColdCertificate": "Resign Committee", + "StakeRegistrationDelegateCertificate": "Stake Key Registration & Delgation", + "StakeVoteDelegationCertificate": "Stake Key Registration & DRep Delegation", + "StakeVoteRegistrationDelegateCertificate": "Stake Key Registration, Delegation, & DRep Delegation", + "VoteRegistrationDelegateCertificate": "Stake Key Registration & DRep Delegation", + "AuthorizeCommitteeHotCertificate": "Authorize Committee", + "VoteDelegationCertificate": "Vote Delegation", + "UpdateCommittee": "Update Committee Action", + "NoConfidence": "No Confidence Action", + "vote": "Vote Signing", + "certificates": "Certificates", + "certificate": "Certificate", + "certificateTitles": { + "certificateType": "Type", + "stakeKey": "Stake Key", + "poolId": "Pool ID", + "drep": "DRep", + "drepId": "DRep ID", + "depositPaid": "Deposit paid", + "depositPaidInfo": "Deposit paid", + "depositReturned": "Deposit returned", + "depositReturnedInfo": "Deposit returned", + "anchorURL": "Anchor URL", + "anchorHash": "Anchor Hash", + "coldCredential": "Cold credential", + "hotCredential": "Hot credential" + }, + "votingProcedures": "Vote", + "votingProcedure": "Vote", + "votingProcedureTitles": { + "vote": "Vote", + "voterType": "Voter Type", + "voterCredential": "Voter Credential", + "drepId": "DRep ID", + "anchorURL": "Anchor URL", + "anchorHash": "Anchor Hash", + "voteTypes": "Vote" + }, + "proposalProcedures": "Action", + "proposalProcedure": "Action", + "proposalProcedureTitles": { + "type": "Type", + "deposit": "Deposit", + "rewardAccount": "Reward Account", + "anchorHash": "Anchor Hash", + "anchorURL": "Anchor URL", + "actionIndex": "Action Index", + "governanceActionID": "Governance Action ID", + "withdrawal": "Withdrawal", + "withdrawalRewardAccount": "Withdrawal Reward Account", + "withdrawalAmount": "Withdrawal Amount", + "constitutionAnchorURL": "Constitution Anchor URL", + "constitutionScriptHash": "Constitution Script Hash", + "coldCredentialHash": "Cold Credential Hash", + "epoch": "Epoch", + "membersToBeAdded": "Members To Be Added", + "hash": "Hash", + "membersToBeRemoved": "Members To Be Removed", + "newQuorumThreshold": "New Quorum Threshold", + "protocolVersionMajor": "Protocol Version Major", + "protocolVersionMinor": "Protocol Version Minor", + "protocolVersionPatch": "Protocol Version Patch" + }, + "governanceActions": { + "info_action": "Info Action", + "hard_fork_initiation_action": "Hard Fork Initiation Action", + "parameter_change_action": "Protocol Parameter Update Action", + "treasury_withdrawals_action": "Treasury Withdrawals Action", + "no_confidence": "No Confidence Action", + "update_committee": "Update Committee Action", + "new_constitution": "Update Constitution Action" + }, + "epoch": "epoch", + "epochs": "epochs", + "voterType": { + "constitutionalCommittee": "Constitutional Committee", + "spo": "SPO", + "drep": "DRep" + }, + "voteTypes": { + "yes": "Yes", + "no": "No", + "abstain": "Abstain" + }, + "credentialType": { + "KeyHash": "Keyhash", + "ScriptHash": "Scripthash" + } + }, + "ProposalProcedure": { + "dRepId": "DRep ID", + "txDetails": { + "deposit": "Deposit", + "rewardAccount": "Reward Account", + "title": "Transaction Details", + "txType": "Transaction Type" + }, + "procedure": { + "anchor": { + "hash": "Anchor Hash", + "url": "Anchor URL" + }, + "title": "Procedure", + "dRepId": "DRep ID" + }, + "governanceAction": { + "actionId": { + "title": "Action ID", + "index": "Index", + "txId": "TX ID" + }, + "hardForkInitiation": { + "title": "Hard Fork Initiation", + "protocolVersion": { + "major": "Protocol Version Major", + "minor": "Protocol Version Minor", + "patch": "Protocol Version Patch" + } + }, + "newConstitutionAction": { + "title": "New Constitution Action", + "constitution": { + "title": "Constitution Details", + "anchor": { + "dataHash": "Anchor Data Hash", + "url": "Constitution Anchor URL" + }, + "scriptHash": "Constitution Script Hash" + } + }, + "infoAction": { + "title": "Info Action" + }, + "noConfidenceAction": { + "title": "No Confidence" + }, + "protocolParamUpdate": { + "title": "Protocol Parameter Update", + "memory": "Memory", + "step": "Step", + "networkGroup": { + "title": "Network Group", + "maxBBSize": "Max BB Size", + "maxTxSize": "Max Tx Size", + "maxBHSize": "Max BH Size", + "maxValSize": "Max Val Size", + "maxTxExUnits": "Max TX Ex Units", + "maxBlockExUnits": "Max BLK Ex Units", + "maxCollateralInputs": "Max Coll Inputs", + "tooltip": { + "maxBBSize": "Max block body size", + "maxTxSize": "Max transaction size", + "maxBHSize": "Max block header size", + "maxValSize": "Max size of a serialized asset value", + "maxTxExUnits": "Max script execution units in a single transaction", + "maxBlockExUnits": "Max script execution units in a single block", + "maxCollateralInputs": "Max number of collateral inputs" + } + }, + "economicGroup": { + "title": "Economic Group", + "minFeeA": "Min Fee A", + "minFeeB": "Min Fee B", + "keyDeposit": "Key Deposit", + "poolDeposit": "Pool Deposit", + "rho": "Rho", + "tau": "Tau", + "minPoolCost": "Min Pool Cost", + "coinsPerUTxOByte": "Coins/UTxO Byte", + "prices": "Price", + "tooltip": { + "minFeeA": "Min fee coefficient", + "minFeeB": "Min fee constant", + "keyDeposit": "Delegation key Lovelace deposit", + "poolDeposit": "Pool registration Lovelace deposit", + "rho": "Monetary expansion", + "tau": "Treasury expansion", + "minPoolCost": "Min fixed rewards cut for pools", + "coinsPerUTxOByte": "Min Lovelace deposit per byte of serialized UTxO", + "prices": "Prices of Plutus execution units" + } + }, + "technicalGroup": { + "title": "Technical Group", + "a0": "A0", + "eMax": "EMax", + "nOpt": "NOpt", + "costModels": "Cost Models", + "PlutusV1": "PlutusV1", + "PlutusV2": "PlutusV2", + "collateralPercentage": "Coll Percentage", + "tooltip": { + "a0": "Pool pledge influence", + "eMax": "Pool retirement maximum epoch", + "nOpt": "Desired number of pools", + "costModels": "Plutus execution cost models", + "collateralPercentage": "Proportion of collateral needed for scripts" + } + }, + "governanceGroup": { + "title": "Governance Group", + "govActionLifetime": "Gov Act Lifetime", + "govActionDeposit": "Gov Act Deposit", + "drepDeposit": "DRep Deposit", + "drepActivity": "DRep Activity", + "ccMinSize": "CC Min Size", + "ccMaxTermLength": "CC Max Term Length", + "dRepVotingThresholds": { + "title": "Governance Voting Thresholds", + "motionNoConfidence": "Motion No Conf", + "committeeNormal": "Comm Normal", + "committeeNoConfidence": "Comm No Conf", + "updateConstitution": "Update Const", + "hardForkInitiation": "Hard Fork Init", + "ppNetworkGroup": "PP Network Grp", + "ppEconomicGroup": "PP Economic Grp", + "ppTechnicalGroup": "PP Technical Grp", + "ppGovernanceGroup": "PP Governance Grp", + "treasuryWithdrawal": "Treasury Withdraw" + }, + "tooltip": { + "govActionLifetime": "governance action maximum lifetime in epochs", + "govActionDeposit": "governance action deposit", + "drepDeposit": "DRep deposit amount", + "drepActivity": "DRep activity period in epochs", + "ccMinSize": "Min constitutional committee size", + "ccMaxTermLength": "Max term length (in epochs) for the constitutional committee members", + "dRepVotingThresholds": { + "title": "Governance voting thresholds", + "motionNoConfidence": "1. Motion of no-confidence", + "committeeNormal": "2a. New committee/threshold (normal state)", + "committeeNoConfidence": "2b. New committee/threshold (state of no-confidence)", + "updateConstitution": "3. Update to the Constitution or proposal policy", + "hardForkInitiation": "4. Hard-fork initiation", + "ppNetworkGroup": "5a. Protocol parameter changes, network group", + "ppEconomicGroup": "5b. Protocol parameter changes, economic group", + "ppTechnicalGroup": "5c. Protocol parameter changes, technical group", + "ppGovernanceGroup": "5d. Protocol parameter changes, governance group", + "treasuryWithdrawal": "6. Treasury withdrawal" + } + } + } + }, + "treasuryWithdrawals": { + "title": "Treasury Withdrawals", + "withdrawals": { + "lovelace": "Withdrawal Amount", + "rewardAccount": "Withdrawal Reward Account" + } + }, + "updateCommitteeAction": { + "title": "Update Committee Action", + "membersToBeAdded": { + "title": "Members To Be Added", + "coldCredential": { + "hash": "Cold Credential Hash", + "epoch": "Epoch" + } + }, + "membersToBeRemoved": { + "title": "Members To Be Removed", + "hash": "Hash" + }, + "newQuorumThreshold": { + "title": "New Quorum Threshold", + "denominator": "Denominator", + "numerator": "Numerator" + } + } + } }, "authorizeDapp": { "title": "Allow this site to", diff --git a/packages/e2e-tests/src/assert/dAppConnectorAssert.ts b/packages/e2e-tests/src/assert/dAppConnectorAssert.ts index 734c93fb4..3d388b5fb 100644 --- a/packages/e2e-tests/src/assert/dAppConnectorAssert.ts +++ b/packages/e2e-tests/src/assert/dAppConnectorAssert.ts @@ -171,7 +171,7 @@ class DAppConnectorAssert { expect(await NoWalletModal.description.getText()).to.equal(await t('dapp.noWallet.description')); await NoWalletModal.createRestoreButton.waitForDisplayed(); - expect(await NoWalletModal.createRestoreButton.getText()).to.equal(await t('dapp.nowallet.btn')); + expect(await NoWalletModal.createRestoreButton.getText()).to.equal(await t('dapp.noWallet.closeButton')); } async assertSeeDAppRemovalConfirmationModal() { diff --git a/packages/staking/src/features/DelegationCard/DelegationTooltip.css.ts b/packages/staking/src/features/DelegationCard/DelegationTooltip.css.ts new file mode 100644 index 000000000..9a37418d1 --- /dev/null +++ b/packages/staking/src/features/DelegationCard/DelegationTooltip.css.ts @@ -0,0 +1,15 @@ +import { style, sx } from '@lace/ui'; +import { theme } from '../theme'; + +export const tooltip = style([ + sx({ + borderRadius: '$small', + boxShadow: '$tooltip', + margin: '$10', + maxWidth: '$214', + padding: '$16', + }), + { + background: theme.colors.$tooltipBgColor, + }, +]); diff --git a/packages/staking/src/features/Drawer/confirmation/AmountInfo.tsx b/packages/staking/src/features/Drawer/confirmation/AmountInfo.tsx index 05f556c28..0f160a217 100644 --- a/packages/staking/src/features/Drawer/confirmation/AmountInfo.tsx +++ b/packages/staking/src/features/Drawer/confirmation/AmountInfo.tsx @@ -17,7 +17,13 @@ export const AmountInfo = ({ }) => (
{renderAmountInfo( - [sign ? `${sign} ` : '', `${Wallet.util.lovelacesToAdaString(amount)} ${cardanoCoin.symbol}`].join(''), + [ + sign || '', + Wallet.util.getFormattedAmount({ + amount, + cardanoCoin, + }), + ].join(''), `${Wallet.util.convertAdaToFiat({ ada: Wallet.util.lovelacesToAdaString(amount.toString()), fiat: cardanoFiatPrice, diff --git a/packages/staking/src/features/overview/Overview.tsx b/packages/staking/src/features/overview/Overview.tsx index d6e603b7d..e37151da8 100644 --- a/packages/staking/src/features/overview/Overview.tsx +++ b/packages/staking/src/features/overview/Overview.tsx @@ -55,7 +55,7 @@ export const Overview = () => { if ( !totalCoinBalance || - !protocolParameters?.stakeKeyDeposit || + !protocolParameters?.hasOwnProperty('stakeKeyDeposit') || !balancesBalance?.available?.coinBalance || !rewardAccounts ) { diff --git a/packages/staking/src/features/overview/OverviewPopup.tsx b/packages/staking/src/features/overview/OverviewPopup.tsx index d0e9c8b54..72e3713ad 100644 --- a/packages/staking/src/features/overview/OverviewPopup.tsx +++ b/packages/staking/src/features/overview/OverviewPopup.tsx @@ -36,7 +36,7 @@ export const OverviewPopup = () => { if ( !totalCoinBalance || - !protocolParameters?.stakeKeyDeposit || + !protocolParameters?.hasOwnProperty('stakeKeyDeposit') || !balancesBalance?.available?.coinBalance || !rewardAccounts ) { diff --git a/packages/staking/src/features/overview/helpers/hasPendingDelegationTransaction.ts b/packages/staking/src/features/overview/helpers/hasPendingDelegationTransaction.ts index 509a8073d..540e3cca6 100644 --- a/packages/staking/src/features/overview/helpers/hasPendingDelegationTransaction.ts +++ b/packages/staking/src/features/overview/helpers/hasPendingDelegationTransaction.ts @@ -1,13 +1,7 @@ +import { ActivityStatus, AssetActivityListProps, DelegationActivityType } from '@lace/core'; import flatMap from 'lodash/flatMap'; -import type { ActivityType, AssetActivityListProps } from '@lace/core'; - -const DelegationTransactionTypes: Set = new Set([ - 'delegation', - 'delegationRegistration', - 'delegationDeregistration', -]); export const hasPendingDelegationTransaction = (walletActivities: AssetActivityListProps[]) => flatMap(walletActivities, ({ items }) => items).some( - ({ type, status }) => type && DelegationTransactionTypes.has(type) && status === 'sending' + ({ type, status }) => type && type in DelegationActivityType && status === ActivityStatus.PENDING ); diff --git a/packages/ui/package.json b/packages/ui/package.json index b9ce66a8d..df2522084 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -29,6 +29,18 @@ "typecheck": "tsc --noEmit", "watch": "yarn build --watch" }, + "resolutions": { + "@storybook/addon-actions": "^6.5.16", + "@storybook/addon-essentials": "^6.5.16", + "@storybook/addon-interactions": "^6.5.16", + "@storybook/addon-links": "^6.5.16", + "@storybook/builder-webpack5": "6.5.16", + "@storybook/jest": "^0.0.10", + "@storybook/manager-webpack5": "6.5.16", + "@storybook/react": "^6.5.16", + "@storybook/test-runner": "^0.10.0", + "@storybook/testing-library": "^0.0.13" + }, "dependencies": { "@lace/icons": "^0.1.0", "@radix-ui/react-alert-dialog": "^1.0.4", diff --git a/packages/ui/src/design-system/index.ts b/packages/ui/src/design-system/index.ts index a26e17f61..6f05636a5 100644 --- a/packages/ui/src/design-system/index.ts +++ b/packages/ui/src/design-system/index.ts @@ -23,8 +23,8 @@ export * as TransactionSummary from './transaction-summary'; export { ToastBar } from './toast-bar'; export * from './tooltip'; export { Message } from './message'; +export { Metadata, MetadataLink } from './metadata'; export { PasswordBox } from './password-box'; -export { Metadata } from './metadata'; export { TextLink } from './text-link'; export * as ProfileDropdown from './profile-dropdown'; export { TextBox } from './text-box'; diff --git a/packages/ui/src/design-system/metadata/index.ts b/packages/ui/src/design-system/metadata/index.ts index 6060859e4..d97b0957f 100644 --- a/packages/ui/src/design-system/metadata/index.ts +++ b/packages/ui/src/design-system/metadata/index.ts @@ -1 +1,2 @@ export { Metadata } from './metadata.component'; +export { MetadataLink } from './metadata-link.component'; diff --git a/packages/ui/src/design-system/metadata/metadata-link.component.tsx b/packages/ui/src/design-system/metadata/metadata-link.component.tsx new file mode 100644 index 000000000..805365668 --- /dev/null +++ b/packages/ui/src/design-system/metadata/metadata-link.component.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +import { TextLink } from '../text-link'; + +import { MetadataBase } from './metadata.base'; + +import type { Props as BaseProps } from './metadata.base'; + +type Props = BaseProps & { + text: string; + url: string; +}; + +export const MetadataLink = ({ + url, + text, + ...props +}: Readonly): JSX.Element => { + return ( + + + + + + ); +}; diff --git a/packages/ui/src/design-system/metadata/metadata.base.tsx b/packages/ui/src/design-system/metadata/metadata.base.tsx new file mode 100644 index 000000000..fb9dbd65f --- /dev/null +++ b/packages/ui/src/design-system/metadata/metadata.base.tsx @@ -0,0 +1,47 @@ +import React from 'react'; + +import { Flex } from '../flex'; +import { Grid, Cell } from '../grid'; +import { Tooltip } from '../tooltip'; +import * as Typography from '../typography'; + +import * as cx from './metadata.css'; + +import type { OmitClassName } from '../../types'; + +export type Props = OmitClassName<'div'> & { + label: string; + tooltip?: string; +}; + +export const MetadataBase = ({ + label, + children, + tooltip, + ...props +}: Readonly): JSX.Element => { + return ( + + + {tooltip == undefined ? ( + + {label} + + ) : ( + +
+ + {label} + +
+
+ )} +
+ + + {children} + + +
+ ); +}; diff --git a/packages/ui/src/design-system/metadata/metadata.component.tsx b/packages/ui/src/design-system/metadata/metadata.component.tsx index f9d9fd0aa..f456c4b98 100644 --- a/packages/ui/src/design-system/metadata/metadata.component.tsx +++ b/packages/ui/src/design-system/metadata/metadata.component.tsx @@ -1,35 +1,20 @@ import React from 'react'; -import { Flex } from '../flex'; -import { Grid, Cell } from '../grid'; import * as Typography from '../typography'; +import { MetadataBase } from './metadata.base'; import * as cx from './metadata.css'; -import type { OmitClassName } from '../../types'; +import type { Props as BaseProps } from './metadata.base'; -type Props = OmitClassName<'div'> & { - label: string; +type Props = BaseProps & { text: string; }; -export const Metadata = ({ - label, - text, - ...props -}: Readonly): JSX.Element => { +export const Metadata = ({ text, ...props }: Readonly): JSX.Element => { return ( - - - - {label} - - - - - {text} - - - + + {text} + ); }; diff --git a/packages/ui/src/design-system/metadata/metadata.stories.tsx b/packages/ui/src/design-system/metadata/metadata.stories.tsx index a504ca41a..d91634f4f 100644 --- a/packages/ui/src/design-system/metadata/metadata.stories.tsx +++ b/packages/ui/src/design-system/metadata/metadata.stories.tsx @@ -42,6 +42,7 @@ const MainComponents = (): JSX.Element => ( diff --git a/packages/ui/src/design-system/text-link/text-link.css.ts b/packages/ui/src/design-system/text-link/text-link.css.ts index 010a970b8..681ccc4f6 100644 --- a/packages/ui/src/design-system/text-link/text-link.css.ts +++ b/packages/ui/src/design-system/text-link/text-link.css.ts @@ -48,6 +48,7 @@ export const label = style([ color: vars.colors.$text_link_label_color_disabled, }, }, + wordBreak: 'break-all', }, ]); diff --git a/packages/ui/src/design-system/tooltip/tooltip-content.css.ts b/packages/ui/src/design-system/tooltip/tooltip-content.css.ts index 29d7cd699..803e3c84e 100644 --- a/packages/ui/src/design-system/tooltip/tooltip-content.css.ts +++ b/packages/ui/src/design-system/tooltip/tooltip-content.css.ts @@ -15,5 +15,6 @@ export const tooltipContent = style([ }), { position: 'relative', + wordBreak: 'break-word', }, ]); diff --git a/packages/ui/src/design-system/transaction-summary/transaction-summary-address.component.tsx b/packages/ui/src/design-system/transaction-summary/transaction-summary-address.component.tsx index 83234cfa0..087402100 100644 --- a/packages/ui/src/design-system/transaction-summary/transaction-summary-address.component.tsx +++ b/packages/ui/src/design-system/transaction-summary/transaction-summary-address.component.tsx @@ -11,23 +11,33 @@ import type { OmitClassName } from '../../types'; type Props = OmitClassName<'div'> & { label: string; address: string; + testID?: string; }; export const Address = ({ label, address, + testID, ...props }: Readonly): JSX.Element => { return ( - + {label} - {address} + + {address} + diff --git a/packages/ui/src/design-system/transaction-summary/transaction-summary-metadata.component.tsx b/packages/ui/src/design-system/transaction-summary/transaction-summary-metadata.component.tsx index 82d4d0c05..ccde339e4 100644 --- a/packages/ui/src/design-system/transaction-summary/transaction-summary-metadata.component.tsx +++ b/packages/ui/src/design-system/transaction-summary/transaction-summary-metadata.component.tsx @@ -11,23 +11,32 @@ import type { OmitClassName } from '../../types'; type Props = OmitClassName<'div'> & { label: string; text: string; + testID?: string; }; export const Metadata = ({ label, text, + testID, ...props }: Readonly): JSX.Element => { return ( - + {label} - + {text} diff --git a/rollup.config.js b/rollup.config.js index d2ef16661..aef7832d8 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -11,7 +11,7 @@ export default ({ tsPluginOptions = { tsconfig: 'src/tsconfig.json', composite: false, - exclude: ['**/*.stories.tsx'] + exclude: ['**/*.stories.tsx', '**/*.test.ts', '**/*.test.tsx'] } } = {}) => ({ input: 'src/index.ts', diff --git a/yarn.lock b/yarn.lock index 13e8e4c34..c7a293ab7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10747,7 +10747,7 @@ __metadata: "@storybook/testing-library": ^0.0.13 "@types/babel__preset-env": ^7 "@types/debounce-promise": ^3.1.6 - "@types/uuid": ^9 + "@types/uuid": ^8 antd: ^4.24.10 axios: 0.21.4 axios-cache-adapter: 2.7.3 @@ -10763,8 +10763,9 @@ __metadata: react-infinite-scroll-component: ^6.1.0 sass: ^1.68.0 storybook: ^7.4.3 + tsconfig-paths-webpack-plugin: 3.5.2 typescript: ^4.3.5 - uuid: ^9.0.1 + uuid: ^8.3.2 zxcvbn: ^4.4.2 peerDependencies: react: 17.0.2 @@ -20146,7 +20147,7 @@ __metadata: languageName: node linkType: hard -"@types/uuid@npm:8.3.4, @types/uuid@npm:^8.3.4": +"@types/uuid@npm:8.3.4, @types/uuid@npm:^8, @types/uuid@npm:^8.3.4": version: 8.3.4 resolution: "@types/uuid@npm:8.3.4" checksum: 6f11f3ff70f30210edaa8071422d405e9c1d4e53abbe50fdce365150d3c698fe7bbff65c1e71ae080cbfb8fded860dbb5e174da96fdbbdfcaa3fb3daa474d20f @@ -20167,13 +20168,6 @@ __metadata: languageName: node linkType: hard -"@types/uuid@npm:^9": - version: 9.0.8 - resolution: "@types/uuid@npm:9.0.8" - checksum: b8c60b7ba8250356b5088302583d1704a4e1a13558d143c549c408bf8920535602ffc12394ede77f8a8083511b023704bc66d1345792714002bfa261b17c5275 - languageName: node - linkType: hard - "@types/validator@npm:^13.7.10": version: 13.7.12 resolution: "@types/validator@npm:13.7.12" @@ -26312,7 +26306,7 @@ __metadata: languageName: node linkType: hard -"console-browserify@npm:^1.1.0": +"console-browserify@npm:^1.1.0, console-browserify@npm:^1.2.0": version: 1.2.0 resolution: "console-browserify@npm:1.2.0" checksum: 226591eeff8ed68e451dffb924c1fb750c654d54b9059b3b261d360f369d1f8f70650adecf2c7136656236a4bfeb55c39281b5d8a55d792ebbb99efd3d848d52 @@ -37688,6 +37682,7 @@ __metadata: madr-tools: ^1.0.0 markdownlint-cli: ^0.31.1 node-gyp: ^9.4.0 + node-polyfill-webpack-plugin: ^3.0.0 node-sass: ^7.0.1 normalize.css: ^8.0.1 p-retry: 5.1.2 @@ -40483,6 +40478,40 @@ __metadata: languageName: node linkType: hard +"node-polyfill-webpack-plugin@npm:^3.0.0": + version: 3.0.0 + resolution: "node-polyfill-webpack-plugin@npm:3.0.0" + dependencies: + assert: ^2.1.0 + browserify-zlib: ^0.2.0 + buffer: ^6.0.3 + console-browserify: ^1.2.0 + constants-browserify: ^1.0.0 + crypto-browserify: ^3.12.0 + domain-browser: ^4.22.0 + events: ^3.3.0 + https-browserify: ^1.0.0 + os-browserify: ^0.3.0 + path-browserify: ^1.0.1 + process: ^0.11.10 + punycode: ^2.3.0 + querystring-es3: ^0.2.1 + readable-stream: ^4.4.2 + stream-browserify: ^3.0.0 + stream-http: ^3.2.0 + string_decoder: ^1.3.0 + timers-browserify: ^2.0.12 + tty-browserify: ^0.0.1 + type-fest: ^4.4.0 + url: ^0.11.3 + util: ^0.12.5 + vm-browserify: ^1.1.2 + peerDependencies: + webpack: ">=5" + checksum: 44d5ae098587d58dd42f011b376b113ae49ea0dbcd92b6f7d2f16946e93ed09c09c0bb46bd4c2473f324bc5872516d33aacb5eafaed24d2657c48bb96265fd6c + languageName: node + linkType: hard + "node-preload@npm:^0.2.1": version: 0.2.1 resolution: "node-preload@npm:0.2.1" @@ -43727,6 +43756,13 @@ __metadata: languageName: node linkType: hard +"punycode@npm:^2.3.0": + version: 2.3.1 + resolution: "punycode@npm:2.3.1" + checksum: bb0a0ceedca4c3c57a9b981b90601579058903c62be23c5e8e843d2c2d4148a3ecf029d5133486fb0e1822b098ba8bba09e89d6b21742d02fa26bda6441a6fb2 + languageName: node + linkType: hard + "pupa@npm:^2.0.1": version: 2.1.1 resolution: "pupa@npm:2.1.1" @@ -43892,6 +43928,15 @@ __metadata: languageName: node linkType: hard +"qs@npm:^6.11.2": + version: 6.11.2 + resolution: "qs@npm:6.11.2" + dependencies: + side-channel: ^1.0.4 + checksum: e812f3c590b2262548647d62f1637b6989cc56656dc960b893fe2098d96e1bd633f36576f4cd7564dfbff9db42e17775884db96d846bebe4f37420d073ecdc0b + languageName: node + linkType: hard + "qs@npm:^6.4.0": version: 6.11.1 resolution: "qs@npm:6.11.1" @@ -45786,6 +45831,19 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:^4.4.2": + version: 4.5.2 + resolution: "readable-stream@npm:4.5.2" + dependencies: + abort-controller: ^3.0.0 + buffer: ^6.0.3 + events: ^3.3.0 + process: ^0.11.10 + string_decoder: ^1.3.0 + checksum: c4030ccff010b83e4f33289c535f7830190773e274b3fcb6e2541475070bdfd69c98001c3b0cb78763fc00c8b62f514d96c2b10a8bd35d5ce45203a25fa1d33a + languageName: node + linkType: hard + "readable-stream@npm:~0.0.2": version: 0.0.4 resolution: "readable-stream@npm:0.0.4" @@ -49088,7 +49146,7 @@ __metadata: languageName: node linkType: hard -"string_decoder@npm:^1.0.0, string_decoder@npm:^1.1.1": +"string_decoder@npm:^1.0.0, string_decoder@npm:^1.1.1, string_decoder@npm:^1.3.0": version: 1.3.0 resolution: "string_decoder@npm:1.3.0" dependencies: @@ -50168,7 +50226,7 @@ __metadata: languageName: node linkType: hard -"timers-browserify@npm:^2.0.4": +"timers-browserify@npm:^2.0.12, timers-browserify@npm:^2.0.4": version: 2.0.12 resolution: "timers-browserify@npm:2.0.12" dependencies: @@ -50788,7 +50846,7 @@ __metadata: languageName: node linkType: hard -"tty-browserify@npm:0.0.1": +"tty-browserify@npm:0.0.1, tty-browserify@npm:^0.0.1": version: 0.0.1 resolution: "tty-browserify@npm:0.0.1" checksum: 93b745d43fa5a7d2b948fa23be8d313576d1d884b48acd957c07710bac1c0d8ac34c0556ad4c57c73d36e11741763ef66b3fb4fb97b06b7e4d525315a3cd45f5 @@ -50930,6 +50988,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^4.4.0": + version: 4.10.3 + resolution: "type-fest@npm:4.10.3" + checksum: 37d265d584a6587253fe4ab2ed25ef787bb0650fe1924319f68ac3197bfaf3142b304a72d6499d61b60c38c9aba7e1e8f5e940df0898c974a8e6c4a37339b64e + languageName: node + linkType: hard + "type-is@npm:~1.6.17, type-is@npm:~1.6.18": version: 1.6.18 resolution: "type-is@npm:1.6.18" @@ -51661,6 +51726,16 @@ __metadata: languageName: node linkType: hard +"url@npm:^0.11.3": + version: 0.11.3 + resolution: "url@npm:0.11.3" + dependencies: + punycode: ^1.4.1 + qs: ^6.11.2 + checksum: f9e7886f46a16f96d2e42fbcc5d682c231c55ef5442c1ff66150c0f6556f6e3a97d094a84f51be15ec2432711d212eb60426659ce418f5fcadeaa3f601532c4e + languageName: node + linkType: hard + "urlpattern-polyfill@npm:10.0.0": version: 10.0.0 resolution: "urlpattern-polyfill@npm:10.0.0" @@ -51901,7 +51976,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:9.0.1, uuid@npm:^9.0.1": +"uuid@npm:9.0.1": version: 9.0.1 resolution: "uuid@npm:9.0.1" bin: @@ -52339,7 +52414,7 @@ __metadata: languageName: node linkType: hard -"vm-browserify@npm:^1.0.1": +"vm-browserify@npm:^1.0.1, vm-browserify@npm:^1.1.2": version: 1.1.2 resolution: "vm-browserify@npm:1.1.2" checksum: 10a1c50aab54ff8b4c9042c15fc64aefccce8d2fb90c0640403242db0ee7fb269f9b102bdb69cfb435d7ef3180d61fd4fb004a043a12709abaf9056cfd7e039d