diff --git a/.github/scripts/generate_latest_report_redirect.sh b/.github/scripts/generate_latest_report_redirect.sh index 688e61239..acf5187d7 100644 --- a/.github/scripts/generate_latest_report_redirect.sh +++ b/.github/scripts/generate_latest_report_redirect.sh @@ -11,7 +11,7 @@ cat < build/index.html - + Redirecting... diff --git a/.github/scripts/generate_report_details.sh b/.github/scripts/generate_report_details.sh index bf8131959..dff702fb2 100644 --- a/.github/scripts/generate_report_details.sh +++ b/.github/scripts/generate_report_details.sh @@ -1,12 +1,12 @@ #!/bin/bash -if [[ ! -d "gh-pages/$REPORT_NAME" ]]; then +if [[ ! -d "gh-pages/$GROUP_NAME/$REPORT_NAME" ]]; then latest_number=0 else - gh_pages_content=$(ls "gh-pages/$REPORT_NAME/") + gh_pages_content=$(ls "gh-pages/$GROUP_NAME/$REPORT_NAME/") latest_number=$(echo "$gh_pages_content" | grep -Eo '[0-9]+' | sort -nr | head -n 1) fi echo "report_number=$((latest_number+1))" >> $GITHUB_OUTPUT -echo "report_url=https://$(dirname "$GH_PAGES").github.io/$(basename "$GH_PAGES")/$REPORT_NAME" >> $GITHUB_OUTPUT +echo "report_url=https://$(dirname "$GH_PAGES").github.io/$(basename "$GH_PAGES")/$GROUP_NAME/$REPORT_NAME" >> $GITHUB_OUTPUT diff --git a/.github/scripts/register_report.sh b/.github/scripts/register_report.sh deleted file mode 100644 index 959368afb..000000000 --- a/.github/scripts/register_report.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -PROJECT_DIR="project" -PROJECT_FILE="projects.txt" - -mkdir -p "$PROJECT_DIR" -cp -r gh-pages/* "$PROJECT_DIR" || true - -if grep -q "$REPORT_NAME" "$PROJECT_DIR/$PROJECT_FILE"; then - echo "Project already exists" - echo "project_exists=true">> $GITHUB_OUTPUT -else - echo "\n$REPORT_NAME" >> "$PROJECT_DIR/$PROJECT_FILE" - echo "project_exists=false">> $GITHUB_OUTPUT -fi diff --git a/.github/scripts/remove_oldest_report.sh b/.github/scripts/remove_oldest_report.sh index e0a809604..208f0d9a9 100644 --- a/.github/scripts/remove_oldest_report.sh +++ b/.github/scripts/remove_oldest_report.sh @@ -1,7 +1,7 @@ #!/bin/bash -if [ -d "gh-pages/$REPORT_NAME" ]; then - cd gh-pages/$REPORT_NAME +if [ -d "gh-pages/$GROUP_NAME/$REPORT_NAME" ]; then + cd gh-pages/$GROUP_NAME/$REPORT_NAME # Count the number of numerical directories dir_count=$(find . -maxdepth 1 -type d -regex './[0-9]+' | wc -l) diff --git a/.github/scripts/set_commit_status.sh b/.github/scripts/set_commit_status.sh index f22c4e885..fec4e145b 100644 --- a/.github/scripts/set_commit_status.sh +++ b/.github/scripts/set_commit_status.sh @@ -48,9 +48,9 @@ fi # Determine target URL based on environment case "$GH_PAGES" in - "IntersectMBO/govtool-test-reports") TARGET_URL="https://intersectmbo.github.io/govtool-test-reports/${REPORT_NAME}/${REPORT_NUMBER}" ;; - "cardanoapi/govtool-test-reports") TARGET_URL="https://cardanoapi.github.io/govtool-test-reports/${REPORT_NAME}/${REPORT_NUMBER}" ;; - *) TARGET_URL="https://intersectmbo.github.io/govtool-test-reports/${REPORT_NAME}/${REPORT_NUMBER}" ;; + "IntersectMBO/govtool-test-reports") TARGET_URL="https://intersectmbo.github.io/govtool-test-reports/${GROUP_NAME}/${REPORT_NAME}/${REPORT_NUMBER}" ;; + "cardanoapi/govtool-test-reports") TARGET_URL="https://cardanoapi.github.io/govtool-test-reports/${GROUP_NAME}/${REPORT_NAME}/${REPORT_NUMBER}" ;; + *) TARGET_URL="https://intersectmbo.github.io/govtool-test-reports/${GROUP_NAME}/${REPORT_NAME}/${REPORT_NUMBER}" ;; esac # Determine test result message diff --git a/.github/scripts/set_deployment_environment.sh b/.github/scripts/set_deployment_environment.sh new file mode 100644 index 000000000..8c9495a57 --- /dev/null +++ b/.github/scripts/set_deployment_environment.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +DEPLOYMENT=${DEPLOYMENT:-"govtool.cardanoapi.io/api"} +GROUP_NAME="qa" + +if [[ "$DEPLOYMENT" == "preview.gov.tools" || "$DEPLOYMENT" == "be.preview.gov.tools" || "$DEPLOYMENT" == "z6b8d2f7a-zca4a4c45-gtw.z937eb260.rustrocks.fr" ]]; then + GROUP_NAME="preview" +elif [[ "$DEPLOYMENT" == "gov.tools" || "$DEPLOYMENT" == "be.gov.tools" ]]; then + GROUP_NAME="mainnet" +elif [[ "$DEPLOYMENT" == "p80-z78acf3c2-zded6a792-gtw.z937eb260.rustrocks.fr" || "$DEPLOYMENT" == "z78acf3c2-z5575152b-gtw.z937eb260.rustrocks.fr" ]]; then + GROUP_NAME="dev" +else + GROUP_NAME="qa" +fi + +# Set environment variable for GitHub Actions +echo "GROUP_NAME=${GROUP_NAME}" >>$GITHUB_ENV +echo "group_name=${GROUP_NAME}" >>$GITHUB_OUTPUT +echo "Setting deployment environment to: ${GROUP_NAME}" diff --git a/.github/workflows/test_backend.yml b/.github/workflows/test_backend.yml index 74b9ca195..a871cef71 100644 --- a/.github/workflows/test_backend.yml +++ b/.github/workflows/test_backend.yml @@ -33,7 +33,7 @@ on: workflow_run: workflows: ["Build and deploy GovTool test stack"] types: [completed] - branches: + branches: - test - infra/test-chores @@ -59,6 +59,7 @@ jobs: - name: Set pending commit status id: set-pending-status + if: ${{ !github.event.schedule }} run: | echo "timestamp=$(date +%s)" >> $GITHUB_OUTPUT curl -X POST -H "Authorization: Bearer ${{ github.token }}" \ @@ -104,6 +105,7 @@ jobs: needs: backend-tests outputs: report_number: ${{ steps.report-details.outputs.report_number }} + group_name: ${{ steps.set-deployment-url.outputs.group_name }} steps: - uses: actions/checkout@v4 - name: Download results @@ -121,27 +123,18 @@ jobs: repository: ${{vars.GH_PAGES}} ssh-key: ${{ secrets.DEPLOY_KEY }} + - name: Set Deployment Environment + id: set-deployment-url + run: | + chmod +x .github/scripts/set_deployment_environment.sh + .github/scripts/set_deployment_environment.sh + - name: Remove oldest report to save space if: ${{success()}} run: | chmod +x .github/scripts/remove_oldest_report.sh .github/scripts/remove_oldest_report.sh - - name: Register report - id: register-project - if: ${{success()}} - run: | - chmod +x .github/scripts/register_report.sh - .github/scripts/register_report.sh - - - if: steps.register-project.outputs.project_exists != 'true' - uses: JamesIves/github-pages-deploy-action@v4 - with: - ssh-key: ${{ secrets.DEPLOY_KEY }} - repository-name: ${{vars.GH_PAGES}} - branch: gh-pages - folder: project - - name: Generate report details id: report-details run: | @@ -153,7 +146,7 @@ jobs: id: allure-report with: allure_results: allure-results - gh_pages: gh-pages/${{env.REPORT_NAME}} + gh_pages: gh-pages/${{steps.set-deployment-url.outputs.group_name}}/${{env.REPORT_NAME}} allure_report: allure-report allure_history: allure-history keep_reports: 2000 @@ -172,7 +165,7 @@ jobs: repository-name: ${{vars.GH_PAGES}} branch: gh-pages folder: build - target-folder: ${{ env.REPORT_NAME }} + target-folder: ${{steps.set-deployment-url.outputs.group_name}}/${{ env.REPORT_NAME }} publish-status: runs-on: ubuntu-latest @@ -185,8 +178,9 @@ jobs: with: name: allure-results path: allure-results + - name: Set Commit Status - if: always() + if: always() && !github.event.schedule run: | chmod +x .github/scripts/set_commit_status.sh .github/scripts/set_commit_status.sh @@ -195,9 +189,11 @@ jobs: TEST_STATUS: ${{ needs.backend-tests.outputs.status }} REPORT_NUMBER: ${{ needs.publish-report.outputs.report_number }} GITHUB_TOKEN: ${{ github.token }} + GROUP_NAME: ${{ needs.publish-report.outputs.group_name }} env: BASE_URL: https://${{github.event.schedule && 'be.preview.gov.tools' || inputs.deployment || 'govtool.cardanoapi.io/api' }} + DEPLOYMENT: ${{ github.event.schedule && 'be.preview.gov.tools' || inputs.deployment || 'govtool.cardanoapi.io/api'}} REPORT_NAME: ${{ github.event.schedule && 'nightly-'}}govtool-backend GH_PAGES: ${{vars.GH_PAGES}} COMMIT_SHA: ${{ github.event.workflow_run.head_sha || github.sha }} diff --git a/.github/workflows/test_integration_playwright.yml b/.github/workflows/test_integration_playwright.yml index 841357529..879b6bbab 100644 --- a/.github/workflows/test_integration_playwright.yml +++ b/.github/workflows/test_integration_playwright.yml @@ -59,6 +59,7 @@ jobs: with: ref: ${{ env.COMMIT_SHA }} - name: Set pending commit status + if: ${{ !github.event.schedule }} id: set-pending-status run: | echo "timestamp=$(date +%s)" >> $GITHUB_OUTPUT @@ -136,7 +137,7 @@ jobs: FAUCET_ADDRESS: ${{vars.FAUCET_ADDRESS}} CARDANOAPI_METADATA_URL: ${{vars.CARDANOAPI_METADATA_URL}} FAUCET_PAYMENT_PRIVATE: ${{secrets.FAUCET_PAYMENT_PRIVATE}} - FAUCET_STAKE_PKH: ${{secrets.FAUCET_STAKE_PKH}} + FAUCET_STAKE_PRIVATE: ${{secrets.FAUCET_STAKE_PRIVATE}} publish-report: runs-on: ubuntu-latest @@ -144,6 +145,7 @@ jobs: needs: integration-tests outputs: report_number: ${{ steps.report-details.outputs.report_number }} + group_name: ${{ steps.set-deployment-url.outputs.group_name }} steps: - uses: actions/checkout@v4 - name: Download report @@ -161,27 +163,18 @@ jobs: repository: ${{vars.GH_PAGES}} ssh-key: ${{ secrets.DEPLOY_KEY }} + - name: Set Deployment Environment + id: set-deployment-url + run: | + chmod +x .github/scripts/set_deployment_environment.sh + .github/scripts/set_deployment_environment.sh + - name: Remove oldest report to save space if: ${{success()}} run: | chmod +x .github/scripts/remove_oldest_report.sh .github/scripts/remove_oldest_report.sh - - name: Register report - id: register-project - if: ${{success()}} - run: | - chmod +x .github/scripts/register_report.sh - .github/scripts/register_report.sh - - - if: steps.register-project.outputs.project_exists != 'true' - uses: JamesIves/github-pages-deploy-action@v4 - with: - ssh-key: ${{ secrets.DEPLOY_KEY }} - repository-name: ${{vars.GH_PAGES}} - branch: gh-pages - folder: project - - name: Generate report details id: report-details run: | @@ -194,7 +187,7 @@ jobs: id: allure-report with: allure_results: allure-results - gh_pages: gh-pages/${{env.REPORT_NAME}} + gh_pages: gh-pages/${{steps.set-deployment-url.outputs.group_name}}/${{env.REPORT_NAME}} allure_report: allure-report allure_history: allure-history keep_reports: 2000 @@ -213,7 +206,7 @@ jobs: repository-name: ${{vars.GH_PAGES}} branch: gh-pages folder: build - target-folder: ${{ env.REPORT_NAME }} + target-folder: ${{steps.set-deployment-url.outputs.group_name}}/${{ env.REPORT_NAME }} publish-status: runs-on: ubuntu-latest @@ -226,8 +219,9 @@ jobs: with: name: allure-results path: allure-results + - name: Set Commit Status - if: always() + if: always() && !github.event.schedule run: | chmod +x .github/scripts/set_commit_status.sh .github/scripts/set_commit_status.sh @@ -236,8 +230,10 @@ jobs: TEST_STATUS: ${{ needs.integration-tests.outputs.status }} REPORT_NUMBER: ${{ needs.publish-report.outputs.report_number }} GITHUB_TOKEN: ${{ github.token }} + GROUP_NAME: ${{ needs.publish-report.outputs.group_name }} env: HOST_URL: https://${{ github.event.schedule && 'preview.gov.tools' || (inputs.deployment || 'govtool.cardanoapi.io') }} + DEPLOYMENT: ${{ github.event.schedule && 'preview.gov.tools' || inputs.deployment || 'govtool.cardanoapi.io'}} REPORT_NAME: ${{ github.event.schedule && 'nightly-'}}govtool-frontend GH_PAGES: ${{vars.GH_PAGES}} COMMIT_SHA: ${{ github.event.workflow_run.head_sha || github.sha }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 145af887e..6c28d95c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,31 @@ changes. ### Added +- Preserve maintenance ending banner state on the wallet connection change [Issue 3681](https://github.com/IntersectMBO/govtool/issues/3681) + +### Fixed + +- Fix missing off chain references in DRep details [Issue 3490](https://github.com/IntersectMBO/govtool/issues/3490) +- Fix blank screen and type error on linkReferences when navigating to edit dRep page that has no links [Issue 3714](https://github.com/IntersectMBO/govtool/issues/3714) +- Fix adding two link input fields when editing the dRep form when no links are present initially [Issue 3709](https://github.com/IntersectMBO/govtool/issues/3709) + +### Changed + +### Removed + +## [v2.0.23](https://github.com/IntersectMBO/govtool/releases/tag/v2.0.23) 2025-05-22 + +### Added + +- Add CIP-129 support for gov_actions hashes in Live Voting (governance actions) [Issue 3619](https://github.com/IntersectMBO/govtool/issues/3619) + - Add maintenance ending banner [Issue 3647](https://github.com/IntersectMBO/govtool/issues/3647) +- Add support for the Protocol Parameter Change and Hard Fork Initiation governance actions ### Fixed +- Fix displaying proposals title in details page [Issue 3192](https://github.com/IntersectMBO/govtool/issues/3192) + ### Changed ### Removed diff --git a/gov-action-loader/frontend/src/App.vue b/gov-action-loader/frontend/src/App.vue index 889b6c540..6c3ce0064 100644 --- a/gov-action-loader/frontend/src/App.vue +++ b/gov-action-loader/frontend/src/App.vue @@ -70,8 +70,8 @@ export default { data() { return { tab: null, - selectedNetwork: 'Sanchonet', // Default selection - networkOptions: ['Sanchonet', 'Preview', 'Preprod'], + selectedNetwork: 'Preview', // Default selection + networkOptions: ['Preview', 'Preprod'], walletInfo: { address: null, balance: null, diff --git a/gov-action-loader/frontend/src/views/BulkLoad.vue b/gov-action-loader/frontend/src/views/BulkLoad.vue index 8ce14efa2..7072e78c1 100644 --- a/gov-action-loader/frontend/src/views/BulkLoad.vue +++ b/gov-action-loader/frontend/src/views/BulkLoad.vue @@ -9,7 +9,7 @@ import { prepareErrorMessage } from '../utils'
Governance Action Bulk Loader
-
Submit to load the required number of a given action to sanchonet.
+
Submit to load the required number of a given action to {{ selectedNetwork.toLowerCase() }}.
@@ -68,6 +68,9 @@ import { prepareErrorMessage } from '../utils' import { submitMultipleProposals } from '../api' export default { + props: { + selectedNetwork: String, + }, data() { return { actionTypes: ['Constitution', 'Info', 'Withdrawal', 'No-Confidence', 'Update-Committee', 'Hardfork', 'Update-Parameters'], diff --git a/gov-action-loader/frontend/src/views/SpecificLoad.vue b/gov-action-loader/frontend/src/views/SpecificLoad.vue index b9cc8744a..b54f5d75a 100644 --- a/gov-action-loader/frontend/src/views/SpecificLoad.vue +++ b/gov-action-loader/frontend/src/views/SpecificLoad.vue @@ -7,7 +7,7 @@ import config from '../config'
Specific Governance Action Loader
-
Fill in the details according to specific action submit to sanchonet.
+
Fill in the details according to specific action submit to {{ selectedNetwork.toLowerCase() }}.
diff --git a/govtool/backend/.gitignore b/govtool/backend/.gitignore index 148b93507..541110bca 100644 --- a/govtool/backend/.gitignore +++ b/govtool/backend/.gitignore @@ -1,3 +1,6 @@ # other .vscode dev-config.json + +# Tool version management file (e.g., asdf version manager) +.tool-versions diff --git a/govtool/backend/Dockerfile b/govtool/backend/Dockerfile index 0380c164d..88b71c171 100644 --- a/govtool/backend/Dockerfile +++ b/govtool/backend/Dockerfile @@ -4,4 +4,4 @@ FROM $BASE_IMAGE_REPO:$BASE_IMAGE_TAG WORKDIR /src COPY . . RUN cabal build -RUN cp dist-newstyle/build/x86_64-linux/ghc-9.2.7/vva-be-2.0.22/x/vva-be/build/vva-be/vva-be /usr/local/bin +RUN cp dist-newstyle/build/x86_64-linux/ghc-9.2.7/vva-be-2.0.23/x/vva-be/build/vva-be/vva-be /usr/local/bin diff --git a/govtool/backend/Dockerfile.qovery b/govtool/backend/Dockerfile.qovery index a5ea19d65..925a0a670 100644 --- a/govtool/backend/Dockerfile.qovery +++ b/govtool/backend/Dockerfile.qovery @@ -4,7 +4,7 @@ FROM $BASE_IMAGE_REPO:$BASE_IMAGE_TAG WORKDIR /src COPY . . RUN cabal build -RUN cp dist-newstyle/build/x86_64-linux/ghc-9.2.7/vva-be-2.0.22/x/vva-be/build/vva-be/vva-be /usr/local/bin +RUN cp dist-newstyle/build/x86_64-linux/ghc-9.2.7/vva-be-2.0.23/x/vva-be/build/vva-be/vva-be /usr/local/bin # Expose the necessary port EXPOSE 9876 diff --git a/govtool/backend/app/Main.hs b/govtool/backend/app/Main.hs index c2d3ca711..d82821424 100644 --- a/govtool/backend/app/Main.hs +++ b/govtool/backend/app/Main.hs @@ -125,6 +125,7 @@ startApp vvaConfig sentryService = do networkInfoCache <- newCache networkTotalStakeCache <- newCache dRepVotingPowerListCache <- newCache + accountInfoCache <- newCache return $ CacheEnv { proposalListCache , getProposalCache @@ -139,6 +140,7 @@ startApp vvaConfig sentryService = do , networkInfoCache , networkTotalStakeCache , dRepVotingPowerListCache + , accountInfoCache } let connectionString = encodeUtf8 (dbSyncConnectionString $ getter vvaConfig) diff --git a/govtool/backend/sql/get-account-info.sql b/govtool/backend/sql/get-account-info.sql new file mode 100644 index 000000000..d11fe68a4 --- /dev/null +++ b/govtool/backend/sql/get-account-info.sql @@ -0,0 +1,23 @@ +SELECT + sa.id, + sa.view, + CASE + WHEN sa.script_hash IS NOT NULL THEN true + ELSE false + END AS is_script_based, + CASE + WHEN ( + SELECT COALESCE(MAX(epoch_no), 0) + FROM stake_registration sr + WHERE sr.addr_id = sa.id + ) > ( + SELECT COALESCE(MAX(epoch_no), 0) + FROM stake_deregistration sd + WHERE sd.addr_id = sa.id + ) THEN true + ELSE false + END AS is_registered +FROM + stake_address sa +WHERE + sa.hash_raw = decode(?, 'hex'); diff --git a/govtool/backend/sql/get-previous-enacted-governance-action-proposal-details.sql b/govtool/backend/sql/get-previous-enacted-governance-action-proposal-details.sql new file mode 100644 index 000000000..6aabbaa3d --- /dev/null +++ b/govtool/backend/sql/get-previous-enacted-governance-action-proposal-details.sql @@ -0,0 +1,15 @@ +SELECT + gap.id, + tx_id, + index, + description, + encode(hash, 'hex') AS hash +FROM + gov_action_proposal gap +JOIN + tx ON gap.tx_id = tx.id +WHERE + gap.type = ? AND gap.enacted_epoch IS NOT NULL +ORDER BY + gap.id DESC +LIMIT 1; \ No newline at end of file diff --git a/govtool/backend/sql/list-dreps.sql b/govtool/backend/sql/list-dreps.sql index 2c3b87aa5..3062c9f48 100644 --- a/govtool/backend/sql/list-dreps.sql +++ b/govtool/backend/sql/list-dreps.sql @@ -126,7 +126,35 @@ DRepData AS ( off_chain_vote_drep_data.motivations, off_chain_vote_drep_data.qualifications, off_chain_vote_drep_data.image_url, - off_chain_vote_drep_data.image_hash + off_chain_vote_drep_data.image_hash, + COALESCE( + ( + SELECT jsonb_agg(ref) + FROM jsonb_array_elements( + CASE + WHEN (ocvd.json::jsonb)->'body'->'references' IS NOT NULL + THEN (ocvd.json::jsonb)->'body'->'references' + ELSE '[]'::jsonb + END + ) AS ref + WHERE ref->>'@type' = 'Identity' + ), + '[]'::jsonb + ) AS identity_references, + COALESCE( + ( + SELECT jsonb_agg(ref) + FROM jsonb_array_elements( + CASE + WHEN (ocvd.json::jsonb)->'body'->'references' IS NOT NULL + THEN (ocvd.json::jsonb)->'body'->'references' + ELSE '[]'::jsonb + END + ) AS ref + WHERE ref->>'@type' = 'Link' + ), + '[]'::jsonb + ) AS link_references FROM drep_hash dh JOIN RankedDRepRegistration ON RankedDRepRegistration.drep_hash_id = dh.id @@ -212,7 +240,29 @@ DRepData AS ( off_chain_vote_drep_data.motivations, off_chain_vote_drep_data.qualifications, off_chain_vote_drep_data.image_url, - off_chain_vote_drep_data.image_hash + off_chain_vote_drep_data.image_hash, + ( + SELECT jsonb_agg(ref) + FROM jsonb_array_elements( + CASE + WHEN (ocvd.json::jsonb)->'body'->'references' IS NOT NULL + THEN (ocvd.json::jsonb)->'body'->'references' + ELSE '[]'::jsonb + END + ) AS ref + WHERE ref->>'@type' = 'Identity' + ), + ( + SELECT jsonb_agg(ref) + FROM jsonb_array_elements( + CASE + WHEN (ocvd.json::jsonb)->'body'->'references' IS NOT NULL + THEN (ocvd.json::jsonb)->'body'->'references' + ELSE '[]'::jsonb + END + ) AS ref + WHERE ref->>'@type' = 'Link' + ) ) SELECT * FROM DRepData WHERE diff --git a/govtool/backend/src/VVA/API.hs b/govtool/backend/src/VVA/API.hs index b95960f52..ebc4d9609 100644 --- a/govtool/backend/src/VVA/API.hs +++ b/govtool/backend/src/VVA/API.hs @@ -40,6 +40,7 @@ import VVA.Config import qualified VVA.DRep as DRep import qualified VVA.Epoch as Epoch import VVA.Network as Network +import VVA.Account as Account import qualified VVA.Proposal as Proposal import qualified VVA.Transaction as Transaction import qualified VVA.Types as Types @@ -78,12 +79,14 @@ type VVAApi = :> QueryParam "search" Text :> Get '[JSON] ListProposalsResponse :<|> "proposal" :> "get" :> Capture "proposalId" GovActionId :> QueryParam "drepId" HexText :> Get '[JSON] GetProposalResponse + :<|> "proposal" :> "enacted-details" :> QueryParam "type" GovernanceActionType :> Get '[JSON] (Maybe EnactedProposalDetailsResponse) :<|> "epoch" :> "params" :> Get '[JSON] GetCurrentEpochParamsResponse :<|> "transaction" :> "status" :> Capture "transactionId" HexText :> Get '[JSON] GetTransactionStatusResponse :<|> "throw500" :> Get '[JSON] () :<|> "network" :> "metrics" :> Get '[JSON] GetNetworkMetricsResponse :<|> "network" :> "info" :> Get '[JSON] GetNetworkInfoResponse :<|> "network" :> "total-stake" :> Get '[JSON] GetNetworkTotalStakeResponse + :<|> "account" :> Capture "stakeKey" HexText :> Get '[JSON] GetAccountInfoResponse server :: App m => ServerT VVAApi m server = drepList @@ -95,12 +98,14 @@ server = drepList :<|> getStakeKeyVotingPower :<|> listProposals :<|> getProposal + :<|> getEnactedProposalDetails :<|> getCurrentEpochParams :<|> getTransactionStatus :<|> throw500 :<|> getNetworkMetrics :<|> getNetworkInfo :<|> getNetworkTotalStake + :<|> getAccountInfo mapDRepType :: Types.DRepType -> DRepType mapDRepType Types.DRep = NormalDRep @@ -132,7 +137,9 @@ drepRegistrationToDrep Types.DRepRegistration {..} = dRepMotivations = dRepRegistrationMotivations, dRepQualifications = dRepRegistrationQualifications, dRepImageUrl = dRepRegistrationImageUrl, - dRepImageHash = HexText <$> dRepRegistrationImageHash + dRepImageHash = HexText <$> dRepRegistrationImageHash, + dRepIdentityReferences = DRepReferences <$> dRepRegistrationIdentityReferences, + dRepLinkReferences = DRepReferences <$> dRepRegistrationLinkReferences } delegationToResponse :: Types.Delegation -> DelegationResponse @@ -442,6 +449,33 @@ getProposal g@(GovActionId govActionTxHash govActionIndex) mDrepId' = do , getProposalResponseVote = voteResponse } +getEnactedProposalDetails :: App m => Maybe GovernanceActionType -> m (Maybe EnactedProposalDetailsResponse) +getEnactedProposalDetails maybeType = do + let proposalType = maybe "HardForkInitiation" governanceActionTypeToText maybeType + + mDetails <- Proposal.getPreviousEnactedProposal proposalType + + let response = enactedProposalDetailsToResponse <$> mDetails + + return response + where + governanceActionTypeToText :: GovernanceActionType -> Text + governanceActionTypeToText actionType = + case actionType of + HardForkInitiation -> "HardForkInitiation" + ParameterChange -> "ParameterChange" + _ -> "HardForkInitiation" + + enactedProposalDetailsToResponse :: Types.EnactedProposalDetails -> EnactedProposalDetailsResponse + enactedProposalDetailsToResponse Types.EnactedProposalDetails{..} = + EnactedProposalDetailsResponse + { enactedProposalDetailsResponseId = enactedProposalDetailsId + , enactedProposalDetailsResponseTxId = enactedProposalDetailsTxId + , enactedProposalDetailsResponseIndex = enactedProposalDetailsIndex + , enactedProposalDetailsResponseDescription = enactedProposalDetailsDescription + , enactedProposalDetailsResponseHash = HexText enactedProposalDetailsHash + } + getCurrentEpochParams :: App m => m GetCurrentEpochParamsResponse getCurrentEpochParams = do CacheEnv {currentEpochCache} <- asks vvaCache @@ -498,3 +532,14 @@ getNetworkMetrics = do , getNetworkMetricsResponseQuorumNumerator = networkMetricsQuorumNumerator , getNetworkMetricsResponseQuorumDenominator = networkMetricsQuorumDenominator } + +getAccountInfo :: App m => HexText -> m GetAccountInfoResponse +getAccountInfo (unHexText -> stakeKey) = do + CacheEnv {accountInfoCache} <- asks vvaCache + Types.AccountInfo {..} <- Account.accountInfo stakeKey + return $ GetAccountInfoResponse + { getAccountInfoResponseId = accountInfoId + , getAccountInfoResponseView = accountInfoView + , getAccountInfoResponseIsRegistered = accountInfoIsRegistered + , getAccountInfoResponseIsScriptBased = accountInfoIsScriptBased + } diff --git a/govtool/backend/src/VVA/API/Types.hs b/govtool/backend/src/VVA/API/Types.hs index d1c88adb0..a270a2872 100644 --- a/govtool/backend/src/VVA/API/Types.hs +++ b/govtool/backend/src/VVA/API/Types.hs @@ -273,7 +273,7 @@ instance ToParamSchema GovernanceActionSortMode where newtype GovernanceActionDetails - = GovernanceActionDetails { getValue :: Value } + = GovernanceActionDetails { getGovernanceActionValue :: Value } deriving newtype (Show) instance FromJSON GovernanceActionDetails where @@ -455,6 +455,40 @@ instance ToSchema ProposalResponse where & example ?~ toJSON exampleProposalResponse +data EnactedProposalDetailsResponse + = EnactedProposalDetailsResponse + { enactedProposalDetailsResponseId :: Integer + , enactedProposalDetailsResponseTxId :: Integer + , enactedProposalDetailsResponseIndex :: Integer + , enactedProposalDetailsResponseDescription :: Maybe Value + , enactedProposalDetailsResponseHash :: HexText + } + deriving (Generic, Show) + +deriveJSON (jsonOptions "enactedProposalDetailsResponse") ''EnactedProposalDetailsResponse + +exampleEnactedProposalDetailsResponse :: Text +exampleEnactedProposalDetailsResponse = "{ \"id\": 123," + <> "\"txId\": 456," + <> "\"index\": 0," + <> "\"description\": {\"key\": \"value\"}," + <> "\"hash\": \"9af10e89979e51b8cdc827c963124a1ef4920d1253eef34a1d5cfe76438e3f11\"}" + +instance ToSchema EnactedProposalDetailsResponse where + declareNamedSchema proxy = do + NamedSchema name_ schema_ <- + genericDeclareNamedSchema + ( fromAesonOptions $ + jsonOptions "enactedProposalDetailsResponse" + ) + proxy + return $ + NamedSchema name_ $ + schema_ + & description ?~ "Enacted Proposal Details Response" + & example + ?~ toJSON exampleEnactedProposalDetailsResponse + exampleListProposalsResponse :: Text exampleListProposalsResponse = "{ \"page\": 0," @@ -559,6 +593,7 @@ instance ToSchema VoteResponse where & example ?~ toJSON exampleVoteResponse + data DRepInfoResponse = DRepInfoResponse { dRepInfoResponseIsScriptBased :: Bool @@ -578,7 +613,7 @@ data DRepInfoResponse , dRepInfoResponseGivenName :: Maybe Text , dRepInfoResponseObjectives :: Maybe Text , dRepInfoResponseMotivations :: Maybe Text - , dRepInfoResponseQualifications :: Maybe Text + , dRepInfoResponseQualifications :: Maybe Text , dRepInfoResponseImageUrl :: Maybe Text , dRepInfoResponseImageHash :: Maybe HexText } @@ -786,6 +821,27 @@ instance ToSchema DRepType where & description ?~ "DRep Type" & enum_ ?~ map toJSON [NormalDRep, SoleVoter] +newtype DRepReferences + = DRepReferences { getDRepReferencesValue :: Value } + deriving newtype (Show) + +instance FromJSON DRepReferences where + parseJSON v = return $ DRepReferences v + +instance ToJSON DRepReferences where + toJSON (DRepReferences d) = d + +instance ToSchema DRepReferences where + declareNamedSchema _ = pure $ NamedSchema (Just "DRepReferences") $ mempty + & type_ ?~ OpenApiObject + & description ?~ "A JSON value that can include nested objects and arrays" + & example ?~ toJSON + (Aeson.object + [ "some_key" .= ("some value" :: String) + , "nested_key" .= Aeson.object ["inner_key" .= (1 :: Int)] + , "array_key" .= [1, 2, 3 :: Int] + ]) + data DRep = DRep { dRepIsScriptBased :: Bool @@ -804,9 +860,11 @@ data DRep , dRepGivenName :: Maybe Text , dRepObjectives :: Maybe Text , dRepMotivations :: Maybe Text - , dRepQualifications :: Maybe Text + , dRepQualifications :: Maybe Text , dRepImageUrl :: Maybe Text , dRepImageHash :: Maybe HexText + , dRepIdentityReferences :: Maybe DRepReferences + , dRepLinkReferences :: Maybe DRepReferences } deriving (Generic, Show) @@ -998,3 +1056,26 @@ instance ToSchema GetNetworkMetricsResponse where & description ?~ "GetNetworkMetricsResponse" & example ?~ toJSON exampleGetNetworkMetricsResponse + +data GetAccountInfoResponse + = GetAccountInfoResponse + { getAccountInfoResponseId :: Integer + , getAccountInfoResponseView :: Text + , getAccountInfoResponseIsRegistered :: Bool + , getAccountInfoResponseIsScriptBased :: Bool + } + deriving (Generic, Show) +deriveJSON (jsonOptions "getAccountInfoResponse") ''GetAccountInfoResponse +exampleGetAccountInfoResponse :: Text +exampleGetAccountInfoResponse = + "{\"stakeKey\": \"stake1u9\"," + <> " \"id\": \"1\"," + <> "\"view\": \"stake_test1uzapf83wydusjln97rqr7fen6vgrz5087yqdxm0akqdqkgstj2345\"," + <> "\"isRegistered\": false," + <> "\"isScriptBased\": false}" +instance ToSchema GetAccountInfoResponse where + declareNamedSchema _ = pure $ NamedSchema (Just "GetAccountInfoResponse") $ mempty + & type_ ?~ OpenApiObject + & description ?~ "GetAccountInfoResponse" + & example + ?~ toJSON exampleGetAccountInfoResponse diff --git a/govtool/backend/src/VVA/Account.hs b/govtool/backend/src/VVA/Account.hs new file mode 100644 index 000000000..10aa92e52 --- /dev/null +++ b/govtool/backend/src/VVA/Account.hs @@ -0,0 +1,35 @@ +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} + +module VVA.Account where + +import Control.Monad.Except (MonadError, throwError) +import Control.Monad.Reader (MonadIO, MonadReader, liftIO) +import Data.ByteString (ByteString) +import Data.FileEmbed (embedFile) +import Data.String (fromString) +import qualified Database.PostgreSQL.Simple as SQL +import VVA.Types (AppError(..), AccountInfo(..)) +import Data.Text (Text, unpack) +import qualified Data.Text.Encoding as Text +import qualified Data.Text.IO as Text +import Data.Has (Has) +import VVA.Pool (ConnectionPool, withPool) + +sqlFrom :: ByteString -> SQL.Query +sqlFrom = fromString . unpack . Text.decodeUtf8 + +accountInfoSql :: SQL.Query +accountInfoSql = sqlFrom $(embedFile "sql/get-account-info.sql") + +accountInfo :: + (Has ConnectionPool r, MonadReader r m, MonadIO m, MonadError AppError m) => + Text -> + m AccountInfo +accountInfo stakeKey = withPool $ \conn -> do + result <- liftIO $ SQL.query conn accountInfoSql (SQL.Only stakeKey) + case result of + [(id, view, is_registered, is_script_based)] -> + return $ AccountInfo id view is_registered is_script_based + _ -> throwError $ CriticalError "Could not query the account info." diff --git a/govtool/backend/src/VVA/DRep.hs b/govtool/backend/src/VVA/DRep.hs index 49482b1e9..f8c9f8737 100644 --- a/govtool/backend/src/VVA/DRep.hs +++ b/govtool/backend/src/VVA/DRep.hs @@ -11,41 +11,74 @@ import Control.Monad.Reader import Crypto.Hash +import Data.Aeson (Value) import Data.ByteString (ByteString) -import qualified Data.ByteString.Base16 as Base16 -import qualified Data.ByteString.Char8 as C +import qualified Data.ByteString.Base16 as Base16 +import qualified Data.ByteString.Char8 as C import Data.FileEmbed (embedFile) import Data.Foldable (Foldable (sum)) import Data.Has (Has) -import qualified Data.Map as M +import qualified Data.Map as M import Data.Maybe (fromMaybe, isJust, isNothing) import Data.Scientific import Data.String (fromString) import Data.Text (Text, pack, unpack, intercalate) -import qualified Data.Text.Encoding as Text +import qualified Data.Text.Encoding as Text import Data.Time -import qualified Database.PostgreSQL.Simple as SQL +import qualified Database.PostgreSQL.Simple as SQL import Database.PostgreSQL.Simple.Types (In(..)) +import Database.PostgreSQL.Simple.FromRow import VVA.Config import VVA.Pool (ConnectionPool, withPool) -import qualified VVA.Proposal as Proposal +import qualified VVA.Proposal as Proposal import VVA.Types (AppError, DRepInfo (..), DRepRegistration (..), DRepStatus (..), DRepType (..), Proposal (..), Vote (..), DRepVotingPowerList (..)) +data DRepQueryResult = DRepQueryResult + { queryDrepHash :: Text + , queryDrepView :: Text + , queryIsScriptBased :: Bool + , queryUrl :: Maybe Text + , queryDataHash :: Maybe Text + , queryDeposit :: Scientific + , queryVotingPower :: Maybe Integer + , queryIsActive :: Bool + , queryTxHash :: Maybe Text + , queryDate :: LocalTime + , queryLatestDeposit :: Scientific + , queryLatestNonDeregisterVotingAnchorWasNotNull :: Bool + , queryMetadataError :: Maybe Text + , queryPaymentAddress :: Maybe Text + , queryGivenName :: Maybe Text + , queryObjectives :: Maybe Text + , queryMotivations :: Maybe Text + , queryQualifications :: Maybe Text + , queryImageUrl :: Maybe Text + , queryImageHash :: Maybe Text + , queryIdentityReferences :: Maybe Value + , queryLinkReferences :: Maybe Value + } deriving (Show) + +instance FromRow DRepQueryResult where + fromRow = DRepQueryResult + <$> field <*> field <*> field <*> field <*> field <*> field + <*> field <*> field <*> field <*> field <*> field <*> field + <*> field <*> field <*> field <*> field <*> field <*> field + <*> field <*> field <*> field <*> field + sqlFrom :: ByteString -> SQL.Query sqlFrom bs = fromString $ unpack $ Text.decodeUtf8 bs listDRepsSql :: SQL.Query listDRepsSql = sqlFrom $(embedFile "sql/list-dreps.sql") - listDReps :: (Has ConnectionPool r, Has VVAConfig r, MonadReader r m, MonadIO m) => Maybe Text -> m [DRepRegistration] listDReps mSearchQuery = withPool $ \conn -> do let searchParam = fromMaybe "" mSearchQuery - results <- liftIO $ SQL.query conn listDRepsSql + results <- liftIO (SQL.query conn listDRepsSql ( searchParam -- COALESCE(?, '') , searchParam -- LENGTH(?) , searchParam -- AND ? @@ -56,44 +89,45 @@ listDReps mSearchQuery = withPool $ \conn -> do , "%" <> searchParam <> "%" -- objectives , "%" <> searchParam <> "%" -- motivations , "%" <> searchParam <> "%" -- qualifications - ) + ) :: IO [DRepQueryResult]) timeZone <- liftIO getCurrentTimeZone return - [ DRepRegistration drepHash drepView isScriptBased url dataHash (floor @Scientific deposit) votingPower status drepType txHash (localTimeToUTC timeZone date) metadataError paymentAddress givenName objectives motivations qualifications imageUrl imageHash - | ( drepHash - , drepView - , isScriptBased - , url - , dataHash - , deposit - , votingPower - , isActive - , txHash - , date - , latestDeposit - , latestNonDeregisterVotingAnchorWasNotNull - , metadataError - , paymentAddress - , givenName - , objectives - , motivations - , qualifications - , imageUrl - , imageHash - ) <- results - , let status = case (isActive, deposit) of + [ DRepRegistration + (queryDrepHash result) + (queryDrepView result) + (queryIsScriptBased result) + (queryUrl result) + (queryDataHash result) + (floor @Scientific $ queryDeposit result) + (queryVotingPower result) + status + drepType + (queryTxHash result) + (localTimeToUTC timeZone $ queryDate result) + (queryMetadataError result) + (queryPaymentAddress result) + (queryGivenName result) + (queryObjectives result) + (queryMotivations result) + (queryQualifications result) + (queryImageUrl result) + (queryImageHash result) + (queryIdentityReferences result) + (queryLinkReferences result) + | result <- results + , let status = case (queryIsActive result, queryDeposit result) of (_, d) | d < 0 -> Retired (isActive, d) | d >= 0 && isActive -> Active | d >= 0 && not isActive -> Inactive - , let latestDeposit' = floor @Scientific latestDeposit :: Integer - , let drepType | latestDeposit' >= 0 && isNothing url = SoleVoter - | latestDeposit' >= 0 && isJust url = DRep - | latestDeposit' < 0 && not latestNonDeregisterVotingAnchorWasNotNull = SoleVoter - | latestDeposit' < 0 && latestNonDeregisterVotingAnchorWasNotNull = DRep - | Data.Maybe.isJust url = DRep + , let latestDeposit' = floor @Scientific (queryLatestDeposit result) :: Integer + , let drepType | latestDeposit' >= 0 && isNothing (queryUrl result) = SoleVoter + | latestDeposit' >= 0 && isJust (queryUrl result) = DRep + | latestDeposit' < 0 && not (queryLatestNonDeregisterVotingAnchorWasNotNull result) = SoleVoter + | latestDeposit' < 0 && queryLatestNonDeregisterVotingAnchorWasNotNull result = DRep + | Data.Maybe.isJust (queryUrl result) = DRep ] - + getVotingPowerSql :: SQL.Query getVotingPowerSql = sqlFrom $(embedFile "sql/get-voting-power.sql") diff --git a/govtool/backend/src/VVA/Proposal.hs b/govtool/backend/src/VVA/Proposal.hs index 89e72a8c8..6c31a8016 100644 --- a/govtool/backend/src/VVA/Proposal.hs +++ b/govtool/backend/src/VVA/Proposal.hs @@ -17,25 +17,34 @@ import Data.Aeson.Types (Parser, parseMaybe) import Data.ByteString (ByteString) import Data.FileEmbed (embedFile) import Data.Foldable (fold) -import Data.Has (Has) -import qualified Data.Map as Map -import Data.Maybe (fromMaybe) +import Data.Has (Has, getter) +import qualified Data.Map as Map +import Data.Maybe (fromMaybe, isJust) import Data.Monoid (Sum (..), getSum) import Data.Scientific import Data.String (fromString) import Data.Text (Text, pack, unpack) -import qualified Data.Text.Encoding as Text -import qualified Data.Text.IO as Text +import qualified Data.Text.Encoding as Text +import qualified Data.Text.IO as Text import Data.Time -import qualified Database.PostgreSQL.Simple as SQL -import qualified Database.PostgreSQL.Simple.Types as PG +import qualified Database.PostgreSQL.Simple as SQL +import qualified Database.PostgreSQL.Simple.Types as PG import Database.PostgreSQL.Simple.ToField (ToField(..)) import Database.PostgreSQL.Simple.ToRow (ToRow(..)) +import GHC.IO.Unsafe (unsafePerformIO) + import VVA.Config import VVA.Pool (ConnectionPool, withPool) -import VVA.Types (AppError (..), Proposal (..)) +import VVA.Types (AppError (..), Proposal (..), EnactedProposalDetails (..)) + +query1 :: (SQL.ToRow q, SQL.FromRow r) => SQL.Connection -> SQL.Query -> q -> IO (Maybe r) +query1 conn q params = do + results <- SQL.query conn q params + case results of + [x] -> return (Just x) + _ -> return Nothing sqlFrom :: ByteString -> SQL.Query sqlFrom bs = fromString $ unpack $ Text.decodeUtf8 bs @@ -84,4 +93,36 @@ getProposals mSearchTerms = withPool $ \conn -> do Left (e :: SomeException) -> do putStrLn $ "Error fetching proposals: " <> show e return [] - Right rows -> return rows \ No newline at end of file + Right rows -> return rows + +latestEnactedProposalSql :: SQL.Query +latestEnactedProposalSql = + let rawSql = sqlFrom $(embedFile "sql/get-previous-enacted-governance-action-proposal-details.sql") + in unsafePerformIO $ do + putStrLn $ "[DEBUG] SQL query content: " ++ show rawSql + return rawSql + +getPreviousEnactedProposal :: + (Has ConnectionPool r, Has VVAConfig r, MonadReader r m, MonadIO m, MonadFail m, MonadError AppError m) => + Text -> + m (Maybe EnactedProposalDetails) +getPreviousEnactedProposal proposalType = withPool $ \conn -> do + let query = latestEnactedProposalSql + let params = [proposalType] + + result <- liftIO $ try $ do + rows <- SQL.query conn query params :: IO [EnactedProposalDetails] + case rows of + [x] -> return (Just x) + _ -> return Nothing + + case result of + Left err -> do + throwError $ CriticalError $ "Database error: " <> pack (show (err :: SomeException)) + Right proposal -> do + case proposal of + Just details -> do + liftIO $ putStrLn $ "[DEBUG] Previous enacted proposal details: " ++ show details + Nothing -> + liftIO $ putStrLn "[DEBUG] No previous enacted proposal found" + return proposal \ No newline at end of file diff --git a/govtool/backend/src/VVA/Types.hs b/govtool/backend/src/VVA/Types.hs index 35cdd37bf..217b19a5a 100644 --- a/govtool/backend/src/VVA/Types.hs +++ b/govtool/backend/src/VVA/Types.hs @@ -5,26 +5,28 @@ {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE TypeApplications #-} +{-# LANGUAGE ScopedTypeVariables #-} module VVA.Types where import Control.Concurrent.QSem import Control.Exception -import Control.Monad.Except (MonadError) -import Control.Monad.Fail (MonadFail) -import Control.Monad.IO.Class (MonadIO) -import Control.Monad.Reader (MonadReader) +import Control.Monad.Except (MonadError) +import Control.Monad.Fail (MonadFail) +import Control.Monad.IO.Class (MonadIO) +import Control.Monad.Reader (MonadReader) -import Data.Aeson (Value, ToJSON (..), object, (.=)) -import qualified Data.Cache as Cache +import Data.Aeson (Value, ToJSON (..), object, (.=)) +import qualified Data.Cache as Cache import Data.Has -import Data.Pool (Pool) -import Data.Text (Text) -import Data.Time (UTCTime, LocalTime) +import Data.Pool (Pool) +import Data.Text (Text) +import Data.Time (UTCTime, LocalTime) import Data.Scientific -import Database.PostgreSQL.Simple (Connection) +import Database.PostgreSQL.Simple (Connection) import Database.PostgreSQL.Simple.FromRow +import Database.PostgreSQL.Simple.FromField (FromField(..), returnError, ResultError(ConversionFailed)) import VVA.Cache import VVA.Config @@ -107,8 +109,25 @@ data DRepVotingPowerList data DRepStatus = Active | Inactive | Retired deriving (Show, Eq, Ord) +instance FromField DRepStatus where + fromField f mdata = do + (value :: Text) <- fromField f mdata + case value of + "Active" -> return Active + "Inactive" -> return Inactive + "Retired" -> return Retired + _ -> returnError ConversionFailed f "Invalid DRepStatus" + data DRepType = DRep | SoleVoter deriving (Show, Eq) +instance FromField DRepType where + fromField f mdata = do + (value :: Text) <- fromField f mdata + case value of + "DRep" -> return DRep + "SoleVoter" -> return SoleVoter + _ -> returnError ConversionFailed f "Invalid DRepType" + data DRepRegistration = DRepRegistration { dRepRegistrationDRepHash :: Text @@ -127,12 +146,39 @@ data DRepRegistration , dRepRegistrationGivenName :: Maybe Text , dRepRegistrationObjectives :: Maybe Text , dRepRegistrationMotivations :: Maybe Text - , dRepRegistrationQualifications :: Maybe Text + , dRepRegistrationQualifications :: Maybe Text , dRepRegistrationImageUrl :: Maybe Text , dRepRegistrationImageHash :: Maybe Text + , dRepRegistrationIdentityReferences :: Maybe Value + , dRepRegistrationLinkReferences :: Maybe Value } deriving (Show) +instance FromRow DRepRegistration where + fromRow = + DRepRegistration + <$> field -- dRepRegistrationDRepHash + <*> field -- dRepRegistrationView + <*> field -- dRepRegistrationIsScriptBased + <*> field -- dRepRegistrationUrl + <*> field -- dRepRegistrationDataHash + <*> (floor @Scientific <$> field) -- dRepRegistrationDeposit + <*> field -- dRepRegistrationVotingPower + <*> field -- dRepRegistrationStatus + <*> field -- dRepRegistrationType + <*> field -- dRepRegistrationLatestTxHash + <*> field -- dRepRegistrationLatestRegistrationDate + <*> field -- dRepRegistrationMetadataError + <*> field -- dRepRegistrationPaymentAddress + <*> field -- dRepRegistrationGivenName + <*> field -- dRepRegistrationObjectives + <*> field -- dRepRegistrationMotivations + <*> field -- dRepRegistrationQualifications + <*> field -- dRepRegistrationImageUrl + <*> field -- dRepRegistrationImageHash + <*> field -- dRepRegistrationIdentityReferences + <*> field -- dRepRegistrationLinkReferences + data Proposal = Proposal { proposalId :: Integer @@ -211,6 +257,40 @@ instance ToJSON TransactionStatus where , "votingProcedure" .= votingProcedure ] +data EnactedProposalDetails = EnactedProposalDetails + { enactedProposalDetailsId :: Integer + , enactedProposalDetailsTxId :: Integer + , enactedProposalDetailsIndex :: Integer + , enactedProposalDetailsDescription :: Maybe Value + , enactedProposalDetailsHash :: Text + } + deriving (Show) + +instance FromRow EnactedProposalDetails where + fromRow = + EnactedProposalDetails + <$> field + <*> field + <*> (floor @Scientific <$> field) + <*> field + <*> field + +instance ToJSON EnactedProposalDetails where + toJSON EnactedProposalDetails + { enactedProposalDetailsId + , enactedProposalDetailsTxId + , enactedProposalDetailsIndex + , enactedProposalDetailsDescription + , enactedProposalDetailsHash + } = + object + [ "id" .= enactedProposalDetailsId + , "tx_id" .= enactedProposalDetailsTxId + , "index" .= enactedProposalDetailsIndex + , "description" .= enactedProposalDetailsDescription + , "hash" .= enactedProposalDetailsHash + ] + data CacheEnv = CacheEnv { proposalListCache :: Cache.Cache () [Proposal] @@ -226,6 +306,7 @@ data CacheEnv , networkInfoCache :: Cache.Cache () NetworkInfo , networkTotalStakeCache :: Cache.Cache () NetworkTotalStake , dRepVotingPowerListCache :: Cache.Cache Text [DRepVotingPowerList] + , accountInfoCache :: Cache.Cache Text AccountInfo } data NetworkInfo @@ -268,3 +349,11 @@ data Delegation , delegationIsDRepScriptBased :: Bool , delegationTxHash :: Text } + +data AccountInfo + = AccountInfo + { accountInfoId :: Integer + , accountInfoView :: Text + , accountInfoIsRegistered :: Bool + , accountInfoIsScriptBased :: Bool + } diff --git a/govtool/backend/vva-be.cabal b/govtool/backend/vva-be.cabal index 1accd1af4..9ae62c3dd 100644 --- a/govtool/backend/vva-be.cabal +++ b/govtool/backend/vva-be.cabal @@ -1,6 +1,6 @@ cabal-version: 3.6 name: vva-be -version: 2.0.22 +version: 2.0.23 -- A short (one-line) description of the package. -- synopsis: @@ -36,6 +36,8 @@ extra-source-files: sql/get-network-total-stake.sql sql/get-dreps-voting-power-list.sql sql/get-filtered-dreps-voting-power.sql + sql/get-previous-enacted-governance-action-proposal-details.sql + sql/get-account-info.sql executable vva-be main-is: Main.hs @@ -123,4 +125,5 @@ library , VVA.Pool , VVA.Types , VVA.Network + , VVA.Account ghc-options: -threaded diff --git a/govtool/frontend/.gitignore b/govtool/frontend/.gitignore index ba36087a4..e32feec72 100644 --- a/govtool/frontend/.gitignore +++ b/govtool/frontend/.gitignore @@ -6,4 +6,7 @@ /.lighthouseci /yarn-error.log .env -coverage \ No newline at end of file +coverage + +# Tool version management file (e.g., asdf version manager) +.tool-versions diff --git a/govtool/frontend/package-lock.json b/govtool/frontend/package-lock.json index 0dc3da411..2492bd6d6 100644 --- a/govtool/frontend/package-lock.json +++ b/govtool/frontend/package-lock.json @@ -1,21 +1,21 @@ { "name": "@govtool/frontend", - "version": "2.0.22", + "version": "2.0.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@govtool/frontend", - "version": "2.0.22", + "version": "2.0.23", "hasInstallScript": true, "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@emurgo/cardano-serialization-lib-asmjs": "^14.1.1", "@hookform/resolvers": "^3.3.1", - "@intersect.mbo/govtool-outcomes-pillar-ui": "v1.4.2", + "@intersect.mbo/govtool-outcomes-pillar-ui": "v1.4.3", "@intersect.mbo/intersectmbo.org-icons-set": "^1.0.8", - "@intersect.mbo/pdf-ui": "0.7.0-beta-29", + "@intersect.mbo/pdf-ui": "0.7.0-beta-35", "@mui/icons-material": "^5.14.3", "@mui/material": "^5.14.4", "@rollup/plugin-babel": "^6.0.4", @@ -3390,9 +3390,9 @@ } }, "node_modules/@intersect.mbo/govtool-outcomes-pillar-ui": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@intersect.mbo/govtool-outcomes-pillar-ui/-/govtool-outcomes-pillar-ui-1.4.2.tgz", - "integrity": "sha512-NpoRLzNFvmuojsOX6ZknWVjmTu2gnTCf8fzP2yjcnOQA+2YPR94NMIszYT8mHfYuRd4M0+Cvt0ssl37CqQdf+w==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@intersect.mbo/govtool-outcomes-pillar-ui/-/govtool-outcomes-pillar-ui-1.4.3.tgz", + "integrity": "sha512-WPdTV30joLgaLGSiezvHLrzAJSOlUAgtQ4Ln4thhXKpjGQlBrLroGGn1uG5pKEaWxTtz5KVjdbFpMkSXB26bdA==", "license": "ISC", "dependencies": { "@fontsource/poppins": "^5.0.14", @@ -3424,9 +3424,9 @@ "license": "ISC" }, "node_modules/@intersect.mbo/pdf-ui": { - "version": "0.7.0-beta-29", - "resolved": "https://registry.npmjs.org/@intersect.mbo/pdf-ui/-/pdf-ui-0.7.0-beta-29.tgz", - "integrity": "sha512-vuC37RW0E0YxaROCX9IbnknvWhlhpP3zEbRVDEyz7qTJM8yMigOi0qdXMhkbKs4dXcvuHLoJcMS090u22V2wRA==", + "version": "0.7.0-beta-35", + "resolved": "https://registry.npmjs.org/@intersect.mbo/pdf-ui/-/pdf-ui-0.7.0-beta-35.tgz", + "integrity": "sha512-S0HAl4pwJm+AqzGxEEHxQ59VsU6Tbvnn5w7F6jG0jd7zUHSCKJAt3PhJQ6VliA5FoOI4z43a2xm7J/9PK+Q9IA==", "dependencies": { "@emurgo/cardano-serialization-lib-asmjs": "^12.0.0-beta.2", "@fontsource/poppins": "^5.0.14", diff --git a/govtool/frontend/package.json b/govtool/frontend/package.json index 1db732b12..56e71b22e 100644 --- a/govtool/frontend/package.json +++ b/govtool/frontend/package.json @@ -1,7 +1,7 @@ { "name": "@govtool/frontend", "private": true, - "version": "2.0.22", + "version": "2.0.23", "type": "module", "scripts": { "build": "vite build", @@ -27,9 +27,9 @@ "@emotion/styled": "^11.11.0", "@emurgo/cardano-serialization-lib-asmjs": "^14.1.1", "@hookform/resolvers": "^3.3.1", - "@intersect.mbo/govtool-outcomes-pillar-ui": "v1.4.2", + "@intersect.mbo/govtool-outcomes-pillar-ui": "v1.4.3", "@intersect.mbo/intersectmbo.org-icons-set": "^1.0.8", - "@intersect.mbo/pdf-ui": "0.7.0-beta-29", + "@intersect.mbo/pdf-ui": "0.7.0-beta-35", "@mui/icons-material": "^5.14.3", "@mui/material": "^5.14.4", "@rollup/plugin-babel": "^6.0.4", diff --git a/govtool/frontend/src/App.tsx b/govtool/frontend/src/App.tsx index 3ef7a5c0a..c4903b4d8 100644 --- a/govtool/frontend/src/App.tsx +++ b/govtool/frontend/src/App.tsx @@ -93,9 +93,8 @@ export default () => { }, [checkTheWalletIsActive]); return ( - <> + - } /> } /> @@ -226,6 +225,6 @@ export default () => { {modals[modal.type].component!} )} - + ); }; diff --git a/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/CreateGovernanceActionForm.tsx b/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/CreateGovernanceActionForm.tsx index 5f3e9b010..fbf3f0bd6 100644 --- a/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/CreateGovernanceActionForm.tsx +++ b/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/CreateGovernanceActionForm.tsx @@ -50,6 +50,8 @@ export const CreateGovernanceActionForm = ({ | GovernanceActionType.NewCommittee | GovernanceActionType.NewConstitution | GovernanceActionType.NoConfidence + | GovernanceActionType.HardForkInitiation + | GovernanceActionType.ParameterChange ], ).some( (field) => !watch(field as unknown as Parameters[0]), @@ -73,6 +75,8 @@ export const CreateGovernanceActionForm = ({ | GovernanceActionType.NewCommittee | GovernanceActionType.NewConstitution | GovernanceActionType.NoConfidence + | GovernanceActionType.HardForkInitiation + | GovernanceActionType.ParameterChange ], ).map(([key, field]) => { const fieldProps = { @@ -85,6 +89,7 @@ export const CreateGovernanceActionForm = ({ ? t(field.placeholderI18nKey) : undefined, rules: field.rules, + maxLength: field.maxLength, }; if (field.component === GovernanceActionField.Input) { diff --git a/govtool/frontend/src/components/organisms/DRepDetailsCard.tsx b/govtool/frontend/src/components/organisms/DRepDetailsCard.tsx index 82b560c44..8d692e788 100644 --- a/govtool/frontend/src/components/organisms/DRepDetailsCard.tsx +++ b/govtool/frontend/src/components/organisms/DRepDetailsCard.tsx @@ -41,7 +41,8 @@ export const DRepDetailsCard = ({ objectives, paymentAddress, qualifications, - references, + identityReferences, + linkReferences, status, url, view, @@ -75,21 +76,6 @@ export const DRepDetailsCard = ({ validate(); }, [url]); - const groupedReferences = references?.reduce>( - (acc, reference) => { - const type = reference["@type"]; - if (!acc[type]) { - acc[type] = []; - } - acc[type].push(reference); - return acc; - }, - {}, - ); - - const linkReferences = groupedReferences?.Link; - const identityReferences = groupedReferences?.Identity; - return ( { useEffect(() => { const isProposalNotFound = - (error as AxiosError)?.response?.data === - `Proposal with id: ${fullProposalId} not found`; + error instanceof AxiosError && + error.response?.data.match(/Proposal with id: .* not found/); if (isProposalNotFound && fullProposalId) { navigate( OUTCOMES_PATHS.governanceActionOutcomes.replace(":id", fullProposalId), diff --git a/govtool/frontend/src/components/organisms/DashboardTopNav.tsx b/govtool/frontend/src/components/organisms/DashboardTopNav.tsx index 45d51d8d3..ef0d91dd7 100644 --- a/govtool/frontend/src/components/organisms/DashboardTopNav.tsx +++ b/govtool/frontend/src/components/organisms/DashboardTopNav.tsx @@ -8,7 +8,10 @@ import { useGetVoterInfo, useScreenDimension, } from "@hooks"; -import { DashboardDrawerMobile } from "@organisms"; +import { + DashboardDrawerMobile, + useMaintenanceEndingBannerContext, +} from "@organisms"; import { useCardano } from "@context"; type DashboardTopNavProps = { @@ -28,6 +31,8 @@ export const DashboardTopNav = ({ const { isEnableLoading } = useCardano(); const { voter } = useGetVoterInfo(); const { dRepVotingPower } = useGetDRepVotingPowerQuery(voter); + const { height: maintenanceEndingBannerHeight } = + useMaintenanceEndingBannerContext(); const openDrawer = () => { setIsDrawerOpen(true); @@ -53,7 +58,7 @@ export const DashboardTopNav = ({ alignItems: "center", backdropFilter: "blur(10px)", backgroundColor: - windowScroll > POSITION_TO_BLUR + windowScroll > POSITION_TO_BLUR + maintenanceEndingBannerHeight ? "rgba(256, 256, 256, 0.7)" : isMobile ? "#FBFBFF59" @@ -65,7 +70,7 @@ export const DashboardTopNav = ({ minHeight: isMobile ? 36 : 48, px: isMobile ? 2 : 5, py: 3, - top: 0, + top: maintenanceEndingBannerHeight || 0, width: "fill-available", zIndex: 100, }} diff --git a/govtool/frontend/src/components/organisms/Drawer.tsx b/govtool/frontend/src/components/organisms/Drawer.tsx index 2ed7256af..d54c729a0 100644 --- a/govtool/frontend/src/components/organisms/Drawer.tsx +++ b/govtool/frontend/src/components/organisms/Drawer.tsx @@ -7,6 +7,7 @@ import { useFeatureFlag } from "@context"; import { useGetVoterInfo } from "@hooks"; import { WalletInfoCard, DRepInfoCard } from "@molecules"; import { openInNewTab } from "@utils"; +import { useMaintenanceEndingBannerContext } from "./MaintenanceEndingBanner"; export const Drawer = () => { const { @@ -14,6 +15,8 @@ export const Drawer = () => { isGovernanceOutcomesPillarEnabled, } = useFeatureFlag(); const { voter } = useGetVoterInfo(); + const { height: maintenanceEndingBannerHeight } = + useMaintenanceEndingBannerContext(); return ( { flexDirection: "column", height: "100vh", position: "sticky", - top: 0, width: `${DRAWER_WIDTH}px`, - + top: maintenanceEndingBannerHeight || 0, overflowY: "auto", - maxHeight: "100vh", + maxHeight: `calc(100vh - ${maintenanceEndingBannerHeight || 0}px)`, }} > { if (loadUserData) { const data: DRepData = state ?? yourselfDRep; - const groupedReferences = data?.references?.reduce< - Record - >((acc, reference) => { - const type = reference["@type"]; - if (!acc[type]) { - acc[type] = []; - } - acc[type].push(reference); - return acc; - }, {}); + reset({ ...data, - objectives: data?.objectives ?? "", - motivations: data?.motivations ?? "", - qualifications: data?.qualifications ?? "", - paymentAddress: data?.paymentAddress ?? "", - image: data?.image ?? "", - linkReferences: groupedReferences?.Link ?? [getEmptyReference("Link")], - identityReferences: groupedReferences?.Identity ?? [ - getEmptyReference("Identity"), - ], + objectives: data?.objectives ?? defaultEditDRepInfoValues.objectives, + motivations: data?.motivations ?? defaultEditDRepInfoValues.motivations, + qualifications: + data?.qualifications ?? defaultEditDRepInfoValues.qualifications, + paymentAddress: + data?.paymentAddress ?? defaultEditDRepInfoValues.paymentAddress, + image: data?.image ?? defaultEditDRepInfoValues.image, + linkReferences: + Array.isArray(data?.linkReferences) && data.linkReferences.length > 0 + ? data.linkReferences + : defaultEditDRepInfoValues.linkReferences, + identityReferences: + Array.isArray(data?.identityReferences) && data.identityReferences.length > 0 + ? data.identityReferences + : defaultEditDRepInfoValues.identityReferences, }); } }, [yourselfDRep, loadUserData]); @@ -83,9 +81,3 @@ export const EditDRepForm = ({ ); }; - -const getEmptyReference = (type: "Link" | "Identity") => ({ - "@type": type, - uri: "", - label: "", -}); diff --git a/govtool/frontend/src/components/organisms/MaintenanceEndingBanner.tsx b/govtool/frontend/src/components/organisms/MaintenanceEndingBanner/MaintenanceEndingBanner.tsx similarity index 72% rename from govtool/frontend/src/components/organisms/MaintenanceEndingBanner.tsx rename to govtool/frontend/src/components/organisms/MaintenanceEndingBanner/MaintenanceEndingBanner.tsx index 37d207e72..669a98820 100644 --- a/govtool/frontend/src/components/organisms/MaintenanceEndingBanner.tsx +++ b/govtool/frontend/src/components/organisms/MaintenanceEndingBanner/MaintenanceEndingBanner.tsx @@ -1,24 +1,28 @@ -import { Box, Typography, IconButton } from "@mui/material"; -import { useState } from "react"; +import { Box, Typography, IconButton, Link } from "@mui/material"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; import { Trans, useTranslation } from "react-i18next"; +import { useMaintenanceEndingBannerContext } from "./MaintenanceEndingBannerContext"; + +const EXPANDED_HEIGHT = 135; +const COLLAPSED_HEIGHT = 50; export const MaintenanceEndingBanner = () => { - const [isExpanded, setIsExpanded] = useState(true); + const { ref, isExpanded, toggleExpanded } = + useMaintenanceEndingBannerContext(); const { t } = useTranslation(); - const handleToggle = () => { - setIsExpanded((prev) => !prev); - }; - return ( {/* Banner Header */} @@ -44,7 +48,7 @@ export const MaintenanceEndingBanner = () => { { {/* Expandable Content */} { /> - {t("system.maintenanceEnding.description3")} + {t("system.maintenanceEnding.description3")}{" "} + + {t("system.maintenanceEnding.linkText")} + diff --git a/govtool/frontend/src/components/organisms/MaintenanceEndingBanner/MaintenanceEndingBannerContext.tsx b/govtool/frontend/src/components/organisms/MaintenanceEndingBanner/MaintenanceEndingBannerContext.tsx new file mode 100644 index 000000000..f57e25d8d --- /dev/null +++ b/govtool/frontend/src/components/organisms/MaintenanceEndingBanner/MaintenanceEndingBannerContext.tsx @@ -0,0 +1,114 @@ +import { + createContext, + createRef, + useContext, + useMemo, + useRef, + useState, + useLayoutEffect, + useEffect, +} from "react"; +import { + getItemFromLocalStorage, + MAINTENANCE_ENDING_BANNER_EXPANDED_KEY, + setItemToLocalStorage, +} from "@/utils"; + +interface MaintenanceEndingBannerContextType { + ref: React.RefObject; + height: number; + isExpanded: boolean; + toggleExpanded: () => void; +} + +const MaintenanceEndingBannerContext = + createContext({ + ref: createRef(), + height: 0, + isExpanded: true, + toggleExpanded: () => {}, + }); + +export const MaintenanceEndingBannerProvider = ({ + children, +}: React.PropsWithChildren) => { + const ref = useRef(null); + const [height, setHeight] = useState(0); + const [isExpanded, setIsExpanded] = useState(true); + + useEffect(() => { + const storedValue = getItemFromLocalStorage( + MAINTENANCE_ENDING_BANNER_EXPANDED_KEY, + ); + if (storedValue !== null) { + setIsExpanded(JSON.parse(storedValue)); + } + }, []); + + const toggleExpanded = () => { + setItemToLocalStorage(MAINTENANCE_ENDING_BANNER_EXPANDED_KEY, !isExpanded); + setIsExpanded((prev) => !prev); + }; + + useLayoutEffect(() => { + let frameId: number | null = null; + + const updatePosition = () => { + if (ref.current) { + const rect = ref.current.getBoundingClientRect(); + + setHeight(rect.height); + } + }; + + const throttledUpdate = () => { + // Context skipping update - frame already queued + if (frameId) return; + + frameId = requestAnimationFrame(() => { + updatePosition(); + frameId = null; + }); + }; + + updatePosition(); + + window.addEventListener("scroll", throttledUpdate, { passive: true }); + window.addEventListener("resize", throttledUpdate); + + return () => { + window.removeEventListener("scroll", throttledUpdate); + window.removeEventListener("resize", throttledUpdate); + + if (frameId) { + cancelAnimationFrame(frameId); + } + }; + }, []); + + const value = useMemo( + () => ({ + ref, + height, + isExpanded, + toggleExpanded, + }), + [ref, height, isExpanded, toggleExpanded], + ); + + return ( + + {children} + + ); +}; + +export const useMaintenanceEndingBannerContext = () => { + const context = useContext(MaintenanceEndingBannerContext); + if (!context) { + throw new Error( + "useMaintenanceEndingBannerContext must be used within a MaintenanceEndingBannerProvider", + ); + } + return context; +}; diff --git a/govtool/frontend/src/components/organisms/MaintenanceEndingBanner/index.ts b/govtool/frontend/src/components/organisms/MaintenanceEndingBanner/index.ts new file mode 100644 index 000000000..e3253604a --- /dev/null +++ b/govtool/frontend/src/components/organisms/MaintenanceEndingBanner/index.ts @@ -0,0 +1,2 @@ +export * from "./MaintenanceEndingBanner"; +export * from "./MaintenanceEndingBannerContext"; diff --git a/govtool/frontend/src/components/organisms/RegisterAsDRepSteps/WhatRetirementMeans.tsx b/govtool/frontend/src/components/organisms/RegisterAsDRepSteps/WhatRetirementMeans.tsx index 2f3421839..3ea078657 100644 --- a/govtool/frontend/src/components/organisms/RegisterAsDRepSteps/WhatRetirementMeans.tsx +++ b/govtool/frontend/src/components/organisms/RegisterAsDRepSteps/WhatRetirementMeans.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useState } from "react"; import * as Sentry from "@sentry/react"; import { Typography } from "@atoms"; @@ -35,10 +35,6 @@ export const WhatRetirementMeans = ({ closeModal(); }; - useEffect(() => { - Sentry.setTag("component_name", "WhatRetirementMeans"); - }, []); - const retireAsDrep = useCallback(async () => { try { setIsRetirementLoading(true); diff --git a/govtool/frontend/src/components/organisms/TopBanners.tsx b/govtool/frontend/src/components/organisms/TopBanners.tsx index f4cd741b8..ad4a878f8 100644 --- a/govtool/frontend/src/components/organisms/TopBanners.tsx +++ b/govtool/frontend/src/components/organisms/TopBanners.tsx @@ -1,13 +1,13 @@ +import { PropsWithChildren } from "react"; import { Box, Link, Typography } from "@mui/material"; import { Trans, useTranslation } from "react-i18next"; -import { useAppContext, useCardano } from "@/context"; +import { useAppContext } from "@/context"; import { LINKS } from "@/consts/links"; import { MaintenanceEndingBanner } from "./MaintenanceEndingBanner"; -export const TopBanners = () => { +export const TopBanners = ({ children }: PropsWithChildren) => { const { isMainnet, networkName, isInBootstrapPhase, isAppInitializing } = useAppContext(); - const { isEnabled } = useCardano(); const { t } = useTranslation(); if (isAppInitializing) { @@ -15,7 +15,7 @@ export const TopBanners = () => { } return ( - <> + {/* NETWORK BANNER */} {!isMainnet && ( @@ -49,18 +49,7 @@ export const TopBanners = () => { {/* GOVTOOL MAINTENANCE ENDING SOON BANNER */} - {isEnabled && ( - - - - )} + {/* BOOTSTRAPPING BANNER */} @@ -92,6 +81,7 @@ export const TopBanners = () => { )} - + {children} + ); }; diff --git a/govtool/frontend/src/components/organisms/TopNav.tsx b/govtool/frontend/src/components/organisms/TopNav.tsx index 5eb2654c5..2d8da5cf0 100644 --- a/govtool/frontend/src/components/organisms/TopNav.tsx +++ b/govtool/frontend/src/components/organisms/TopNav.tsx @@ -8,7 +8,7 @@ import { useCardano, useFeatureFlag, useModal } from "@context"; import { useScreenDimension, useTranslation } from "@hooks"; import { openInNewTab } from "@utils"; import { DrawerMobile } from "./DrawerMobile"; -import { MaintenanceEndingBanner } from "./MaintenanceEndingBanner"; +import { useMaintenanceEndingBannerContext } from "./MaintenanceEndingBanner"; const POSITION_TO_BLUR = 80; @@ -26,6 +26,9 @@ export const TopNav = ({ isConnectButton = true }) => { const navigate = useNavigate(); const { t } = useTranslation(); + const { height: maintenanceEndingBannerHeight } = + useMaintenanceEndingBannerContext(); + useEffect(() => { const onScroll = () => { if (!containerRef.current?.nextElementSibling) return; @@ -137,9 +140,14 @@ export const TopNav = ({ isConnectButton = true }) => { ); return ( - - - + (null); * @param children - The child components to render. */ const AppContextProvider = ({ children }: PropsWithChildren) => { - useEffect(() => { - Sentry.setTag("component_name", "AppContextProvider"); - }, []); const { fetchEpochParams, epochParams } = useGetEpochParams(); const { fetchNetworkInfo, networkInfo } = useGetNetworkInfo(); diff --git a/govtool/frontend/src/context/contextProviders.tsx b/govtool/frontend/src/context/contextProviders.tsx index b17e122be..f456018bb 100644 --- a/govtool/frontend/src/context/contextProviders.tsx +++ b/govtool/frontend/src/context/contextProviders.tsx @@ -8,6 +8,8 @@ import { GovernanceActionProvider } from "./governanceAction"; import { AdaHandleProvider } from "./adaHandle"; import { ProposalDiscussionProvider } from "./proposalDiscussion"; +import { MaintenanceEndingBannerProvider } from "@/components/organisms/MaintenanceEndingBanner/MaintenanceEndingBannerContext"; + interface Props { children: React.ReactNode; } @@ -21,7 +23,11 @@ const ContextProviders = ({ children }: Props) => ( - {children} + + + {children} + + diff --git a/govtool/frontend/src/context/governanceAction.tsx b/govtool/frontend/src/context/governanceAction.tsx index ecb94c6c0..112513d45 100644 --- a/govtool/frontend/src/context/governanceAction.tsx +++ b/govtool/frontend/src/context/governanceAction.tsx @@ -4,7 +4,6 @@ import { createContext, useContext, useCallback, - useEffect, } from "react"; import { NodeObject } from "jsonld"; import { blake2bHex } from "blakejs"; @@ -43,10 +42,6 @@ const GovernanceActionProvider = ({ children }: PropsWithChildren) => { * @returns The JSON-LD representation of the governance action. */ - useEffect(() => { - Sentry.setTag("component_name", "GovernanceActionProvider"); - }, []); - const createGovernanceActionJsonLD = useCallback( async (govActionMetadata: GovActionMetadata) => { try { diff --git a/govtool/frontend/src/context/usersnapContext.tsx b/govtool/frontend/src/context/usersnapContext.tsx index 2b8643f0f..0100e443b 100644 --- a/govtool/frontend/src/context/usersnapContext.tsx +++ b/govtool/frontend/src/context/usersnapContext.tsx @@ -6,6 +6,7 @@ import React, { useMemo, } from "react"; import { InitOptions, WidgetApi, loadSpace } from "@usersnap/browser"; +import { useTranslation } from "react-i18next"; type WidgetValues = { assignee?: string; @@ -65,6 +66,7 @@ export const UsersnapProvider = ({ children, }: UsersnapProviderProps) => { const [usersnapApi, setUsersnapApi] = useState(null); + const { t } = useTranslation(); const openFeedbackWindow = useCallback(() => { if (usersnapApi) { @@ -85,7 +87,7 @@ export const UsersnapProvider = ({ } }; initUsersnapSpace(); - }, [initParams, API_KEY]); + }, [initParams, API_KEY, t]); const value = useMemo(() => ({ openFeedbackWindow }), [openFeedbackWindow]); diff --git a/govtool/frontend/src/context/wallet.tsx b/govtool/frontend/src/context/wallet.tsx index a9a38f2a9..cced1021c 100644 --- a/govtool/frontend/src/context/wallet.tsx +++ b/govtool/frontend/src/context/wallet.tsx @@ -130,7 +130,7 @@ type TreasuryProps = { type ProtocolParameterChangeProps = { prevGovernanceActionHash: string; - prevGovernanceActionIndex: number; + prevGovernanceActionIndex: string; protocolParamsUpdate: Partial; } & VotingAnchor; @@ -166,7 +166,6 @@ export type QuorumThreshold = { numerator: string; denominator: string; }; - type ProtocolParamsUpdate = { adaPerUtxo: string; collateralPercentage: number; @@ -226,6 +225,7 @@ interface CardanoContextType { setStakeKey: (key: string) => void; stakeKeys: string[]; walletApi?: CardanoApiWallet; + walletName?: string; registeredStakeKeysListState: string[]; buildSignSubmitConwayCertTx: ({ certBuilder, @@ -314,6 +314,7 @@ const CardanoProvider = (props: Props) => { changeAddress: undefined, usedAddress: undefined, }); + const [walletName, setWalletName] = useState(undefined); const { t } = useTranslation(); const epochParams = getItemFromLocalStorage(PROTOCOL_PARAMS_KEY); @@ -355,25 +356,25 @@ const CardanoProvider = (props: Props) => { const isStakeKeyRegistered = () => !!registeredStakeKeysListState.length; const enable = useCallback( - async (walletName: string) => { - setIsEnableLoading(walletName); + async (name: string) => { + setIsEnableLoading(name); await checkIsMaintenanceOn(); - // todo: use .getSupportedExtensions() to check if wallet supports CIP-95 - if (!isEnabled && walletName) { + // TODO: use .getSupportedExtensions() to check if wallet supports CIP-95 + if (!isEnabled && name) { try { // Check that this wallet supports CIP-95 connection - if (!window.cardano[walletName].supportedExtensions) { + if (!window.cardano[name].supportedExtensions) { throw new Error(t("errors.walletNoCIP30Support")); } else if ( - !window.cardano[walletName].supportedExtensions.some( + !window.cardano[name].supportedExtensions.some( (item) => item.cip === 95, ) ) { throw new Error(t("errors.walletNoCIP30Nor90Support")); } // Enable wallet connection - const enabledApi: CardanoApiWallet = await window.cardano[walletName] + const enabledApi: CardanoApiWallet = await window.cardano[name] .enable({ extensions: [{ cip: 95 }], }) @@ -382,7 +383,7 @@ const CardanoProvider = (props: Props) => { category: "wallet", message: "Wallet connected", level: "info", - data: window.cardano[walletName], + data: window.cardano[name], }); return enabledWalletApi; }) @@ -394,6 +395,7 @@ const CardanoProvider = (props: Props) => { setIsEnabled(true); setWalletApi(enabledApi); + setWalletName(name); // Check if wallet has enabled the CIP-95 extension const enabledExtensions = await enabledApi.getExtensions(); if (!enabledExtensions.some((item) => item.cip === 95)) { @@ -479,7 +481,7 @@ const CardanoProvider = (props: Props) => { const dRepIDs = await getPubDRepID(enabledApi); setPubDRepKey(dRepIDs?.dRepKey || ""); setDRepID(dRepIDs?.dRepID || ""); - setItemToLocalStorage(`${WALLET_LS_KEY}_name`, walletName); + setItemToLocalStorage(`${WALLET_LS_KEY}_name`, name); return { status: t("ok"), stakeKey: stakeKeySet }; } catch (e) { @@ -513,6 +515,7 @@ const CardanoProvider = (props: Props) => { setAddress(undefined); setStakeKey(undefined); setIsEnabled(false); + setWalletName(undefined); Sentry.addBreadcrumb({ category: "wallet", @@ -758,8 +761,8 @@ const CardanoProvider = (props: Props) => { // TODO: type error // eslint-disable-next-line @typescript-eslint/no-shadow, @typescript-eslint/no-explicit-any } catch (error: any) { - const walletName = getItemFromLocalStorage(`${WALLET_LS_KEY}_name`); - const isWalletConnected = await window.cardano[walletName].isEnabled(); + const name = getItemFromLocalStorage(`${WALLET_LS_KEY}_name`); + const isWalletConnected = await window.cardano[name].isEnabled(); if (!isWalletConnected) { disconnectWallet(); @@ -1343,7 +1346,7 @@ const CardanoProvider = (props: Props) => { if (prevGovernanceActionHash && prevGovernanceActionIndex) { const prevGovernanceActionId = GovernanceActionId.new( TransactionHash.from_hex(prevGovernanceActionHash), - prevGovernanceActionIndex, + Number(prevGovernanceActionIndex), ); protocolParamChangeAction = ParameterChangeAction.new_with_policy_hash_and_action_id( @@ -1490,6 +1493,7 @@ const CardanoProvider = (props: Props) => { stakeKey, stakeKeys, walletApi, + walletName, }), [ address, @@ -1524,6 +1528,7 @@ const CardanoProvider = (props: Props) => { stakeKey, stakeKeys, walletApi, + walletName, ], ); diff --git a/govtool/frontend/src/hooks/forms/useCreateGovernanceActionForm.ts b/govtool/frontend/src/hooks/forms/useCreateGovernanceActionForm.ts index f792c2bfa..c13eb5bb6 100644 --- a/govtool/frontend/src/hooks/forms/useCreateGovernanceActionForm.ts +++ b/govtool/frontend/src/hooks/forms/useCreateGovernanceActionForm.ts @@ -1,4 +1,10 @@ -import { Dispatch, SetStateAction, useCallback, useState } from "react"; +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useState, +} from "react"; import { useNavigate } from "react-router-dom"; import { useFormContext } from "react-hook-form"; import { blake2bHex } from "blakejs"; @@ -60,6 +66,8 @@ export const useCreateGovernanceActionForm = ( buildNewConstitutionGovernanceAction, buildUpdateCommitteeGovernanceAction, buildSignSubmitConwayCertTx, + buildHardForkGovernanceAction, + buildProtocolParameterChangeGovernanceAction, } = useCardano(); // App Management @@ -85,6 +93,12 @@ export const useCreateGovernanceActionForm = ( } = useFormContext(); const govActionType = watch("governance_action_type"); + useEffect(() => { + if (govActionType === GovernanceActionType.ParameterChange) { + setValue("protocolParameters", JSON.stringify(protocolParams)); + } + }, [govActionType]); + // Navigation const backToForm = useCallback(() => { setStep?.(3); @@ -212,6 +226,48 @@ export const useCreateGovernanceActionForm = ( return buildTreasuryGovernanceAction(treasuryActionDetails); } + case GovernanceActionType.HardForkInitiation: { + if ( + data.major === undefined || + data.minor === undefined || + data.prevGovernanceActionHash === undefined || + data.prevGovernanceActionIndex === undefined + ) { + throw new Error( + t("errors.invalidHardForkInitiationGovernanceActionType"), + ); + } + const hardForkActionDetails = { + ...commonGovActionDetails, + prevGovernanceActionHash: data.prevGovernanceActionHash, + prevGovernanceActionIndex: data.prevGovernanceActionIndex, + major: data.major, + minor: data.minor, + }; + return buildHardForkGovernanceAction(hardForkActionDetails); + } + + case GovernanceActionType.ParameterChange: { + if ( + data.protocolParameters === undefined || + data.prevGovernanceActionHash === undefined || + data.prevGovernanceActionIndex === undefined + ) { + throw new Error( + t("errors.invalidParameterChangeGovernanceActionType"), + ); + } + const protocolParamsUpdate = JSON.parse(data.protocolParameters); + const parameterChangeActionDetails = { + ...commonGovActionDetails, + protocolParamsUpdate, + prevGovernanceActionHash: data.prevGovernanceActionHash, + prevGovernanceActionIndex: data.prevGovernanceActionIndex, + }; + return buildProtocolParameterChangeGovernanceAction( + parameterChangeActionDetails, + ); + } default: throw new Error(t("errors.invalidGovernanceActionType")); } diff --git a/govtool/frontend/src/i18n/locales/en.json b/govtool/frontend/src/i18n/locales/en.json index d057b97cb..b25cf774d 100644 --- a/govtool/frontend/src/i18n/locales/en.json +++ b/govtool/frontend/src/i18n/locales/en.json @@ -205,6 +205,21 @@ "label": "New Constitution Script Hash", "placeholder": "New Constitution Script Hash", "tip": "Script hash of the new constitution" + }, + "major": { + "label": "Major", + "placeholder": "Major version", + "tip": "Major version of the new hard fork" + }, + "minor": { + "label": "Minor", + "placeholder": "Minor version", + "tip": "Minor version of the new hard fork" + }, + "protocolParameters": { + "label": "Protocol Parameters", + "placeholder": "Protocol parameters", + "tip": "Protocol parameters to be changed" } }, "validations": { @@ -740,10 +755,11 @@ "title": "This tool is connected to {{networkName}}", "bootstrappingWarning": "Govtool is in the Bootstrapping phase. Some features are not available. Learn more", "maintenanceEnding": { - "title": "⚠️ GovTool Maintenance Ending Soon", - "description1": "GovTool has not been included in the 2025 Cardano budget.", - "description2": "Unless alternative support is secured, <0>maintenance will end in June 2025.", - "description3": "We remain committed to transparency and will keep you updated." + "title": "⚠️ Funding for GovTool is at risk", + "description1": "GovTool was not included in the current Cardano budget.", + "description2": "Without new support, active development and <0>maintenance will end in June 2025.", + "description3": "Learn what this means and what’s next:", + "linkText": "The future of GovTool" } }, "tooltips": { diff --git a/govtool/frontend/src/main.tsx b/govtool/frontend/src/main.tsx index f9b4a9a7f..469c54445 100644 --- a/govtool/frontend/src/main.tsx +++ b/govtool/frontend/src/main.tsx @@ -54,6 +54,12 @@ Sentry.init({ }, }); +Sentry.setTag("pdf_ui_version", pkg.dependencies["@intersect.mbo/pdf-ui"]); +Sentry.setTag( + "govtool_outcomes_pillar_ui_version", + pkg.dependencies["@intersect.mbo/govtool-outcomes-pillar-ui"], +); + ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( diff --git a/govtool/frontend/src/models/api.ts b/govtool/frontend/src/models/api.ts index bc374ccad..0b6dbd473 100644 --- a/govtool/frontend/src/models/api.ts +++ b/govtool/frontend/src/models/api.ts @@ -150,6 +150,12 @@ export enum DRepListSort { Status = "Status", } +type Reference = { + "@type": "Identity" | "Links"; + label: string; + uri: string; +}; + export type DrepDataDTO = { deposit: number; drepId: string; @@ -163,6 +169,8 @@ export type DrepDataDTO = { view: string; votingPower?: number; imageUrl: string | null; + identityReferences: Reference[]; + linkReferences: Reference[]; // either base64 for IPFS image or URL for regular image image: string | null; }; @@ -173,7 +181,6 @@ export type DRepData = DrepDataDTO & { objectives: string | null; motivations: string | null; qualifications: string | null; - references: Reference[]; doNotList: boolean; imageUrl: string | null; // either base64 for IPFS image or URL for regular image @@ -261,3 +268,10 @@ type DRepVotingPower = { }; export type DRepVotingPowerListResponse = DRepVotingPower[]; + +export type Account = { + id: number, + view: string, + isRegistered: boolean, + isScriptBased: boolean +} diff --git a/govtool/frontend/src/pages/Dashboard.tsx b/govtool/frontend/src/pages/Dashboard.tsx index 684558337..88648389d 100644 --- a/govtool/frontend/src/pages/Dashboard.tsx +++ b/govtool/frontend/src/pages/Dashboard.tsx @@ -4,10 +4,12 @@ import { Box } from "@mui/material"; import { Background, ScrollToManage } from "@atoms"; import { + BUDGET_DISCUSSION_PATHS, CONNECTED_NAV_ITEMS, DRAWER_WIDTH, OUTCOMES_PATHS, PATHS, + PDF_PATHS, } from "@consts"; import { useCardano } from "@context"; import { useScreenDimension, useTranslation } from "@hooks"; @@ -34,18 +36,37 @@ export const Dashboard = () => { return outcomesNavItem ?? ""; } + if (path.startsWith(BUDGET_DISCUSSION_PATHS.budgetDiscussion)) { + const budgetNavItem = findNavItem( + CONNECTED_NAV_ITEMS, + BUDGET_DISCUSSION_PATHS.budgetDiscussion, + ); + + return budgetNavItem ?? ""; + } + + if (path.startsWith(PDF_PATHS.proposalDiscussion)) { + const proposalDiscussionNavItem = findNavItem( + CONNECTED_NAV_ITEMS, + PDF_PATHS.proposalDiscussion, + ); + + return proposalDiscussionNavItem ?? ""; + } return findNavItem(CONNECTED_NAV_ITEMS, path) ?? ""; }; - const findNavItem = (items: NavItem[], targetPath: string): string | null => ( - items.reduce((result, item) => ( - result ?? ( - targetPath === item.navTo + const findNavItem = (items: NavItem[], targetPath: string): string | null => + items.reduce( + (result, item) => + result ?? + (targetPath === item.navTo ? item.label - : (item.childNavItems ? findNavItem(item.childNavItems, targetPath) : null) - ) - ), null) - ); + : item.childNavItems + ? findNavItem(item.childNavItems, targetPath) + : null), + null, + ); useEffect(() => { if (divRef.current && pathname !== PATHS.dashboardGovernanceActions) { @@ -67,7 +88,12 @@ export const Dashboard = () => { return ( - + {isMobile ? null : } { useEffect(() => { const isProposalNotFound = - (error as AxiosError)?.response?.data === - `Proposal with id: ${fullProposalId} not found`; + error instanceof AxiosError && + error.response?.data.match(/Proposal with id: .* not found/); if (isProposalNotFound && fullProposalId) { navigate( OUTCOMES_PATHS.governanceActionOutcomes.replace(":id", fullProposalId), diff --git a/govtool/frontend/src/pages/ProposalDiscussion.tsx b/govtool/frontend/src/pages/ProposalDiscussion.tsx index bbc0e8efe..196f73c15 100644 --- a/govtool/frontend/src/pages/ProposalDiscussion.tsx +++ b/govtool/frontend/src/pages/ProposalDiscussion.tsx @@ -12,6 +12,7 @@ import { useValidateMutation } from "@/hooks/mutations"; import { useScreenDimension } from "@/hooks/useScreenDimension"; import { Footer, TopNav } from "@/components/organisms"; import { useGetDRepVotingPowerList, useGetVoterInfo } from "@/hooks"; +import { getAdaHolderVotingPower, getAccount } from "@/services"; const ProposalDiscussion = React.lazy( () => import("@intersect.mbo/pdf-ui/cjs"), @@ -79,6 +80,8 @@ export const ProposalDiscussionPillar = () => { username={username} setUsername={setUsername} epochParams={epochParams} + getAdaHolderVotingPower={getAdaHolderVotingPower} + getAccount={getAccount} {...snackbarContext} /> diff --git a/govtool/frontend/src/services/requests/getAccount.ts b/govtool/frontend/src/services/requests/getAccount.ts new file mode 100644 index 000000000..f6badbebf --- /dev/null +++ b/govtool/frontend/src/services/requests/getAccount.ts @@ -0,0 +1,8 @@ +import { Account } from "@/models"; +import { API } from "../API"; + +export const getAccount = async ({ stakeKey }: { stakeKey?: string }) => { + const response = await API.get(`/account/${stakeKey || ""}`); + + return response.data; +}; diff --git a/govtool/frontend/src/services/requests/getAdaHolderVotingPower.ts b/govtool/frontend/src/services/requests/getAdaHolderVotingPower.ts index 79f73d925..c08b17b30 100644 --- a/govtool/frontend/src/services/requests/getAdaHolderVotingPower.ts +++ b/govtool/frontend/src/services/requests/getAdaHolderVotingPower.ts @@ -5,7 +5,9 @@ export const getAdaHolderVotingPower = async ({ }: { stakeKey?: string; }) => { - const response = await API.get(`/ada-holder/get-voting-power/${stakeKey}`); + const response = await API.get( + `/ada-holder/get-voting-power/${stakeKey}`, + ); return response.data; }; diff --git a/govtool/frontend/src/services/requests/getProposal.ts b/govtool/frontend/src/services/requests/getProposal.ts index 65744d6d0..a4aeda00b 100644 --- a/govtool/frontend/src/services/requests/getProposal.ts +++ b/govtool/frontend/src/services/requests/getProposal.ts @@ -7,10 +7,10 @@ export const getProposal = async ( proposalId: string, drepId?: string, ): Promise => { - const isCIP129Identifier = proposalId.includes("gov_action"); + const isCIP129Identifier = proposalId.startsWith("gov_action"); if (isCIP129Identifier) { - const { txID } = decodeCIP129Identifier(proposalId); - proposalId = txID; + const { txID, index } = decodeCIP129Identifier(proposalId); + proposalId = `${txID}#${parseInt(index, 16)}`; } const encodedHash = encodeURIComponent(proposalId); diff --git a/govtool/frontend/src/services/requests/index.ts b/govtool/frontend/src/services/requests/index.ts index 0448747f2..598fe76d3 100644 --- a/govtool/frontend/src/services/requests/index.ts +++ b/govtool/frontend/src/services/requests/index.ts @@ -22,3 +22,4 @@ export * from "./postDRepRemoveVote"; export * from "./postDRepRetire"; export * from "./postDRepVote"; export * from "./getDRepVotingPowerList"; +export * from "./getAccount"; diff --git a/govtool/frontend/src/stories/DRepDetailsCard.stories.ts b/govtool/frontend/src/stories/DRepDetailsCard.stories.ts index 17754f8c9..71625f098 100644 --- a/govtool/frontend/src/stories/DRepDetailsCard.stories.ts +++ b/govtool/frontend/src/stories/DRepDetailsCard.stories.ts @@ -28,29 +28,31 @@ const meta = { "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras semper tortor ullamcorper volutpat vehicula. Duis varius orci a elit luctus, in fringilla nisl fringilla. Fusce pellentesque convallis dapibus. In hac habitasse platea dictumst. Nunc efficitur ipsum at ipsum blandit, ac eleifend purus pulvinar. Pellentesque orci quam, interdum eget massa id, sollicitudin lacinia turpis. Nullam lectus quam, congue commodo sollicitudin in, pretium sit amet metus. Integer pretium, odio eu dictum posuere.", qualifications: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc porta iaculis sodales. Praesent non nisi fermentum, porta sem in, porta arcu. In dignissim pulvinar est eu dignissim. Duis vitae vehicula dui. Praesent posuere egestas lacus, at pulvinar elit tempus ut. Etiam vulputate, lorem in accumsan.", - references: [ + linkReferences: [ { - "@type": "Link", + "@type": "Links", label: "Link Reference", uri: "https://example.com/", }, { - "@type": "Link", + "@type": "Links", label: "Another Link Reference", uri: "https://example.com/", }, + ], + identityReferences: [ { "@type": "Identity", label: "Identity Reference", uri: "https://example.com/", }, { - "@type": "GovernanceMetadata", + "@type": "Identity", label: "GovernanceMetadata Reference", uri: "https://example.com/", }, { - "@type": "Other", + "@type": "Identity", label: "Other Reference", uri: "https://example.com/", }, diff --git a/govtool/frontend/src/stories/GovernanceActionDetailsCard.stories.ts b/govtool/frontend/src/stories/GovernanceActionDetailsCard.stories.ts index 67e6001f7..7d1b14eab 100644 --- a/govtool/frontend/src/stories/GovernanceActionDetailsCard.stories.ts +++ b/govtool/frontend/src/stories/GovernanceActionDetailsCard.stories.ts @@ -59,7 +59,7 @@ const commonArgs = { metadataHash: "exampleMetadataHash", references: [ { - "@type": "Reference", + "@type": "Links", uri: "https://exampleurl.com", label: "Example label", }, diff --git a/govtool/frontend/src/types/@intersect.mbo.d.ts b/govtool/frontend/src/types/@intersect.mbo.d.ts index 5db4fed7b..813a34eca 100644 --- a/govtool/frontend/src/types/@intersect.mbo.d.ts +++ b/govtool/frontend/src/types/@intersect.mbo.d.ts @@ -5,7 +5,7 @@ enum MetadataValidationStatus { INCORRECT_FORMAT = "INCORRECT_FORMAT", } declare module "@intersect.mbo/pdf-ui/cjs" { - import { EpochParams } from "@/models"; + import { EpochParams, Account } from "@/models"; type ProposalDiscussionProps = { pdfApiUrl: string; @@ -32,6 +32,16 @@ declare module "@intersect.mbo/pdf-ui/cjs" { epochParams?: EpochParams; username: string; setUsername: (username: string) => void; + getAdaHolderVotingPower: ({ + stakeKey, + }: { + stakeKey?: string; + }) => Promise; + getAccount: ({ + stakeKey, + }: { + stakeKey?: string; + }) => Promise; }; type GovernanceActionsOutcomesProps = { diff --git a/govtool/frontend/src/types/governanceAction.ts b/govtool/frontend/src/types/governanceAction.ts index 77bad990a..bdc53934f 100644 --- a/govtool/frontend/src/types/governanceAction.ts +++ b/govtool/frontend/src/types/governanceAction.ts @@ -12,8 +12,8 @@ export enum GovernanceActionType { } export enum GovernanceActionField { - Input = "input", - TextArea = "textarea", + Input = "Input", + TextArea = "TextArea", } export type FieldSchema = { @@ -22,6 +22,7 @@ export type FieldSchema = { placeholderI18nKey: NestedKeys; tipI18nKey?: NestedKeys; rules?: Omit; + maxLength?: number; }; // Following properties are based on [CIP-108](https://github.com/Ryun1/CIPs/blob/governance-metadata-actions/CIP-0108/README.md) @@ -60,19 +61,27 @@ export type NewConstitutionActionFieldSchema = Partial<{ constitutionHash: FieldSchema; scriptHash: FieldSchema; }>; +export type ProtocolParameterActionFieldSchema = Partial<{ + prevGovernanceActionHash: FieldSchema; + prevGovernanceActionIndex: FieldSchema; + protocolParameters: FieldSchema; +}>; export type GovernanceActionFieldSchemas = | SharedGovernanceActionFieldSchema & TreasuryGovernanceActionFieldSchema & NewCommitteeActionFieldSchema & HardForkInitiationActionFieldSchema & - NewConstitutionActionFieldSchema; + NewConstitutionActionFieldSchema & + ProtocolParameterActionFieldSchema; export type GovernanceActionFields = Record< | GovernanceActionType.InfoAction | GovernanceActionType.TreasuryWithdrawals | GovernanceActionType.NoConfidence | GovernanceActionType.NewCommittee - | GovernanceActionType.NewConstitution, + | GovernanceActionType.NewConstitution + | GovernanceActionType.HardForkInitiation + | GovernanceActionType.ParameterChange, GovernanceActionFieldSchemas >; diff --git a/govtool/frontend/src/utils/getGovActionId.ts b/govtool/frontend/src/utils/getGovActionId.ts index 4b38e447a..e85d13e6b 100644 --- a/govtool/frontend/src/utils/getGovActionId.ts +++ b/govtool/frontend/src/utils/getGovActionId.ts @@ -9,8 +9,10 @@ export const getShortenedGovActionId = ( const firstPart = txHash.slice(0, 4); const lastPart = txHash.slice(-4); - return `${firstPart}...${lastPart}#${index}`; + return txHash.startsWith("gov_action") + ? `${firstPart}...${lastPart}` + : `${firstPart}...${lastPart}#${index}`; }; export const getFullGovActionId = (txHash: string, index: number | string) => - `${txHash}#${index}`; + (txHash.startsWith("gov_action") ? txHash : `${txHash}#${index}`); diff --git a/govtool/frontend/src/utils/localStorage.ts b/govtool/frontend/src/utils/localStorage.ts index 0bc866244..b1d6c9ae2 100644 --- a/govtool/frontend/src/utils/localStorage.ts +++ b/govtool/frontend/src/utils/localStorage.ts @@ -3,6 +3,8 @@ export const PROTOCOL_PARAMS_KEY = "protocol_params"; export const NETWORK_METRICS_KEY = "network_metrics"; export const NETWORK_INFO_KEY = "network_info"; export const NETWORK_TOTAL_STAKE_KEY = "network_total_stake"; +export const MAINTENANCE_ENDING_BANNER_EXPANDED_KEY = + "maintenance_ending_banner_expanded"; export const WALLET_LS_KEY = "wallet_data"; diff --git a/govtool/frontend/src/utils/tests/dRep.test.ts b/govtool/frontend/src/utils/tests/dRep.test.ts index 00a06a4f2..f35f7cbd0 100644 --- a/govtool/frontend/src/utils/tests/dRep.test.ts +++ b/govtool/frontend/src/utils/tests/dRep.test.ts @@ -14,7 +14,8 @@ const EXAMPLE_DREP: DRepData = { status: DRepStatus.Active, type: "DRep" as TDRepType, givenName: "name", - references: [], + identityReferences: [], + linkReferences: [], latestRegistrationDate: "2024-07-10", paymentAddress: null, objectives: null, diff --git a/govtool/frontend/src/utils/tests/getGovActionId.test.ts b/govtool/frontend/src/utils/tests/getGovActionId.test.ts index c0e92949f..a7ce6fa4d 100644 --- a/govtool/frontend/src/utils/tests/getGovActionId.test.ts +++ b/govtool/frontend/src/utils/tests/getGovActionId.test.ts @@ -28,6 +28,13 @@ describe("getShortenedGovActionId", () => { const result = getShortenedGovActionId(txHash, index); expect(result).toBe("#1"); }); + + it("should handle a hash starting with 'gov_action' correctly", () => { + const txHash = "gov_action_1234567890abcdef"; + const index = 7; + const result = getShortenedGovActionId(txHash, index); + expect(result).toBe("gov_...cdef"); + }); }); describe("getFullGovActionId", () => { @@ -37,4 +44,11 @@ describe("getFullGovActionId", () => { const result = getFullGovActionId(txHash, index); expect(result).toBe("1234567890abcdef1234567890abcdef#10"); }); + + it("should return the full id without index if txHash starts with 'gov_action'", () => { + const txHash = "gov_action_1234567890abcdef"; + const index = 5; + const result = getFullGovActionId(txHash, index); + expect(result).toBe("gov_action_1234567890abcdef"); + }); }); diff --git a/govtool/metadata-validation/.gitignore b/govtool/metadata-validation/.gitignore index 4b56acfbe..b0c0e90e0 100644 --- a/govtool/metadata-validation/.gitignore +++ b/govtool/metadata-validation/.gitignore @@ -54,3 +54,6 @@ pids # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Tool version management file (e.g., asdf version manager) +.tool-versions diff --git a/govtool/metadata-validation/package-lock.json b/govtool/metadata-validation/package-lock.json index d425f918b..59f24cd09 100644 --- a/govtool/metadata-validation/package-lock.json +++ b/govtool/metadata-validation/package-lock.json @@ -1,12 +1,12 @@ { "name": "@govtool/metadata-validation", - "version": "2.0.22", + "version": "2.0.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@govtool/metadata-validation", - "version": "2.0.22", + "version": "2.0.23", "license": "UNLICENSED", "dependencies": { "@nestjs/axios": "^3.0.2", diff --git a/govtool/metadata-validation/package.json b/govtool/metadata-validation/package.json index 26d09b2d6..865f15b43 100644 --- a/govtool/metadata-validation/package.json +++ b/govtool/metadata-validation/package.json @@ -1,6 +1,6 @@ { "name": "@govtool/metadata-validation", - "version": "2.0.22", + "version": "2.0.23", "description": "", "author": "", "private": true, diff --git a/govtool/metadata-validation/src/main.ts b/govtool/metadata-validation/src/main.ts index faf7322c7..5dd0961fb 100644 --- a/govtool/metadata-validation/src/main.ts +++ b/govtool/metadata-validation/src/main.ts @@ -13,7 +13,7 @@ async function bootstrap() { const config = new DocumentBuilder() .setTitle('Metadata Validation Tool') .setDescription('The Metadata Validation Tool API description') - .setVersion("2.0.22") + .setVersion("2.0.23") .build(); const document = SwaggerModule.createDocument(app, config); diff --git a/tests/govtool-frontend/playwright/lib/constants/index.ts b/tests/govtool-frontend/playwright/lib/constants/index.ts index 407772241..11fe3a300 100644 --- a/tests/govtool-frontend/playwright/lib/constants/index.ts +++ b/tests/govtool-frontend/playwright/lib/constants/index.ts @@ -1,5 +1,5 @@ import { faker } from "@faker-js/faker"; -import { InvalidMetadataType } from "@types"; +import { BudgetProposalFilterTypes, InvalidMetadataType } from "@types"; export const SECURITY_RELEVANT_PARAMS_MAP: Record = { maxBlockBodySize: "max_block_size", @@ -61,3 +61,15 @@ export const InvalidMetadata: InvalidMetadataType[] = [ hash: "e71bf6171adda3754a87fff5c2d8d9e404eb3366428a5be13f7e76357a39004f", }, ]; + +export const budgetProposalfilterOptionNames: Array = + [ + "Newest", + "Oldest", + "Most comments", + "Least comments", + "Name A-Z", + "Name Z-A", + "Proposer A-Z", + "Proposer Z-A", + ]; diff --git a/tests/govtool-frontend/playwright/lib/helpers/auth.ts b/tests/govtool-frontend/playwright/lib/helpers/auth.ts index 980cfa4f6..44ef71661 100644 --- a/tests/govtool-frontend/playwright/lib/helpers/auth.ts +++ b/tests/govtool-frontend/playwright/lib/helpers/auth.ts @@ -2,7 +2,7 @@ import { importWallet } from "@fixtures/importWallet"; import { valid as mockValid } from "@mock/index"; import LoginPage from "@pages/loginPage"; import ProposalDiscussionPage from "@pages/proposalDiscussionPage"; -import { BrowserContext, Page } from "@playwright/test"; +import { BrowserContext, expect, Page } from "@playwright/test"; import { ProposalType, StaticWallet } from "@types"; import { ShelleyWallet } from "./crypto"; import convertBufferToHex from "./convertBufferToHex"; @@ -57,9 +57,16 @@ export async function createAuthWithUserName({ const proposalDiscussionPage = new ProposalDiscussionPage(page); await proposalDiscussionPage.goto(); - await proposalDiscussionPage.verifyIdentityBtn.click({ timeout: 15_000 }); - - await proposalDiscussionPage.setUsername(mockValid.username()); + await proposalDiscussionPage.verifyIdentityBtn.click({ timeout: 60_000 }); + try { + await expect(page.getByTestId("username-input")).toBeVisible({ + timeout: 10_000, + }); + await proposalDiscussionPage.setUsername(mockValid.username()); + } catch (error) { + // Ignore error if username is already set + console.log("Username is already set"); + } await context.storageState({ path: auth }); } diff --git a/tests/govtool-frontend/playwright/lib/helpers/cardano.ts b/tests/govtool-frontend/playwright/lib/helpers/cardano.ts index e4ff03501..582d2608a 100644 --- a/tests/govtool-frontend/playwright/lib/helpers/cardano.ts +++ b/tests/govtool-frontend/playwright/lib/helpers/cardano.ts @@ -50,7 +50,7 @@ export async function skipIfNotInfoAndBootstrapping(type: ProposalType) { export async function skipIfMainnet() { if (environments.networkId === 1) { await allure.description( - "Ada spendable features are not available on mainnet." + "Test skipped on mainnet to prevent affecting the production environment or using real ADA." ); test.skip(); } diff --git a/tests/govtool-frontend/playwright/lib/helpers/crypto.ts b/tests/govtool-frontend/playwright/lib/helpers/crypto.ts index 62442ac9f..13bcd9512 100644 --- a/tests/govtool-frontend/playwright/lib/helpers/crypto.ts +++ b/tests/govtool-frontend/playwright/lib/helpers/crypto.ts @@ -2,7 +2,12 @@ import environments from "../constants/environments"; import { ed25519 as ed } from "@noble/curves/ed25519"; import { bech32 } from "bech32"; import * as blake from "blakejs"; -import { rewardAddressBech32, rewardAddressRawBytes } from "./shellyWallet"; +import { + addressBech32, + addressRawBytes, + rewardAddressBech32, + rewardAddressRawBytes, +} from "./shellyWallet"; const KEY_HASH_LENGTH = 28; const ADDR_LENGTH = KEY_HASH_LENGTH * 2 + 1; @@ -103,20 +108,15 @@ export class ShelleyWallet { } addressBech32(networkId: number): string { - const prefix = networkId == 0 ? "addr_test" : "addr"; - return bech32.encode( - prefix, - bech32.toWords(Buffer.from(this.addressRawBytes(networkId))), - 200 - ); + const stakePkh = Buffer.from(this.stakeKey.pkh).toString("hex"); + const paymentPkh = Buffer.from(this.paymentKey.pkh).toString("hex"); + return addressBech32(networkId, paymentPkh, stakePkh); } addressRawBytes(networkId) { - const concatenatedArray1 = new Uint8Array(ADDR_LENGTH); - concatenatedArray1[0] = networkId; - concatenatedArray1.set(this.paymentKey.pkh, 1); - concatenatedArray1.set(this.stakeKey.pkh, KEY_HASH_LENGTH + 1); - return concatenatedArray1; + const stakePkh = Buffer.from(this.stakeKey.pkh).toString("hex"); + const paymentPkh = Buffer.from(this.paymentKey.pkh).toString("hex"); + return addressRawBytes(networkId, paymentPkh, stakePkh); } rewardAddressRawBytes(network: number) { diff --git a/tests/govtool-frontend/playwright/lib/helpers/shellyWallet.ts b/tests/govtool-frontend/playwright/lib/helpers/shellyWallet.ts index aa8001906..8c2e57082 100644 --- a/tests/govtool-frontend/playwright/lib/helpers/shellyWallet.ts +++ b/tests/govtool-frontend/playwright/lib/helpers/shellyWallet.ts @@ -2,7 +2,9 @@ import { bech32 } from "bech32"; import { blake2bHex } from "blakejs"; import convertBufferToHex from "./convertBufferToHex"; import { ShelleyWallet } from "./crypto"; + const KEY_HASH_LENGTH = 28; +const ADDR_LENGTH = KEY_HASH_LENGTH * 2 + 1; export default function extractDRepFromWallet(wallet: ShelleyWallet) { const dRepPubKey = convertBufferToHex(wallet.dRepKey.public); @@ -36,6 +38,36 @@ export function rewardAddressBech32( ); } +export function addressBech32( + networkId: number, + paymentPkh: string, + stakePkh: string +): string { + const prefix = networkId == 0 ? "addr_test" : "addr"; + return bech32.encode( + prefix, + bech32.toWords( + Buffer.from(addressRawBytes(networkId, paymentPkh, stakePkh)) + ), + 200 + ); +} + +export function addressRawBytes( + networkId: number, + paymentPkh: string, + stakePkh: string +) { + const concatenatedArray1 = new Uint8Array(ADDR_LENGTH); + concatenatedArray1[0] = networkId; + concatenatedArray1.set(Uint8Array.from(Buffer.from(paymentPkh, "hex")), 1); + concatenatedArray1.set( + Uint8Array.from(Buffer.from(stakePkh, "hex")), + KEY_HASH_LENGTH + 1 + ); + return concatenatedArray1; +} + export async function generateWallets(num: number) { return await Promise.all( Array.from({ length: num }, () => diff --git a/tests/govtool-frontend/playwright/lib/helpers/transaction.ts b/tests/govtool-frontend/playwright/lib/helpers/transaction.ts index 95ca3bfa5..1cd3e914c 100644 --- a/tests/govtool-frontend/playwright/lib/helpers/transaction.ts +++ b/tests/govtool-frontend/playwright/lib/helpers/transaction.ts @@ -126,8 +126,8 @@ export async function registerDRepForWallet(wallet: ShelleyWallet) { wallet: wallet.json(), }; const registrationRes = await kuberService.dRepRegistration( - convertBufferToHex(wallet.stakeKey.private), - convertBufferToHex(wallet.stakeKey.pkh), + convertBufferToHex(wallet.dRepKey.private), + convertBufferToHex(wallet.dRepKey.pkh), metadataAnchorAndWallet ); await pollTransaction(registrationRes.txId, registrationRes.lockInfo); diff --git a/tests/govtool-frontend/playwright/lib/pages/budgetDiscussionPage.ts b/tests/govtool-frontend/playwright/lib/pages/budgetDiscussionPage.ts index 3ca0fa9db..571d0688a 100644 --- a/tests/govtool-frontend/playwright/lib/pages/budgetDiscussionPage.ts +++ b/tests/govtool-frontend/playwright/lib/pages/budgetDiscussionPage.ts @@ -1,6 +1,10 @@ import { functionWaitedAssert, waitedLoop } from "@helpers/waitedLoop"; import { expect, Locator, Page } from "@playwright/test"; -import { BudgetDiscussionEnum, ProposedGovAction } from "@types"; +import { + BudgetDiscussionEnum, + BudgetProposalFilterTypes, + ProposedGovAction, +} from "@types"; import environments from "lib/constants/environments"; import BudgetDiscussionDetailsPage from "./budgetDiscussionDetailsPage"; @@ -140,16 +144,29 @@ export default class BudgetDiscussionPage { } async sortAndValidate( - option: "asc" | "desc", + type: BudgetProposalFilterTypes, validationFn: (p1: ProposedGovAction, p2: ProposedGovAction) => boolean ) { + const sortMappings = { + "Proposer A-Z": "&sort[creator][govtool_username]=ASC", + "Proposer Z-A": "&sort[creator][govtool_username]=DESC", + "Name A-Z": "&sort[bd_proposal_detail][proposal_name]=ASC", + "Name Z-A": "&sort[bd_proposal_detail][proposal_name]=DESC", + "Most comments": "&sort[prop_comments_number]=DESC", + "Least comments": "&sort[prop_comments_number]=ASC", + Oldest: "&sort[createdAt]=ASC", + Newest: "&sort[createdAt]=DESC", + }; + + const urlParam = sortMappings[type]; + const populateParam = "&populate[0]=bd_costing"; + const responsePromise = this.page.waitForResponse((response) => - response - .url() - .includes(`&sort[createdAt]=${option}&populate[0]=bd_costing`) + response.url().includes(`${urlParam}${populateParam}`) ); await this.sortBtn.click(); + await this.page.getByTestId(`${type}-sort-option`).click(); const response = await responsePromise; let proposals: ProposedGovAction[] = (await response.json()).data; diff --git a/tests/govtool-frontend/playwright/lib/pages/budgetDiscussionSubmissionPage.ts b/tests/govtool-frontend/playwright/lib/pages/budgetDiscussionSubmissionPage.ts index b37520f54..96cc46d98 100644 --- a/tests/govtool-frontend/playwright/lib/pages/budgetDiscussionSubmissionPage.ts +++ b/tests/govtool-frontend/playwright/lib/pages/budgetDiscussionSubmissionPage.ts @@ -51,13 +51,8 @@ export default class BudgetDiscussionSubmissionPage { readonly verifyIdentityBtn = this.page.getByTestId("verify-identity-button"); readonly saveDraftBtn = this.page.getByTestId("draft-button"); readonly submitBtn = this.page.getByTestId("submit-button"); - readonly countryOfIncorporationBtn = this.page.getByTestId( - "country-of-incorporation" - ); - readonly agreeCheckbox = this.page.getByLabel( - "I agree to the information in" - ); //BUG missing test Ids + readonly agreeCheckbox = this.page.getByTestId("agree-checkbox"); readonly submitCheckbox = this.page.getByTestId("submit-checkbox"); // input @@ -65,18 +60,18 @@ export default class BudgetDiscussionSubmissionPage { readonly linkUrlInput = this.page.getByTestId("link-0-url-input"); // proposal-ownership - readonly companyNameInput = this.page.getByLabel("Company Name *"); //BUG missing test Ids - readonly companyDomainNameInput = this.page.getByLabel( - "Company Domain Name *" - ); //BUG missing test Ids - readonly groupNameInput = this.page.getByLabel("Group Name *"); //BUG missing test Ids - readonly groupTypeInput = this.page.getByLabel("Type of Group *"); //BUG missing test Ids - readonly keyInformationOfGroupInput = this.page.getByLabel( - "Key Information to Identify" - ); //BUG missing test Ids - readonly contactDetailsInput = this.page.getByLabel( - "Please provide your preferred" - ); //BUG missing test Ids + readonly companyNameInput = this.page.getByTestId("company-name-input"); + readonly companyDomainNameInput = this.page.getByTestId( + "company-domain-input" + ); + readonly groupNameInput = this.page.getByTestId("group-name-input"); + readonly groupTypeInput = this.page.getByTestId("group-type-input"); + readonly keyInformationOfGroupInput = this.page.getByTestId( + "group-identity-information-input" + ); + readonly contactDetailsInput = this.page.getByTestId( + "provide-preferred-input" + ); // problem-statements readonly problemStatementInput = this.page.getByTestId( @@ -88,56 +83,50 @@ export default class BudgetDiscussionSubmissionPage { readonly suplimentaryEndorsementInput = this.page.getByTestId( "supplementary-endorsement-input" ); - readonly productRoadmapDescriptionInput = this.page.getByLabel( - "Please explain how your" - ); // BUG missing test Ids + readonly productRoadmapDescriptionInput = this.page.getByTestId( + "proposal-roadmap-description-input" + ); // proposal-details - readonly proposalNameInput = this.page.getByLabel( - "What is your proposed name to" - ); //BUG missing testId + readonly proposalNameInput = this.page.getByTestId("proposal-name-input"); readonly proposalDescriptionInput = this.page.getByTestId( "proposal-description-input" ); readonly proposalKeyDependenciesInput = this.page.getByTestId( "key-dependencies-input" ); - readonly proposalMaintainAndSupportInput = this.page.getByLabel( - "How will this proposal be" - ); //BUG missing testId + readonly proposalMaintainAndSupportInput = this.page.getByTestId( + "proposal-maintain-and-support-input" + ); readonly milestonesInput = this.page.getByTestId( "key-proposal-deliverables-input" ); readonly teamSizeAndDurationInput = this.page.getByTestId( "resourcing-duration-estimates-input" ); - readonly previousExperienceInput = this.page.getByLabel( - "Please provide previous" - ); //BUG missing testId - readonly otherDescriptionInput = this.page.getByLabel( - "Please describe what you have" + readonly previousExperienceInput = this.page.getByTestId( + "proposal-previous-experience-input" + ); + readonly otherDescriptionInput = this.page.getByTestId( + "other-contract-description" ); // costing - readonly adaAmountInput = this.page.getByLabel("ADA Amount *"); //BUG missing test Ids - readonly usaToAdaCnversionRateInput = this.page.getByLabel( - "USD to ADA Conversion Rate *" - ); //BUG missing test Ids - readonly preferredCurrencyInput = this.page.getByLabel( - "Amount in preferred currency *" + readonly adaAmountInput = this.page.getByTestId("ada-amount-input"); + readonly usaToAdaCnversionRateInput = this.page.getByTestId( + "usd-ada-conversion-input" + ); + readonly preferredCurrencyInput = this.page.getByTestId( + "preferred-currency-amount-input" ); readonly costBreakdownInput = this.page.getByTestId("cost-breakdown-input"); readonly venderDetailsInput = this.page.getByLabel("Please provide further"); //BUG missing test Ids // select - readonly beneficiaryCountrySelect = this.page.getByTestId( - "beneficiary-country-of-residence" - ); - readonly beneficiaryNationalitySelect = this.page.getByTestId( - "beneficiary-nationality" + readonly proposalCommittee = this.page.getByTestId("proposal-committee"); + readonly countryOfIncorporationBtn = this.page.getByTestId( + "country-of-incorporation" ); - - readonly companyTypeSelect = this.page.getByTestId("beneficiary-type"); readonly publicChampionSelect = this.page.getByTestId( "proposal-public-champion" ); @@ -234,9 +223,9 @@ export default class BudgetDiscussionSubmissionPage { // costing readonly adaAmountContent = this.page.getByTestId("ada-amount-content"); - readonly adaToUsdConversionRateContent = this.page.getByTestId( + readonly usdToAdaConversionRateContent = this.page.getByTestId( "usd-to-ada-conversion-rate-content" - ); // BUG typo + ); readonly preferredCurrencyContent = this.page.getByTestId( "preferred-currency-content" ); @@ -271,10 +260,10 @@ export default class BudgetDiscussionSubmissionPage { proposalOwnership: BudgetProposalOwnershipProps, isNaviagted = true ) { - await this.companyTypeSelect.click(); + await this.proposalCommittee.click(); await this.page - .getByRole("option", { name: proposalOwnership.companyType }) - .click(); //BUG missing testId + .getByTestId(`${proposalOwnership.companyType.toLowerCase()}-submission`) + .click(); await this.contactDetailsInput.fill(proposalOwnership.contactDetails); @@ -391,10 +380,10 @@ export default class BudgetDiscussionSubmissionPage { await this.usaToAdaCnversionRateInput.fill( costing.usdToAdaConversionRate.toString() ); - await this.costBreakdownInput.fill(costing.costBreakdown); await this.preferredCurrencyInput.fill( costing.AmountInPreferredCurrency.toString() ); + await this.costBreakdownInput.fill(costing.costBreakdown); } async fillupCostingForm(costing: BudgetCostingProps) { @@ -772,7 +761,7 @@ export default class BudgetDiscussionSubmissionPage { await expect(this.adaAmountContent).toHaveText( `₳ ${formatWithThousandSeparator(proposalInformations.costing.adaAmount)}` ); - await expect(this.adaToUsdConversionRateContent).toHaveText( + await expect(this.usdToAdaConversionRateContent).toHaveText( proposalInformations.costing.usdToAdaConversionRate.toString() ); @@ -796,13 +785,9 @@ export default class BudgetDiscussionSubmissionPage { // further information for (let i = 0; i < proposalInformations.furtherInformation.length; i++) { - //BUG missing testId - await expect( - this.currentPage.getByRole("link", { - name: proposalInformations.furtherInformation[i].prop_link_text, - exact: true, - }) - ).toBeVisible(); + await expect(this.currentPage.getByTestId(`link-${i}-label`)).toHaveText( + proposalInformations.furtherInformation[i].prop_link_text + ); } // administration and auditing @@ -836,7 +821,7 @@ export default class BudgetDiscussionSubmissionPage { proposalOwnership: BudgetProposalOwnershipProps, isValid: boolean = true ) { - const companyTypeSelectContent = await this.companyTypeSelect.textContent(); + const companyTypeSelectContent = await this.proposalCommittee.textContent(); if (isValid) { if (proposalOwnership.companyType === "Company") { diff --git a/tests/govtool-frontend/playwright/lib/pages/outcomeDetailsPage.ts b/tests/govtool-frontend/playwright/lib/pages/outcomeDetailsPage.ts index dd94a1c6b..cbf39d9ca 100644 --- a/tests/govtool-frontend/playwright/lib/pages/outcomeDetailsPage.ts +++ b/tests/govtool-frontend/playwright/lib/pages/outcomeDetailsPage.ts @@ -106,6 +106,10 @@ export default class OutcomeDetailsPage { isLoggedIn ); + if (!govActionDetailsPage) { + return; + } + const outcomeResponse = await outcomeResponsePromise; const proposalToCheck = (await outcomeResponse.json())[0]; @@ -267,7 +271,7 @@ export default class OutcomeDetailsPage { { message: `CC "Abstain" vote count checked for ${currentPageUrl}`, } - ).toHaveText(`Abstain Votes${proposalToCheck.pool_abstain_votes}`); //BUG missing testIds + ).toHaveText(`Abstain Votes${proposalToCheck.cc_abstain_votes}`); //BUG missing testIds const noPercentage = 100 - parseFloat(yesPercentage.replace("%", "")); await expect(govActionDetailsPage.ccCommitteeNoVotes, { diff --git a/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts b/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts index 031dfa5e9..915cb2c67 100644 --- a/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts +++ b/tests/govtool-frontend/playwright/lib/pages/outcomesPage.ts @@ -177,8 +177,7 @@ export default class OutComesPage { await this.page.getByTestId(this.getSortTestId(sortOption)).click(); const response = await responsePromise; - const data = await response.json(); - let outcomeProposalList: outcomeProposal[] = data.length != 0 ? data : null; + const outcomeProposalList: outcomeProposal[] = await response.json(); // API validation if (outcomeProposalList.length <= 1) return; @@ -374,25 +373,30 @@ export default class OutComesPage { } async fetchOutcomeIdAndTitleFromNetwork( - governanceActionId: string, - governanceActionTitle: string - ) { + governanceActionId?: string, + governanceActionTitle?: string + ): Promise<{ governanceActionId: string; governanceActionTitle: string }> { let updatedGovernanceActionId = governanceActionId; let updatedGovernanceActionTitle = governanceActionTitle; + await this.page.route( "**/governance-actions?search=&filters=&sort=**", async (route) => { const response = await route.fetch(); const data: outcomeProposal[] = await response.json(); - if (!governanceActionId) { - if (data.length > 0) { - const randomIndexForId = Math.floor(Math.random() * data.length); - updatedGovernanceActionId = - data[randomIndexForId].tx_hash + - "#" + - data[randomIndexForId].index; + + if (!updatedGovernanceActionId && data.length > 0) { + const randomIndex = Math.floor(Math.random() * data.length); + updatedGovernanceActionId = `${data[randomIndex].tx_hash}#${data[randomIndex].index}`; + } + + if (!updatedGovernanceActionTitle) { + const itemWithTitle = data.find((item) => item.title != null); + if (itemWithTitle) { + updatedGovernanceActionTitle = itemWithTitle.title; } } + await route.fulfill({ status: 200, contentType: "application/json", @@ -410,31 +414,39 @@ export default class OutComesPage { await route.continue(); return; } + const data: outcomeMetadata = await response.json(); - if (!governanceActionTitle && data.data.title != null) { + if (!updatedGovernanceActionTitle && data.data.title) { updatedGovernanceActionTitle = data.data.title; } + await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(data), }); } catch (error) { + // Just return without handling the error return; } } ); - const responsePromise = this.page.waitForResponse( + const actionsResponsePromise = this.page.waitForResponse( "**/governance-actions?search=&filters=&sort=**" ); - const metadataResponsePromise = this.page.waitForResponse( - "**/governance-actions/metadata?**" - ); await this.goto(); - await responsePromise; - await metadataResponsePromise; + await actionsResponsePromise; + + const needMetadataForTitle = !updatedGovernanceActionTitle; + const metadataResponsePromise = needMetadataForTitle + ? this.page.waitForResponse("**/governance-actions/metadata?**") + : Promise.resolve(null); + if (needMetadataForTitle) { + await metadataResponsePromise; + } + return { governanceActionId: updatedGovernanceActionId, governanceActionTitle: updatedGovernanceActionTitle, @@ -443,10 +455,15 @@ export default class OutComesPage { async searchOutcomesById(governanceActionId: string) { await this.searchInput.fill(governanceActionId); - await expect( - this.page.getByRole("progressbar").getByRole("img") - ).toBeVisible(); + try { + await expect( + this.page.getByRole("progressbar").getByRole("img") + ).toBeVisible(); + } catch (error) { + // Handle the case where the progress bar is not visible + console.warn("Progress bar not visible, proceeding with search."); + } await functionWaitedAssert( async () => { const idSearchOutcomeCards = await this.getAllOutcomes(); @@ -467,9 +484,14 @@ export default class OutComesPage { async searchOutcomesByTitle(governanceActionTitle: string) { await this.searchInput.fill(governanceActionTitle); - await expect( - this.page.getByRole("progressbar").getByRole("img") - ).toBeVisible(); + try { + await expect( + this.page.getByRole("progressbar").getByRole("img") + ).toBeVisible(); + } catch (error) { + // Handle the case where the progress bar is not visible + console.warn("Progress bar not visible, proceeding with search."); + } await functionWaitedAssert( async () => { @@ -532,17 +554,26 @@ export default class OutComesPage { { timeout: 60_000 } ); - const metricsResponsePromise = page.waitForResponse( - (response) => response.url().includes(`/misc/network/metrics?epoch`), - { timeout: 60_000 } - ); - const outcomePage = new OutComesPage(page); await outcomePage.goto({ filter: filterKey }); const outcomeListResponse = await outcomeListResponsePromise; const proposals = await outcomeListResponse.json(); + if (proposals.length === 0) { + expect(true, "No proposals found!").toBeTruthy(); + return { + govActionDetailsPage: null, + outcomeResponsePromise: null, + metricsResponsePromise: null, + }; + } + + const metricsResponsePromise = page.waitForResponse( + (response) => response.url().includes(`/misc/network/metrics?epoch`), + { timeout: 60_000 } + ); + expect( proposals.length, proposals.length == 0 && "No proposals found!" diff --git a/tests/govtool-frontend/playwright/lib/pages/proposalDiscussionPage.ts b/tests/govtool-frontend/playwright/lib/pages/proposalDiscussionPage.ts index 07da72fda..c2c301c39 100644 --- a/tests/govtool-frontend/playwright/lib/pages/proposalDiscussionPage.ts +++ b/tests/govtool-frontend/playwright/lib/pages/proposalDiscussionPage.ts @@ -1,5 +1,10 @@ import { expect, Locator, Page } from "@playwright/test"; -import { ProposalCreateRequest, ProposalType, ProposedGovAction } from "@types"; +import { + ProposalCreateRequest, + ProposalDiscussionFilterTypes, + ProposalType, + ProposedGovAction, +} from "@types"; import environments from "lib/constants/environments"; import ProposalDiscussionDetailsPage from "./proposalDiscussionDetailsPage"; import { functionWaitedAssert, waitedLoop } from "@helpers/waitedLoop"; @@ -147,16 +152,30 @@ export default class ProposalDiscussionPage { } async sortAndValidate( - option: "asc" | "desc", + type: ProposalDiscussionFilterTypes, validationFn: (p1: ProposedGovAction, p2: ProposedGovAction) => boolean ) { + const sortMappings = { + "Name A-Z": "&sort[prop_name]=ASC", + "Name Z-A": "&sort[prop_name]=DESC", + "Most comments": "&sort[proposal][prop_comments_number]=DESC", + "Least comments": "&sort[proposal][prop_comments_number]=ASC", + "Most likes": "&sort[proposal][prop_likes]=DESC", + "Least likes": "&sort[proposal][prop_likes]=ASC", + "Most dislikes": "&sort[proposal][prop_dislikes]=DESC", + "Least dislikes": "&sort[proposal][prop_dislikes]=ASC", + Oldest: "&sort[createdAt]=ASC", + Newest: "&sort[createdAt]=DESC", + }; + + const urlParam = sortMappings[type]; + const populateParam = "&populate[0]=proposal_links"; const responsePromise = this.page.waitForResponse((response) => - response - .url() - .includes(`&sort[createdAt]=${option}&populate[0]=proposal_links`) + response.url().includes(`${urlParam}${populateParam}`) ); await this.sortBtn.click(); + await this.page.getByTestId(`${type}-sort-option`).click(); const response = await responsePromise; let proposals: ProposedGovAction[] = (await response.json()).data; diff --git a/tests/govtool-frontend/playwright/lib/services/kuberService.ts b/tests/govtool-frontend/playwright/lib/services/kuberService.ts index 206a348de..d97e5e514 100644 --- a/tests/govtool-frontend/playwright/lib/services/kuberService.ts +++ b/tests/govtool-frontend/playwright/lib/services/kuberService.ts @@ -177,6 +177,7 @@ const kuberService = { })); const inputs = wallets.map((wallet) => wallet.address); + inputs.push(getWalletConfigForFaucet().address); return kuber.signAndSubmitTx({ inputs, selections, @@ -237,8 +238,8 @@ const kuberService = { }, dRepRegistration: ( - stakeSigningKey: string, - pkh: string, + dRepSigningKey: string, + dRepPkh: string, metadata: WalletAndAnchorType ) => { const kuber = new Kuber( @@ -247,12 +248,12 @@ const kuberService = { ); const req = { - certificates: [Kuber.generateCert("registerdrep", pkh, metadata)], + certificates: [Kuber.generateCert("registerdrep", dRepPkh, metadata)], selections: [ { type: "PaymentSigningKeyShelley_ed25519", description: "Stake Signing Key", - cborHex: `5820${stakeSigningKey}`, + cborHex: `5820${dRepSigningKey}`, }, ], }; @@ -261,7 +262,7 @@ const kuberService = { dRepDeRegistration: ( addr: string, signingKey: string, - stakePrivateKey: string, + dRepPrivateKey: string, pkh: string ) => { const kuber = new Kuber(addr, signingKey); @@ -269,7 +270,7 @@ const kuberService = { { type: "PaymentSigningKeyShelley_ed25519", description: "Payment Signing Key", - cborHex: "5820" + stakePrivateKey, + cborHex: "5820" + dRepPrivateKey, }, ]; const req = { @@ -287,13 +288,13 @@ const kuberService = { ); const req = { certificates: wallets.map((wallet) => - Kuber.generateCert("deregisterdrep", wallet.stake.pkh) + Kuber.generateCert("deregisterdrep", wallet.dRep.pkh) ), selections: wallets.map((wallet) => { return { type: "PaymentSigningKeyShelley_ed25519", description: "Stake Signing Key", - cborHex: `5820${wallet.stake.private}`, + cborHex: `5820${wallet.dRep.private}`, }; }), inputs: getWalletConfigForFaucet().address, diff --git a/tests/govtool-frontend/playwright/lib/types.ts b/tests/govtool-frontend/playwright/lib/types.ts index b336993e8..2ffdac42c 100644 --- a/tests/govtool-frontend/playwright/lib/types.ts +++ b/tests/govtool-frontend/playwright/lib/types.ts @@ -196,13 +196,38 @@ export type ProposalCreateRequest = { has_guardrails?: boolean; is_draft: boolean; }; - export type ProposedGovAction = { id: number; attributes: { gov_action_type_name: string; + prop_comments_number: number; + prop_likes: number; + prop_dislikes: number; createdAt: string; updatedAt: string; + content: { + id: string; + attributes: { + proposal_id: string; + prop_name: string; + }; + }; + creator: { + data: { + id: number; + attributes: { + govtool_username: string; + }; + }; + }; + bd_proposal_detail: { + data: { + id: number; + attributes: { + proposal_name: string; + }; + }; + }; }; }; @@ -521,3 +546,25 @@ export enum BudgetProposalStageEnum { } export type VoterType = "DReps" | "SPOs" | "CC"; + +export type BudgetProposalFilterTypes = + | "Newest" + | "Oldest" + | "Most comments" + | "Least comments" + | "Name A-Z" + | "Name Z-A" + | "Proposer A-Z" + | "Proposer Z-A"; + +export type ProposalDiscussionFilterTypes = + | "Newest" + | "Oldest" + | "Most likes" + | "Least likes" + | "Most dislikes" + | "Least dislikes" + | "Most comments" + | "Least comments" + | "Name A-Z" + | "Name Z-A"; diff --git a/tests/govtool-frontend/playwright/lib/walletManager.ts b/tests/govtool-frontend/playwright/lib/walletManager.ts index 53192e886..23842a22d 100644 --- a/tests/govtool-frontend/playwright/lib/walletManager.ts +++ b/tests/govtool-frontend/playwright/lib/walletManager.ts @@ -65,5 +65,15 @@ class WalletManager { return await LockInterceptor.intercept("tempWallets", popCb); } + + async updateWalletGivenName(address: string, givenName: string) { + const wallets: StaticWallet[] = (await getFile("wallets.json")) ?? []; + wallets.map((wallet: StaticWallet) => { + if (wallet.address === address) { + wallet.givenName = givenName; + } + }); + await createFile("wallets.json", wallets); + } } export default WalletManager.getInstance(); diff --git a/tests/govtool-frontend/playwright/package-lock.json b/tests/govtool-frontend/playwright/package-lock.json index 9f2a897ee..edbd65f52 100644 --- a/tests/govtool-frontend/playwright/package-lock.json +++ b/tests/govtool-frontend/playwright/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@cardanoapi/cardano-test-wallet": "^3.3.1", + "@cardanoapi/cardano-test-wallet": "^3.3.2", "@faker-js/faker": "^8.4.1", "@noble/curves": "^1.3.0", "@noble/ed25519": "^2.0.0", @@ -43,9 +43,9 @@ } }, "node_modules/@cardanoapi/cardano-test-wallet": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@cardanoapi/cardano-test-wallet/-/cardano-test-wallet-3.3.1.tgz", - "integrity": "sha512-AOIDoEkVRYanW4O0pflfcJ2K4Pb3cUHf93cz8pJANU3qIZRQXW+ubGVM60s1WodTcJEmUccpxxXZJuuGRBGXCw==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@cardanoapi/cardano-test-wallet/-/cardano-test-wallet-3.3.2.tgz", + "integrity": "sha512-N80hAiw/9SkEQJyx8lYB0FqfnzCcWI+wzjPHgULzjz9C9b92irE0zvV6X1/GgN96OYLV+V1VCzgkJ7A1aGnCvQ==", "license": "MIT" }, "node_modules/@cbor-extract/cbor-extract-darwin-arm64": { diff --git a/tests/govtool-frontend/playwright/package.json b/tests/govtool-frontend/playwright/package.json index b0127afe7..91e185062 100644 --- a/tests/govtool-frontend/playwright/package.json +++ b/tests/govtool-frontend/playwright/package.json @@ -48,7 +48,7 @@ "generate-faucet-wallet": "ts-node ./generate_faucet_wallet.ts" }, "dependencies": { - "@cardanoapi/cardano-test-wallet": "^3.3.1", + "@cardanoapi/cardano-test-wallet": "^3.3.2", "@faker-js/faker": "^8.4.1", "@noble/curves": "^1.3.0", "@noble/ed25519": "^2.0.0", diff --git a/tests/govtool-frontend/playwright/playwright.config.ts b/tests/govtool-frontend/playwright/playwright.config.ts index 4e8757478..be00da55a 100644 --- a/tests/govtool-frontend/playwright/playwright.config.ts +++ b/tests/govtool-frontend/playwright/playwright.config.ts @@ -61,7 +61,6 @@ export default defineConfig({ { name: "dRep auth setup", testMatch: "**/dRep.auth.setup.ts", - dependencies: environments.ci ? ["dRep setup"] : [], }, { name: "proposal discussion auth setup", @@ -143,7 +142,7 @@ export default defineConfig({ use: { ...devices["Desktop Chrome"] }, testMatch: "**/*.dRep.spec.ts", dependencies: environments.ci - ? ["dRep auth setup"] + ? ["dRep auth setup" , "dRep setup"] : [], teardown: environments.ci && "cleanup artifacts", }, diff --git a/tests/govtool-frontend/playwright/tests/11-proposal-budget/proposalBudget.dRep.pb.spec.ts b/tests/govtool-frontend/playwright/tests/11-proposal-budget/proposalBudget.dRep.pb.spec.ts index 8e38001f0..4c511cf39 100644 --- a/tests/govtool-frontend/playwright/tests/11-proposal-budget/proposalBudget.dRep.pb.spec.ts +++ b/tests/govtool-frontend/playwright/tests/11-proposal-budget/proposalBudget.dRep.pb.spec.ts @@ -35,7 +35,9 @@ test.describe("Budget proposal dRep behaviour", () => { await budgetDiscussionDetailsPage.voteOnPoll(choice); - await expect(budgetDiscussionDetailsPage.pollYesBtn).not.toBeVisible(); + await expect(budgetDiscussionDetailsPage.pollYesBtn).not.toBeVisible({ + timeout: 60_000, + }); await expect(budgetDiscussionDetailsPage.pollNoBtn).not.toBeVisible(); await expect( budgetDiscussionDetailsPage.currentPage.getByTestId( @@ -59,7 +61,9 @@ test.describe("Budget proposal dRep behaviour", () => { await budgetDiscussionDetailsPage.voteOnPoll(choice); await budgetDiscussionDetailsPage.changePollVote(); - await expect(budgetDiscussionDetailsPage.pollYesBtn).not.toBeVisible(); + await expect(budgetDiscussionDetailsPage.pollYesBtn).not.toBeVisible({ + timeout: 60_000, + }); await expect(budgetDiscussionDetailsPage.pollNoBtn).not.toBeVisible(); // vote must be changed @@ -100,25 +104,17 @@ test.describe("Budget proposal dRep behaviour", () => { .locator('[data-testid^="comment-"][data-testid$="-content-card"]') .first(); - await expect( - dRepCommentedCard.getByText("DRep", { exact: true }) - ).toBeVisible(); - - const isDRepGivenNameVisible = await dRepCommentedCard - .getByTestId("given-name") - .isVisible(); - - expect( - isDRepGivenNameVisible, - !isDRepGivenNameVisible && "Missing given-name testId" - ).toBeTruthy(); + await expect(dRepCommentedCard.getByTestId("dRep-tag")).toBeVisible(); - await expect(dRepCommentedCard.getByTestId("given-name")).toHaveText( - dRep03Wallet.givenName + await expect(dRepCommentedCard.getByTestId("dRep-given-name")).toHaveText( + dRep03Wallet.givenName, + { timeout: 60_000 } ); - await expect(dRepCommentedCard.getByTestId("drep-id")).toHaveText( - dRep03Wallet.dRepId - ); + const dRepIdWithoutDotted = ( + await dRepCommentedCard.getByTestId("dRep-id").textContent() + ).replace(/\./g, ""); + + expect(dRep03Wallet.dRepId).toContain(dRepIdWithoutDotted); }); }); diff --git a/tests/govtool-frontend/playwright/tests/11-proposal-budget/proposalBudget.loggedin.pb.spec.ts b/tests/govtool-frontend/playwright/tests/11-proposal-budget/proposalBudget.loggedin.pb.spec.ts index d8747f469..ed705d51a 100644 --- a/tests/govtool-frontend/playwright/tests/11-proposal-budget/proposalBudget.loggedin.pb.spec.ts +++ b/tests/govtool-frontend/playwright/tests/11-proposal-budget/proposalBudget.loggedin.pb.spec.ts @@ -3,6 +3,7 @@ import { budgetProposal01Wallet } from "@constants/staticWallets"; import { faker } from "@faker-js/faker"; import { test } from "@fixtures/walletExtension"; import { setAllureEpic } from "@helpers/allure"; +import { skipIfMainnet } from "@helpers/cardano"; import BudgetDiscussionDetailsPage from "@pages/budgetDiscussionDetailsPage"; import BudgetDiscussionPage from "@pages/budgetDiscussionPage"; import { expect } from "@playwright/test"; @@ -50,6 +51,8 @@ test.describe("Budget proposal logged in state", () => { }); test("11I. Should comments on any proposal", async ({}) => { + await skipIfMainnet(); + const comment = faker.lorem.words(5); await budgetDiscussionDetailsPage.addComment(comment); await expect( @@ -60,6 +63,8 @@ test.describe("Budget proposal logged in state", () => { }); test("11J. Should reply to any comments", async ({}) => { + await skipIfMainnet(); + const randComment = faker.lorem.words(5); const randReply = faker.lorem.words(5); diff --git a/tests/govtool-frontend/playwright/tests/11-proposal-budget/proposalBudget.spec.ts b/tests/govtool-frontend/playwright/tests/11-proposal-budget/proposalBudget.spec.ts index 733ed6fd7..4cf3b5fb3 100644 --- a/tests/govtool-frontend/playwright/tests/11-proposal-budget/proposalBudget.spec.ts +++ b/tests/govtool-frontend/playwright/tests/11-proposal-budget/proposalBudget.spec.ts @@ -1,4 +1,5 @@ import environments from "@constants/environments"; +import { budgetProposalfilterOptionNames } from "@constants/index"; import { faker } from "@faker-js/faker"; import { test } from "@fixtures/walletExtension"; import { setAllureEpic } from "@helpers/allure"; @@ -8,7 +9,11 @@ import { functionWaitedAssert } from "@helpers/waitedLoop"; import BudgetDiscussionDetailsPage from "@pages/budgetDiscussionDetailsPage"; import BudgetDiscussionPage from "@pages/budgetDiscussionPage"; import { expect } from "@playwright/test"; -import { BudgetDiscussionEnum } from "@types"; +import { + BudgetDiscussionEnum, + BudgetProposalFilterTypes, + ProposedGovAction, +} from "@types"; const mockBudgetProposal = require("../../lib/_mock/budgetProposal.json"); const mockPoll = require("../../lib/_mock/budgetProposalPoll.json"); @@ -103,15 +108,41 @@ test.describe("Budget proposal list manipulation", () => { }); test("11B_3. Should sort budget proposals", async () => { - await budgetDiscussionPage.sortAndValidate( - "asc", - (p1, p2) => p1.attributes.createdAt <= p2.attributes.createdAt - ); - - await budgetDiscussionPage.sortAndValidate( - "desc", - (p1, p2) => p1.attributes.createdAt >= p2.attributes.createdAt - ); + const sortOptions = { + Oldest: (p1: ProposedGovAction, p2: ProposedGovAction) => + p1.attributes.createdAt <= p2.attributes.createdAt, + Newest: (p1: ProposedGovAction, p2: ProposedGovAction) => + p1.attributes.createdAt >= p2.attributes.createdAt, + "Most comments": (p1: ProposedGovAction, p2: ProposedGovAction) => + p1.attributes.prop_comments_number >= + p2.attributes.prop_comments_number, + "Least comments": (p1: ProposedGovAction, p2: ProposedGovAction) => + p1.attributes.prop_comments_number <= + p2.attributes.prop_comments_number, + "Name A-Z": (p1: ProposedGovAction, p2: ProposedGovAction) => + p1.attributes.bd_proposal_detail.data.attributes.proposal_name.localeCompare( + p2.attributes.bd_proposal_detail.data.attributes.proposal_name + ) <= 0, + "Name Z-A": (p1: ProposedGovAction, p2: ProposedGovAction) => + p1.attributes.bd_proposal_detail.data.attributes.proposal_name.localeCompare( + p2.attributes.bd_proposal_detail.data.attributes.proposal_name + ) >= 0, + "Proposer A-Z": (p1: ProposedGovAction, p2: ProposedGovAction) => + p1.attributes.creator.data.attributes.govtool_username.localeCompare( + p2.attributes.creator.data.attributes.govtool_username + ) <= 0, + "Proposer Z-A": (p1: ProposedGovAction, p2: ProposedGovAction) => + p1.attributes.creator.data.attributes.govtool_username.localeCompare( + p2.attributes.creator.data.attributes.govtool_username + ) >= 0, + }; + + for (const [option, validationFn] of Object.entries(sortOptions)) { + await budgetDiscussionPage.sortAndValidate( + option as BudgetProposalFilterTypes, + validationFn + ); + } }); }); }); diff --git a/tests/govtool-frontend/playwright/tests/12-proposal-budget-submission/proposalBudgetSubmission.loggedin.pb.spec.ts b/tests/govtool-frontend/playwright/tests/12-proposal-budget-submission/proposalBudgetSubmission.loggedin.pb.spec.ts index ce5863062..24f836706 100644 --- a/tests/govtool-frontend/playwright/tests/12-proposal-budget-submission/proposalBudgetSubmission.loggedin.pb.spec.ts +++ b/tests/govtool-frontend/playwright/tests/12-proposal-budget-submission/proposalBudgetSubmission.loggedin.pb.spec.ts @@ -15,6 +15,7 @@ import { import { faker } from "@faker-js/faker"; import { test } from "@fixtures/budgetProposal"; import { setAllureEpic } from "@helpers/allure"; +import { skipIfMainnet } from "@helpers/cardano"; import { createNewPageWithWallet } from "@helpers/page"; import BudgetDiscussionDetailsPage from "@pages/budgetDiscussionDetailsPage"; import BudgetDiscussionPage from "@pages/budgetDiscussionPage"; @@ -62,7 +63,7 @@ test.describe("Budget proposal 01 wallet", () => { test("12D_1. Should verify all field of “proposal ownership” section", async () => { // default field await expect( - budgetProposalSubmissionPage.companyTypeSelect + budgetProposalSubmissionPage.proposalCommittee ).toBeVisible(); await expect( @@ -70,10 +71,10 @@ test.describe("Budget proposal 01 wallet", () => { ).toBeVisible(); // company type field - await budgetProposalSubmissionPage.companyTypeSelect.click(); + await budgetProposalSubmissionPage.proposalCommittee.click(); await budgetProposalSubmissionPage.currentPage - .getByRole("option", { name: CompanyEnum.Company }) - .click(); //BUG missing testId + .getByTestId("company-submission") + .click(); await expect( budgetProposalSubmissionPage.companyNameInput @@ -86,10 +87,10 @@ test.describe("Budget proposal 01 wallet", () => { ).toBeVisible(); // group type field - await budgetProposalSubmissionPage.companyTypeSelect.click(); + await budgetProposalSubmissionPage.proposalCommittee.click(); await budgetProposalSubmissionPage.currentPage - .getByRole("option", { name: CompanyEnum.Group }) - .click(); //BUG missing testId + .getByTestId("group-submission") + .click(); await expect(budgetProposalSubmissionPage.groupNameInput).toBeVisible(); await expect(budgetProposalSubmissionPage.groupTypeInput).toBeVisible(); await expect( @@ -421,6 +422,7 @@ test.describe("Budget proposal 01 wallet", () => { }); test("12C. Should save and view draft proposal", async ({ browser }) => { + await skipIfMainnet(); const page = await createNewPageWithWallet(browser, { storageState: budgetProposal02AuthFile, wallet: budgetProposal02Wallet, @@ -436,7 +438,7 @@ test("12C. Should save and view draft proposal", async ({ browser }) => { await budgetSubmissionPage.viewLastDraft(); - await expect(budgetSubmissionPage.companyTypeSelect).toHaveText( + await expect(budgetSubmissionPage.proposalCommittee).toHaveText( draftProposalOwnership.companyType ); @@ -469,6 +471,8 @@ test("12C. Should save and view draft proposal", async ({ browser }) => { }); test("12H. Should submit a valid budget proposal", async ({ browser }) => { + await skipIfMainnet(); + const page = await createNewPageWithWallet(browser, { storageState: budgetProposal03AuthFile, wallet: budgetProposal03Wallet, @@ -489,6 +493,7 @@ test("12H. Should submit a valid budget proposal", async ({ browser }) => { test("12I. Should submit a valid draft budget proposal", async ({ browser, }) => { + await skipIfMainnet(); test.slow(); const page = await createNewPageWithWallet(browser, { storageState: budgetProposal04AuthFile, @@ -517,6 +522,7 @@ test("12I. Should submit a valid draft budget proposal", async ({ test("12J. Should verify created proposal appears in my proposals list", async ({ browser, }) => { + await skipIfMainnet(); test.slow(); const page = await createNewPageWithWallet(browser, { storageState: budgetProposal05AuthFile, diff --git a/tests/govtool-frontend/playwright/tests/2-delegation/delegationFunctionality.delegation.spec.ts b/tests/govtool-frontend/playwright/tests/2-delegation/delegationFunctionality.delegation.spec.ts index 573ffa93b..f2c04e9ba 100644 --- a/tests/govtool-frontend/playwright/tests/2-delegation/delegationFunctionality.delegation.spec.ts +++ b/tests/govtool-frontend/playwright/tests/2-delegation/delegationFunctionality.delegation.spec.ts @@ -63,9 +63,9 @@ test.describe("Delegate to others", () => { await dRepDirectoryPage.delegateToDRep(dRepId); // Verify dRepId in dRep directory - await expect( - page.getByTestId(`${dRepId}-delegate-button`) - ).not.toBeVisible(); + await expect(page.getByTestId(`${dRepId}-delegate-button`)).not.toBeVisible( + { timeout: 60_000 } + ); await expect(page.getByTestId(`${dRepId}-delegated-card`)).toBeVisible(); await expect( @@ -123,7 +123,7 @@ test.describe("Change delegation", () => { await dRepDirectoryPage.delegateToDRep(dRepIdSecond); await expect( page.getByTestId(`${dRepIdSecond}-delegated-card`) - ).toBeVisible(); + ).toBeVisible({ timeout: 60_000 }); await expect( page .getByTestId(`${dRepIdSecond}-delegated-card`) diff --git a/tests/govtool-frontend/playwright/tests/3-drep-registration/dRepRegistration.dRep.spec.ts b/tests/govtool-frontend/playwright/tests/3-drep-registration/dRepRegistration.dRep.spec.ts index 91fb769dd..1e1bdcbfa 100644 --- a/tests/govtool-frontend/playwright/tests/3-drep-registration/dRepRegistration.dRep.spec.ts +++ b/tests/govtool-frontend/playwright/tests/3-drep-registration/dRepRegistration.dRep.spec.ts @@ -222,7 +222,7 @@ test.describe("Temporary DReps", () => { await dRepPage.getByTestId("continue-retirement-button").click(); await expect( dRepPage.getByTestId("retirement-transaction-submitted-modal") - ).toBeVisible({ timeout: 15_000 }); + ).toBeVisible({ timeout: 60_000 }); dRepPage.getByTestId("confirm-modal-button").click(); await waitForTxConfirmation(dRepPage); diff --git a/tests/govtool-frontend/playwright/tests/3-drep-registration/editDRep.dRep.spec.ts b/tests/govtool-frontend/playwright/tests/3-drep-registration/editDRep.dRep.spec.ts index 782df6ada..383a43a89 100644 --- a/tests/govtool-frontend/playwright/tests/3-drep-registration/editDRep.dRep.spec.ts +++ b/tests/govtool-frontend/playwright/tests/3-drep-registration/editDRep.dRep.spec.ts @@ -2,7 +2,6 @@ import { dRep02Wallet } from "@constants/staticWallets"; import { faker } from "@faker-js/faker"; import { test } from "@fixtures/walletExtension"; import { setAllureEpic } from "@helpers/allure"; -import { ShelleyWallet } from "@helpers/crypto"; import { invalid as mockInvalid, valid as mockValid } from "@mock/index"; import { skipIfMainnet, @@ -10,7 +9,6 @@ import { } from "@helpers/cardano"; import EditDRepPage from "@pages/editDRepPage"; import { expect } from "@playwright/test"; -import environments from "@constants/environments"; import { dRep02AuthFile } from "@constants/auth"; import { generateInvalidDRepInfo, generateValidDRepInfo } from "@helpers/dRep"; diff --git a/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.dRep.spec.ts b/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.dRep.spec.ts index fd75c0b1d..7e6623005 100644 --- a/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.dRep.spec.ts +++ b/tests/govtool-frontend/playwright/tests/4-proposal-visibility/proposalVisibility.dRep.spec.ts @@ -147,7 +147,11 @@ test.describe("Check vote count", () => { : GovernanceActionType; const responsesPromise = Object.keys(voteWhiteListOption).map((filterKey) => page.waitForResponse((response) => - response.url().includes(`&type[]=${voteWhiteListOption[filterKey]}`) + response + .url() + .includes( + `proposal/list?page=0&pageSize=7&type[]=${voteWhiteListOption[filterKey]}` + ) ) ); diff --git a/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmission.ga.spec.ts b/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmission.ga.spec.ts index dcc0fbaa3..7cd57efc1 100644 --- a/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmission.ga.spec.ts +++ b/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmission.ga.spec.ts @@ -38,14 +38,6 @@ Object.values(ProposalType).forEach((proposalType, index) => { const wallet = await walletManager.popWallet("proposalSubmission"); - const stakeKeys = await createKeyFromPrivateKeyHex( - environments.faucet.stake.private || "" - ); - const { pkh: stakePkh, public: stakePublic } = stakeKeys.json(); - wallet.stake.pkh = stakePkh; - wallet.stake.private = getWalletConfigForFaucet().stake.private; - wallet.stake.public = stakePublic; - await logWalletDetails(wallet.address); const tempUserAuth = await createTempUserAuth(page, wallet); @@ -73,7 +65,10 @@ Object.values(ProposalType).forEach((proposalType, index) => { await proposalSubmissionPage.proposalCreateBtn.click(); await proposalDiscussionPage.continueBtn.click(); - const rewardAddress = rewardAddressBech32(environments.networkId, stakePkh); + const rewardAddress = rewardAddressBech32( + environments.networkId, + wallet.stake.pkh + ); await proposalSubmissionPage.createProposal(rewardAddress, proposalType); @@ -129,6 +124,7 @@ test.describe("Proposed as a governance action", async () => { }); test.afterEach(async () => { + await skipIfMainnet(); // cleanup await proposalDiscussionDetailPage.goto(proposalId); diff --git a/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmission.loggedin.pd.spec.ts b/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmission.loggedin.pd.spec.ts index d7126799a..63ef2d23e 100644 --- a/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmission.loggedin.pd.spec.ts +++ b/tests/govtool-frontend/playwright/tests/7-proposal-submission/proposalSubmission.loggedin.pd.spec.ts @@ -18,10 +18,11 @@ import { getDraftProposalWalletAndState } from "@helpers/auth"; import { skipIfNotInfoAndBootstrapping, isBootStrapingPhase, + skipIfMainnet, } from "@helpers/cardano"; import { ShelleyWallet } from "@helpers/crypto"; import { createNewPageWithWallet } from "@helpers/page"; -import { invalid, valid as mockValid } from "@mock/index"; +import { rewardAddressBech32 } from "@helpers/shellyWallet"; import ProposalDiscussionDetailsPage from "@pages/proposalDiscussionDetailsPage"; import ProposalSubmissionPage from "@pages/proposalSubmissionPage"; import { expect } from "@playwright/test"; @@ -132,6 +133,7 @@ test.describe("Proposal created logged state", () => { page, wallet, }) => { + await skipIfMainnet(); await skipIfNotInfoAndBootstrapping(type); const proposalSubmissionPage = new ProposalSubmissionPage(page); @@ -308,28 +310,42 @@ test.describe("Proposal created logged state", () => { test("7O. Should display insufficient balance modal when submitting proposal with insufficient funds", async ({ page, - proposalId, }) => { + await skipIfMainnet(); + const proposalCreationPage = new ProposalSubmissionPage(page); + await proposalCreationPage.goto(); + + const receiverAddress = rewardAddressBech32( + environments.networkId, + proposal01Wallet.stake.pkh + ); + + await proposalCreationPage.createProposal(receiverAddress); + const proposalDiscussionDetailsPage = new ProposalDiscussionDetailsPage( page ); - await proposalDiscussionDetailsPage.goto(proposalId); - await proposalDiscussionDetailsPage.verifyIdentityBtn.click(); - await proposalDiscussionDetailsPage.submitAsGABtn.click(); - - const proposalSubmissionPage = new ProposalSubmissionPage(page); - await expect( - proposalSubmissionPage.currentPage.getByText( - "Insufficient wallet balance", - { exact: true } - ) - ).toBeVisible(); // BUG missing test id + try { + await proposalDiscussionDetailsPage.submitAsGABtn.click(); + await expect( + proposalCreationPage.currentPage.getByTestId( + "insufficient-wallet-balance-title" + ) + ).toHaveText(/Insufficient wallet balance/); + + await proposalCreationPage.currentPage + .getByTestId("insufficient-wallet-balance-dialog-button") + .click(); + } finally { + await proposalDiscussionDetailsPage.deleteProposal(); + } }); }); test.describe("Proposal Draft", () => { test("7C. Should list unfinished Draft ", async ({ browser }) => { + await skipIfMainnet(); const page = await createNewPageWithWallet(browser, { storageState: proposal03AuthFile, wallet: proposal03Wallet, @@ -346,6 +362,7 @@ test.describe("Proposal Draft", () => { }); test("7L. Should save proposal as a draft", async ({ browser }) => { + await skipIfMainnet(); const page = await createNewPageWithWallet(browser, { storageState: proposal04AuthFile, wallet: proposal04Wallet, @@ -422,6 +439,7 @@ test.describe("Proposal Draft", () => { test(`7M_${index + 1}. Should edit a ${proposalType.toLowerCase()} proposal draft`, async ({ browser, }) => { + await skipIfMainnet(); test.slow(); const { storageState, wallet } = getDraftProposalWalletAndState(proposalType); @@ -496,6 +514,7 @@ test.describe("Proposal Draft", () => { }); test("7N. Should submit a draft proposal", async ({ browser }) => { + await skipIfMainnet(); const page = await createNewPageWithWallet(browser, { storageState: proposal06AuthFile, wallet: proposal06Wallet, diff --git a/tests/govtool-frontend/playwright/tests/8-proposal-discussion/proposalDiscussion.loggedin.pd.spec.ts b/tests/govtool-frontend/playwright/tests/8-proposal-discussion/proposalDiscussion.loggedin.pd.spec.ts index 98f4c4e36..40c725509 100644 --- a/tests/govtool-frontend/playwright/tests/8-proposal-discussion/proposalDiscussion.loggedin.pd.spec.ts +++ b/tests/govtool-frontend/playwright/tests/8-proposal-discussion/proposalDiscussion.loggedin.pd.spec.ts @@ -9,15 +9,16 @@ import { createNewPageWithWallet } from "@helpers/page"; import ProposalDiscussionDetailsPage from "@pages/proposalDiscussionDetailsPage"; import { Page, expect } from "@playwright/test"; import { setAllureEpic } from "@helpers/allure"; -import ProposalSubmissionPage from "@pages/proposalSubmissionPage"; import { proposal01AuthFile, proposal02AuthFile, user01AuthFile, } from "@constants/auth"; +import { skipIfMainnet } from "@helpers/cardano"; test.beforeEach(async () => { await setAllureEpic("8. Proposal Discussion Forum"); + await skipIfMainnet(); }); test.describe("Proposal created logged in state", () => { diff --git a/tests/govtool-frontend/playwright/tests/8-proposal-discussion/proposalDiscussion.spec.ts b/tests/govtool-frontend/playwright/tests/8-proposal-discussion/proposalDiscussion.spec.ts index e4303ae9b..a895ecfa5 100644 --- a/tests/govtool-frontend/playwright/tests/8-proposal-discussion/proposalDiscussion.spec.ts +++ b/tests/govtool-frontend/playwright/tests/8-proposal-discussion/proposalDiscussion.spec.ts @@ -13,7 +13,11 @@ import { functionWaitedAssert } from "@helpers/waitedLoop"; import ProposalDiscussionDetailsPage from "@pages/proposalDiscussionDetailsPage"; import ProposalDiscussionPage from "@pages/proposalDiscussionPage"; import { expect } from "@playwright/test"; -import { ProposalType } from "@types"; +import { + ProposalDiscussionFilterTypes, + ProposalType, + ProposedGovAction, +} from "@types"; const mockProposal = require("../../lib/_mock/proposal.json"); const mockPoll = require("../../lib/_mock/proposalPoll.json"); @@ -65,15 +69,41 @@ test.describe("Filter and sort proposals", () => { }); test("8B_2. Should sort the list of proposed governance actions.", async () => { - await proposalDiscussionPage.sortAndValidate( - "asc", - (p1, p2) => p1.attributes.createdAt <= p2.attributes.createdAt - ); - - await proposalDiscussionPage.sortAndValidate( - "desc", - (p1, p2) => p1.attributes.createdAt >= p2.attributes.createdAt - ); + const sortOptions = { + Oldest: (p1: ProposedGovAction, p2: ProposedGovAction) => + p1.attributes.createdAt <= p2.attributes.createdAt, + Newest: (p1: ProposedGovAction, p2: ProposedGovAction) => + p1.attributes.createdAt >= p2.attributes.createdAt, + "Most likes": (p1: ProposedGovAction, p2: ProposedGovAction) => + p1.attributes.prop_likes >= p2.attributes.prop_likes, + "Least likes": (p1: ProposedGovAction, p2: ProposedGovAction) => + p1.attributes.prop_likes <= p2.attributes.prop_likes, + "Most dislikes": (p1: ProposedGovAction, p2: ProposedGovAction) => + p1.attributes.prop_dislikes >= p2.attributes.prop_dislikes, + "Least dislikes": (p1: ProposedGovAction, p2: ProposedGovAction) => + p1.attributes.prop_dislikes <= p2.attributes.prop_dislikes, + "Most comments": (p1: ProposedGovAction, p2: ProposedGovAction) => + p1.attributes.prop_comments_number >= + p2.attributes.prop_comments_number, + "Least comments": (p1: ProposedGovAction, p2: ProposedGovAction) => + p1.attributes.prop_comments_number <= + p2.attributes.prop_comments_number, + "Name A-Z": (p1: ProposedGovAction, p2: ProposedGovAction) => + p1.attributes.content.attributes.prop_name.localeCompare( + p2.attributes.content.attributes.prop_name + ) <= 0, + "Name Z-A": (p1: ProposedGovAction, p2: ProposedGovAction) => + p1.attributes.content.attributes.prop_name.localeCompare( + p2.attributes.content.attributes.prop_name + ) >= 0, + }; + + for (const [sortOption, sortFunction] of Object.entries(sortOptions)) { + await proposalDiscussionPage.sortAndValidate( + sortOption as ProposalDiscussionFilterTypes, + sortFunction + ); + } }); }); @@ -119,7 +149,7 @@ test("8C. Should search the list of proposed governance actions.", async ({ const proposalTitle = await proposalCard .locator('[data-testid^="proposal-"][data-testid$="-title"]') .innerText(); - expect(proposalTitle).toContain(proposalName); + expect(proposalTitle.trim()).toContain(proposalName.trim()); } }, { diff --git a/tests/govtool-frontend/playwright/tests/dRep.auth.setup.ts b/tests/govtool-frontend/playwright/tests/dRep.auth.setup.ts index f1593d593..a5e22ad69 100644 --- a/tests/govtool-frontend/playwright/tests/dRep.auth.setup.ts +++ b/tests/govtool-frontend/playwright/tests/dRep.auth.setup.ts @@ -3,12 +3,10 @@ import { dRep01Wallet, dRep02Wallet } from "@constants/staticWallets"; import { test as setup } from "@fixtures/walletExtension"; import { setAllureEpic, setAllureStory } from "@helpers/allure"; import { createAuth } from "@helpers/auth"; -import { skipIfMainnet } from "@helpers/cardano"; setup.beforeEach(async () => { await setAllureEpic("Setup"); await setAllureStory("Authentication"); - await skipIfMainnet(); }); const drepAuthConfigs = [ diff --git a/tests/govtool-frontend/playwright/tests/dRep.setup.ts b/tests/govtool-frontend/playwright/tests/dRep.setup.ts index afb3c798c..584120038 100644 --- a/tests/govtool-frontend/playwright/tests/dRep.setup.ts +++ b/tests/govtool-frontend/playwright/tests/dRep.setup.ts @@ -45,11 +45,9 @@ setup("Register DRep of static wallets", async () => { const metadataPromises = dRepWallets.map(async (dRepWallet) => { const metadataResponse = await uploadMetadataAndGetJsonHash(); const givenName = metadataResponse.givenName; - const index = dRepWallets.indexOf(dRepWallet); - dRepWallets[index] = { - ...dRepWallet, - givenName, - }; + + await walletManager.updateWalletGivenName(dRepWallet.address, givenName); + return { ...metadataResponse, wallet: dRepWallet, diff --git a/tests/govtool-frontend/playwright/tests/proposal-budget.dRep.setup.ts b/tests/govtool-frontend/playwright/tests/proposal-budget.dRep.setup.ts index 0a91e509a..78191ad17 100644 --- a/tests/govtool-frontend/playwright/tests/proposal-budget.dRep.setup.ts +++ b/tests/govtool-frontend/playwright/tests/proposal-budget.dRep.setup.ts @@ -11,6 +11,7 @@ import { expect } from "@playwright/test"; import { test as setup } from "@fixtures/walletExtension"; import kuberService from "@services/kuberService"; +import walletManager from "lib/walletManager"; setup.beforeEach(async () => { await setAllureEpic("Setup"); @@ -28,11 +29,12 @@ setup("Register DRep of proposal budget static wallets", async () => { async (dRepWallet) => { const metadataResponse = await uploadMetadataAndGetJsonHash(); const givenName = metadataResponse.givenName; - const index = dRepWallets.indexOf(dRepWallet); - dRepWallets[index] = { - ...dRepWallet, - givenName, - }; + + await walletManager.updateWalletGivenName( + dRepWallet.address, + givenName + ); + return { ...metadataResponse, wallet: dRepWallet, diff --git a/tests/govtool-frontend/playwright/tests/proposal-submission.ga.auth.setup.ts b/tests/govtool-frontend/playwright/tests/proposal-submission.ga.auth.setup.ts index 171a2c20f..e88d7690e 100644 --- a/tests/govtool-frontend/playwright/tests/proposal-submission.ga.auth.setup.ts +++ b/tests/govtool-frontend/playwright/tests/proposal-submission.ga.auth.setup.ts @@ -3,10 +3,12 @@ import { test as setup } from "@fixtures/walletExtension"; import { createAuthWithUserName } from "@helpers/auth"; import walletManager from "lib/walletManager"; import { proposalSubmissionAuthFile } from "@constants/auth"; +import { skipIfMainnet } from "@helpers/cardano"; setup.beforeEach(async () => { await setAllureEpic("Setup"); await setAllureStory("Authentication"); + await skipIfMainnet(); }); setup( diff --git a/tests/govtool-frontend/playwright/tests/proposal.setup.ts b/tests/govtool-frontend/playwright/tests/proposal.setup.ts index 5087fd136..36ec41f0e 100644 --- a/tests/govtool-frontend/playwright/tests/proposal.setup.ts +++ b/tests/govtool-frontend/playwright/tests/proposal.setup.ts @@ -1,13 +1,14 @@ import environments from "@constants/environments"; import { setAllureEpic, setAllureStory } from "@helpers/allure"; import { skipIfBalanceIsInsufficient, skipIfMainnet } from "@helpers/cardano"; -import { generateWallets } from "@helpers/shellyWallet"; +import { addressBech32, generateWallets } from "@helpers/shellyWallet"; import { pollTransaction } from "@helpers/transaction"; import { test as setup } from "@fixtures/walletExtension"; import kuberService from "@services/kuberService"; import walletManager from "lib/walletManager"; import { functionWaitedAssert } from "@helpers/waitedLoop"; import { getWalletConfigForFaucet } from "@helpers/index"; +import { createKeyFromPrivateKeyHex } from "@helpers/crypto"; const PROPOSAL_WALLETS_COUNT = environments.isScheduled ? 1 : 5; @@ -33,20 +34,38 @@ setup.beforeEach(async () => { }); setup("Setup temporary proposal wallets", async () => { - setup.setTimeout(2 * environments.txTimeOut); + setup.setTimeout(environments.txTimeOut); const proposalWallets = await generateWallets(PROPOSAL_WALLETS_COUNT); - - // initialize wallets - const initializeRes = await kuberService.initializeWallets( - [...proposalWallets], - getWalletConfigForFaucet().address, - getWalletConfigForFaucet().payment.private + const stakeKeys = await createKeyFromPrivateKeyHex( + environments.faucet.stake.private || "" ); - await pollTransaction(initializeRes.txId, initializeRes.lockInfo); + const { pkh: stakePkh, public: stakePublic } = stakeKeys.json(); + + const enrichedProposalWallets = proposalWallets.map((wallet) => { + const stake = { + pkh: stakePkh, + private: environments.faucet.stake.private, + public: stakePublic, + }; + + const walletAddress = addressBech32( + environments.networkId, + wallet.payment.pkh, + stakePkh + ); + + return { + ...wallet, + address: walletAddress, + stake, + }; + }); + + proposalWallets.splice(0, proposalWallets.length, ...enrichedProposalWallets); const amountOutputs = proposalWallets.map((wallet) => { - return { address: wallet.address, value: govActionDeposit }; + return { address: wallet.address, value: govActionDeposit + 22000000 }; }); const transferRes = await kuberService.multipleTransferADA( amountOutputs,