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 f5e5756f9..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 "$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 new file mode 100644 index 000000000..208f0d9a9 --- /dev/null +++ b/.github/scripts/remove_oldest_report.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +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) + + if [ "$dir_count" -gt 10 ]; then + # Find the oldest numerical directory + oldest_dir=$(find . -maxdepth 1 -type d -regex './[0-9]+' | sort -V | head -1) + if [ -n "$oldest_dir" ]; then + echo "More than 10 report directories exist. Removing oldest: $oldest_dir" + rm -rf "$oldest_dir" + fi + else + echo "Only $dir_count report directories exist (threshold: 10). Nothing to remove." + fi + + cd ../../ +else + echo "Report directory does not exist yet" +fi 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 15433a89f..a871cef71 100644 --- a/.github/workflows/test_backend.yml +++ b/.github/workflows/test_backend.yml @@ -16,6 +16,7 @@ on: - "sanchogov.tools/api" - "staging.govtool.byron.network/api" - "govtool.cardanoapi.io/api" + - "be.preview.gov.tools" - "z6b8d2f7a-zca4a4c45-gtw.z937eb260.rustrocks.fr" - "z78acf3c2-z5575152b-gtw.z937eb260.rustrocks.fr" - "be.gov.tools" @@ -28,13 +29,17 @@ on: - "preview" - "mainnet" - "preprod" + workflow_run: workflows: ["Build and deploy GovTool test stack"] types: [completed] - branches: + branches: - test - infra/test-chores + schedule: + - cron: "0 0 * * *" # 12AM UTC + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: false @@ -42,7 +47,7 @@ concurrency: jobs: backend-tests: runs-on: ubuntu-latest - if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} + if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' || github.event.schedule }} outputs: start_time: ${{ steps.set-pending-status.outputs.timestamp }} status: ${{ steps.run-tests.outcome }} @@ -54,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 }}" \ @@ -99,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 @@ -116,19 +123,17 @@ jobs: repository: ${{vars.GH_PAGES}} ssh-key: ${{ secrets.DEPLOY_KEY }} - - name: Register report - id: register-project + - 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/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 + chmod +x .github/scripts/remove_oldest_report.sh + .github/scripts/remove_oldest_report.sh - name: Generate report details id: report-details @@ -141,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 @@ -160,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 @@ -173,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 @@ -183,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://${{inputs.deployment || 'govtool.cardanoapi.io/api' }} - REPORT_NAME: govtool-backend + 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 537aeecc5..39dcef54b 100644 --- a/.github/workflows/test_integration_playwright.yml +++ b/.github/workflows/test_integration_playwright.yml @@ -33,10 +33,13 @@ on: workflow_run: workflows: ["Check and Build QA"] types: [completed] - branches: + branches: - test - infra/test-chores + schedule: + - cron: "0 0 * * *" # 12AM UTC + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: false @@ -44,7 +47,7 @@ concurrency: jobs: integration-tests: runs-on: ubuntu-latest - if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }} + if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' || github.event.schedule }} outputs: start_time: ${{ steps.set-pending-status.outputs.timestamp }} status: ${{ steps.run-test.outcome }} @@ -56,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 @@ -106,6 +110,11 @@ jobs: export BLOCKFROST_API_KEY="${{ secrets.BLOCKFROST_API_KEY_MAINNET }}" fi + # Set schedule workflow variable + if [[ "$GITHUB_EVENT_NAME" == "schedule" ]]; then + export SCHEDULED_WORKFLOW="true" + fi + npm run test:headless - name: Upload report @@ -131,7 +140,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 @@ -139,6 +148,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 @@ -156,19 +166,17 @@ jobs: repository: ${{vars.GH_PAGES}} ssh-key: ${{ secrets.DEPLOY_KEY }} - - name: Register report - id: register-project + - 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/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 + chmod +x .github/scripts/remove_oldest_report.sh + .github/scripts/remove_oldest_report.sh - name: Generate report details id: report-details @@ -182,7 +190,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 @@ -201,7 +209,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 @@ -214,8 +222,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 @@ -224,8 +233,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://${{inputs.deployment || 'p80-z6b8d2f7a-ze34e4cb2-gtw.z937eb260.rustrocks.fr' }} - REPORT_NAME: govtool-frontend + HOST_URL: https://${{ github.event.schedule && 'preview.gov.tools' || (inputs.deployment || 'p80-z6b8d2f7a-ze34e4cb2-gtw.z937eb260.rustrocks.fr') }} + DEPLOYMENT: ${{ github.event.schedule && 'preview.gov.tools' || inputs.deployment || 'p80-z6b8d2f7a-ze34e4cb2-gtw.z937eb260.rustrocks.fr'}} + 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 3b16353ba..d2c4a786e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,74 @@ changes. ### Added +- Preserve maintenance ending banner state on the wallet connection change [Issue 3681](https://github.com/IntersectMBO/govtool/issues/3681) +- Add authors for Live Voting Governance Actions [Issue 3745](https://github.com/IntersectMBO/govtool/issues/3745) +- Add support for ed25519 author signature validation on gov actions [Issue 3745](https://github.com/IntersectMBO/govtool/issues/3745) + ### Fixed +- Fix blank page on dRep details when link or identity references contain objects: { @value: ... } not strings [Issue 3733](https://github.com/IntersectMBO/govtool/issues/3733) +- 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 +- Adjust top menu (navbar) layout when wallet is not connected [Issue-3682](https://github.com/IntersectMBO/govtool/issues/3682) +- Unification of sections 'Receiving Address' and 'Amount' in Treasury Withdrawal Governance Action [Issue-3828](https://github.com/IntersectMBO/govtool/issues/3828) ### 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 + +## [v2.0.22](https://github.com/IntersectMBO/govtool/releases/tag/v2.0.22) 2025-05-15 + +### Added + +### Fixed + +- Fix an issue where the submit button remained disabled after removing an invalid value from the IMAGE input field on DRrep form [Issue 3560](https://github.com/IntersectMBO/govtool/issues/3560) +- Fix app crash on unhandled wallet error [Issue 3123](https://github.com/IntersectMBO/govtool/issues/3123) +- Preserve new lines in markdown text [Issue 2712](https://github.com/IntersectMBO/govtool/issues/2712) +- Add scroll to markdown tables [Issue 3615](https://github.com/IntersectMBO/govtool/issues/3615) + +### Changed + +### Removed + +## [v2.0.21](https://github.com/IntersectMBO/govtool/releases/tag/v2.0.21) 2025-05-09 + +### Added + +- Add support for the tables in markdown [Issue 3581](https://github.com/IntersectMBO/govtool/issues/3581) + +### Fixed + +- Fix invalid metadata status background on voted on card + +### Changed + +- Update first CTA on GovTool home page [Issue 3467](https://github.com/IntersectMBO/govtool/issues/3467) +- Change link to docs in learn more about governance [Issue 3494](https://github.com/IntersectMBO/govtool/issues/3494) + +### Removed + +- Remove additional canonicalization of the metadata [Issue 3591](https://github.com/IntersectMBO/govtool/issues/3591) + ## [v2.0.20](https://github.com/IntersectMBO/govtool/releases/tag/v2.0.20) 2025-04-16 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ccc9e9126..01009d4aa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -107,7 +107,63 @@ Once it's filed: #### Your First Code Contribution -TODO +Welcome to contributing to `GovTool`! Whether you're fixing a bug, adding a feature, or improving documentation, we’re excited to have you on board. This section guides you through your first code contribution to make the process smooth and rewarding. + +#### Getting Started + +1. **Set Up Your Environment**: + - Follow the instructions in [`README.md`](./README.md) file and navigate to the specific folder corresponding to the form you want to fix or enhance. + - Follow the setup instructions to clone the repository and install the necessary dependencies. + - Make sure you're using the latest version of the project to avoid potential conflicts. + +2. **Find an Issue to Work On**: + - Browse open issues on the [GovTool GitHub Issues page](https://github.com/IntersectMBO/govtool/issues). + - Look for issues labeled `🐛 Bug` or `💡 Feature idea`. + - For existing feature idea tasks, comment on the issue to express interest or share thoughts. + +3. **Claim an Issue**: + - Comment on the issue to let maintainers know you’re working on it. This helps avoid duplicate efforts. + - Move the issue from `todo` to `in progress` on the project board (if you have permissions). + + +4. **Fork the Repository**: + - This step applies only if you lack permission to create branches or perform actions in the `govtool` repository. + - Fork the `govtool` repository on GitHub to create a copy under your account. + + +4. **Create a Branch**: + - Create a new branch from the `develop` branch with a descriptive name (see [Branch Naming](#branch-naming)). + - Example: `feat/123-add-voting-ui` or `fix/456-update-api-endpoint`. + +5. **Make Your Changes**: + - Write clean, well-documented code following the [Style Guides](#style-guides) for React, Haskell, CSS, or other relevant technologies. + - Add or update tests to ensure your changes are robust. + - Keep your changes focused and aligned with the issue’s scope. + +6. **Commit Your Changes**: + - Write clear, concise commit messages following the [Commit Messages](#commit-messages) guidelines. + +7. **Submit a Pull Request**: + + `If You Have Push Permissions to the Original Repository` + - Push your branch to the repository and create a pull request (PR) to the `develop` branch. + + `If You Don’t Have Push Permissions (Using a Forked Repository)` + - Push your branch to the forked repository and create a pull request (PR) in the `govtool` main repository, using the `govtool` `develop` branch as the base and your fork’s branch name as the head repository. + + - Use the PR template provided in the repository and link the related issue (e.g., `issues #123`). + - Describe your changes clearly, including why they’re needed and how they were tested. + - If your PR isn’t ready for review, mark it as a draft. + +8. **Address Feedback**: + - Expect reviews from maintainers or other contributors (see [CODEOWNERS](./CODEOWNERS)). + - Respond to feedback promptly and make requested changes. + - Once approved, your PR will be merged into `develop` by the author after passing tests. + +9. **Celebrate Your Contribution!**: + - Once merged, your changes will move through the [Development Processes](#development-processes) (QA, staging, etc.). + - You’re now a `GovTool` contributor! Share your achievement and consider tackling another issue. + ## Working Conventions diff --git a/docs/GOVERNANCE_ACTION_SUBMISSION.md b/docs/GOVERNANCE_ACTION_SUBMISSION.md index 1c6a29d99..21327e029 100644 --- a/docs/GOVERNANCE_ACTION_SUBMISSION.md +++ b/docs/GOVERNANCE_ACTION_SUBMISSION.md @@ -134,7 +134,7 @@ const buildProtocolParameterChangeGovernanceAction: ( protocolParameterChangeProps: ProtocolParameterChangeProps ) => Promise; -const buildHardForkInitiationGovernanceAction: ( +const buildHardForkGovernanceAction: ( hardForkInitiationProps: HardForkInitiationProps ) => Promise; @@ -210,7 +210,7 @@ const { buildSignSubmitConwayCertTx, buildNewInfoGovernanceAction, buildProtocolParameterChangeGovernanceAction, - buildHardForkInitiationGovernanceAction, + buildHardForkGovernanceAction, buildTreasuryGovernanceAction, buildNewConstitutionGovernanceAction, buildUpdateCommitteeGovernanceAction, @@ -241,7 +241,7 @@ govActionBuilder = await buildProtocolParameterChangeGovernanceAction({ }); // hash of the previous Governance Action, index of the previous Governance Action, url of the metadata, hash of the metadata, and the major and minor numbers of the hard fork initiation -govActionBuilder = await buildHardForkInitiationGovernanceAction({ +govActionBuilder = await buildHardForkGovernanceAction({ prevGovernanceActionHash, prevGovernanceActionIndex, url, diff --git a/gov-action-loader/README.md b/gov-action-loader/README.md new file mode 100644 index 000000000..0f851daf2 --- /dev/null +++ b/gov-action-loader/README.md @@ -0,0 +1,14 @@ +# Governance Action Loader + +This directory contains the platform for submitting governance action data transactions on-chain, supporting both individual and bulk submission methods. + +## 📍 Navigation + +- [Frontend](./frontend/) +- [Backend](./backend/) + +## Frontend +The Governance Action Loader frontend is a web application that communicates with the backend via a REST interface to facilitate the submission of on-chain governance data transactions. + +## Backend +The Governance Action Loader backend uses a predefined wallet to execute transactions for on-chain governance data. \ No newline at end of file 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 5dbddfbff..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.20/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 e920201fc..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.20/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..1ae00d6fa 100644 --- a/govtool/backend/sql/list-dreps.sql +++ b/govtool/backend/sql/list-dreps.sql @@ -95,7 +95,7 @@ HasNonDeregisterVotingAnchor AS ( EXISTS ( SELECT 1 FROM drep_registration dr_sub - WHERE + WHERE dr_sub.drep_hash_id = dr.drep_hash_id AND dr_sub.voting_anchor_id IS NULL AND COALESCE(dr_sub.deposit, 0) >= 0 @@ -126,7 +126,59 @@ 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( + jsonb_build_object( + 'uri', COALESCE( + CASE WHEN jsonb_typeof(ref->'uri') = 'string' THEN ref->>'uri' END, + ref->'uri'->>'@value' + ), + '@type', ref->>'@type', + 'label', COALESCE( + CASE WHEN jsonb_typeof(ref->'label') = 'string' THEN ref->>'label' END, + ref->'label'->>'@value' + ) + ) + ) + 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( + jsonb_build_object( + 'uri', COALESCE( + CASE WHEN jsonb_typeof(ref->'uri') = 'string' THEN ref->>'uri' END, + ref->'uri'->>'@value' + ), + '@type', ref->>'@type', + 'label', COALESCE( + CASE WHEN jsonb_typeof(ref->'label') = 'string' THEN ref->>'label' END, + ref->'label'->>'@value' + ) + ) + ) + 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 @@ -157,7 +209,7 @@ DRepData AS ( LEFT JOIN FetchError fetch_error ON fetch_error.voting_anchor_id = leva.voting_anchor_id LEFT JOIN HasNonDeregisterVotingAnchor hndva ON hndva.drep_hash_id = dh.id LEFT JOIN off_chain_vote_data ocvd ON ocvd.voting_anchor_id = leva.voting_anchor_id - LEFT JOIN off_chain_vote_drep_data ON off_chain_vote_drep_data.off_chain_vote_data_id = ocvd.id + LEFT JOIN off_chain_vote_drep_data ON off_chain_vote_drep_data.off_chain_vote_data_id = ocvd.id LEFT JOIN voting_procedure ON voting_procedure.drep_voter = dh.id LEFT JOIN tx voting_procedure_transaction ON voting_procedure_transaction.id = voting_procedure.tx_id LEFT JOIN block voting_procedure_block ON voting_procedure_block.id = voting_procedure_transaction.block_id @@ -212,7 +264,53 @@ 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( + jsonb_build_object( + 'uri', COALESCE( + CASE WHEN jsonb_typeof(ref->'uri') = 'string' THEN ref->>'uri' END, + ref->'uri'->>'@value' + ), + '@type', ref->>'@type', + 'label', COALESCE( + CASE WHEN jsonb_typeof(ref->'label') = 'string' THEN ref->>'label' END, + ref->'label'->>'@value' + ) + ) + ) + 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( + jsonb_build_object( + 'uri', COALESCE( + CASE WHEN jsonb_typeof(ref->'uri') = 'string' THEN ref->>'uri' END, + ref->'uri'->>'@value' + ), + '@type', ref->>'@type', + 'label', COALESCE( + CASE WHEN jsonb_typeof(ref->'label') = 'string' THEN ref->>'label' END, + ref->'label'->>'@value' + ) + ) + ) + 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 @@ -225,4 +323,4 @@ WHERE objectives ILIKE ? OR motivations ILIKE ? OR qualifications ILIKE ? - ) \ No newline at end of file + ) diff --git a/govtool/backend/sql/list-proposals.sql b/govtool/backend/sql/list-proposals.sql index 8ccbfd3d1..dfb54e12d 100644 --- a/govtool/backend/sql/list-proposals.sql +++ b/govtool/backend/sql/list-proposals.sql @@ -303,7 +303,22 @@ SELECT COALESCE(cv.ccNoVotes, 0) cc_no_votes, COALESCE(cv.ccAbstainVotes, 0) cc_abstain_votes, prev_gov_action.index as prev_gov_action_index, - encode(prev_gov_action_tx.hash, 'hex') as prev_gov_action_tx_hash + encode(prev_gov_action_tx.hash, 'hex') as prev_gov_action_tx_hash, + off_chain_vote_data.json as json_content, + COALESCE( + ( + SELECT jsonb_agg( + jsonb_build_object( + 'name', author_elem->>'name', + 'publicKey', author_elem->'witness'->>'publicKey', + 'signature', author_elem->'witness'->>'signature', + 'witnessAlgorithm', author_elem->'witness'->>'witnessAlgorithm' + ) + ) + FROM jsonb_array_elements(off_chain_vote_data.json->'authors') AS author_elem + ), + '[]'::jsonb + ) AS authors FROM gov_action_proposal JOIN ActiveProposals ON gov_action_proposal.id = ActiveProposals.id @@ -355,4 +370,5 @@ GROUP BY off_chain_vote_gov_action_data.title, off_chain_vote_gov_action_data.abstract, off_chain_vote_gov_action_data.motivation, - off_chain_vote_gov_action_data.rationale; \ No newline at end of file + off_chain_vote_gov_action_data.rationale, + off_chain_vote_data.json; diff --git a/govtool/backend/src/VVA/API.hs b/govtool/backend/src/VVA/API.hs index b95960f52..3c14926e0 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 @@ -237,7 +244,9 @@ proposalToResponse timeZone Types.Proposal {..} = proposalResponseCcNoVotes = proposalCcNoVotes, proposalResponseCcAbstainVotes = proposalCcAbstainVotes, proposalResponsePrevGovActionIndex = proposalPrevGovActionIndex, - proposalResponsePrevGovActionTxHash = HexText <$> proposalPrevGovActionTxHash + proposalResponsePrevGovActionTxHash = HexText <$> proposalPrevGovActionTxHash, + proposalResponseJson = proposalJson, + proposalResponseAuthors = ProposalAuthors <$> proposalAuthors } voteToResponse :: Types.Vote -> VoteParams @@ -442,6 +451,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 +534,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..d14fd54cb 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 @@ -401,9 +401,53 @@ data ProposalResponse , proposalResponseCcAbstainVotes :: Integer , proposalResponsePrevGovActionIndex :: Maybe Integer , proposalResponsePrevGovActionTxHash :: Maybe HexText + , proposalResponseJson :: Maybe Value + , proposalResponseAuthors :: Maybe ProposalAuthors } deriving (Generic, Show) +newtype ProposalAuthors = ProposalAuthors { getProposalAuthors :: Value } + deriving newtype (Show) + +instance FromJSON ProposalAuthors where + parseJSON v@(Array _) = pure $ ProposalAuthors v + parseJSON _ = fail "ProposalAuthors must be a JSON array" + +instance ToJSON ProposalAuthors where + toJSON (ProposalAuthors v) = v + +instance ToSchema ProposalAuthors where + declareNamedSchema _ = pure $ NamedSchema (Just "ProposalAuthors") $ mempty + & type_ ?~ OpenApiArray + & description ?~ "A JSON array of proposal authors" + & example ?~ toJSON + [ object + [ "name" .= ("Alice" :: Text) + , "witnessAlgorithm" .= ("algo" :: Text) + , "publicKey" .= ("key" :: Text) + , "signature" .= ("sig" :: Text) + ] + , object + [ "name" .= ("Bob" :: Text) + , "witnessAlgorithm" .= ("algo2" :: Text) + , "publicKey" .= ("key2" :: Text) + , "signature" .= ("sig2" :: Text) + ] + ] + +exampleProposalAuthors :: Text +exampleProposalAuthors = + "[\ + \ {\"name\": \"Alice\",\ + \ \"witnessAlgorithm\": \"Ed25519\",\ + \ \"publicKey\": \"abcdef123456\",\ + \ \"signature\": \"deadbeef\"},\ + \ {\"name\": \"Bob\",\ + \ \"witnessAlgorithm\": \"Ed25519\",\ + \ \"publicKey\": \"123456abcdef\",\ + \ \"signature\": \"beefdead\"}\ + \]" + deriveJSON (jsonOptions "proposalResponse") ''ProposalResponse exampleProposalResponse :: Text @@ -433,7 +477,9 @@ exampleProposalResponse = "{ \"id\": \"proposalId123\"," <> "\"cCNoVotes\": 0," <> "\"cCAbstainVotes\": 0," <> "\"prevGovActionIndex\": 0," - <> "\"prevGovActionTxHash\": \"47c14a128cd024f1b990c839d67720825921ad87ed875def42641ddd2169b39c\"}" + <> "\"prevGovActionTxHash\": \"47c14a128cd024f1b990c839d67720825921ad87ed875def42641ddd2169b39c\"," + <> "\"authors\": " <> exampleProposalAuthors + <> "}" instance ToSchema Value where declareNamedSchema _ = pure $ NamedSchema (Just "Value") $ mempty @@ -455,6 +501,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 +639,7 @@ instance ToSchema VoteResponse where & example ?~ toJSON exampleVoteResponse + data DRepInfoResponse = DRepInfoResponse { dRepInfoResponseIsScriptBased :: Bool @@ -578,7 +659,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 +867,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 +906,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 +1102,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..25c248bd3 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 @@ -162,6 +208,8 @@ data Proposal , proposalCcAbstainVotes :: Integer , proposalPrevGovActionIndex :: Maybe Integer , proposalPrevGovActionTxHash :: Maybe Text + , proposalJson :: Maybe Value + , proposalAuthors :: Maybe Value } deriving (Show) @@ -195,6 +243,8 @@ instance FromRow Proposal where <*> (floor @Scientific <$> field) -- proposalCcAbstainVotes <*> field -- prevGovActionIndex <*> field -- prevGovActionTxHash + <*> field -- proposalJson + <*> field -- proposalAuthors data TransactionStatus = TransactionStatus { transactionConfirmed :: Bool @@ -211,6 +261,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 +310,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 +353,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 745f3bc87..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.20 +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 435748478..24ffb835c 100644 --- a/govtool/frontend/package-lock.json +++ b/govtool/frontend/package-lock.json @@ -1,23 +1,24 @@ { "name": "@govtool/frontend", - "version": "2.0.20", + "version": "2.0.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@govtool/frontend", - "version": "2.0.20", + "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": "1.4.1", + "@intersect.mbo/govtool-outcomes-pillar-ui": "v1.5.1", "@intersect.mbo/intersectmbo.org-icons-set": "^1.0.8", - "@intersect.mbo/pdf-ui": "0.7.0-beta-25", + "@intersect.mbo/pdf-ui": "1.0.3-beta", "@mui/icons-material": "^5.14.3", "@mui/material": "^5.14.4", + "@noble/ed25519": "^2.3.0", "@rollup/plugin-babel": "^6.0.4", "@rollup/pluginutils": "^5.1.0", "@sentry/react": "^7.77.0", @@ -26,6 +27,7 @@ "bech32": "^2.0.0", "blakejs": "^1.2.1", "buffer": "^6.0.3", + "cbor-web": "^10.0.3", "date-fns": "^2.30.0", "esbuild": "^0.25.0", "i18next": "^23.7.19", @@ -43,7 +45,7 @@ "react-query": "^3.39.3", "react-router-dom": "^6.13.0", "rehype-katex": "^7.0.1", - "remark-breaks": "^4.0.0", + "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "storybook-addon-manual-mocks": "^1.0.3", "storybook-addon-module-mock": "^1.3.4", @@ -3390,9 +3392,9 @@ } }, "node_modules/@intersect.mbo/govtool-outcomes-pillar-ui": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@intersect.mbo/govtool-outcomes-pillar-ui/-/govtool-outcomes-pillar-ui-1.4.1.tgz", - "integrity": "sha512-cev6uUOiWH2KY4ozv17oxGkfonQ2oLDTlpcqU7HFntekUPwjEx0rOqaZPakGShxR6rb+GrBJ4Y7Vnf1bBk2Otg==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@intersect.mbo/govtool-outcomes-pillar-ui/-/govtool-outcomes-pillar-ui-1.5.1.tgz", + "integrity": "sha512-Vy82YWllHcksh8ZATsQ6I3Bg29+Lstn8G5Nzlja2MFMJoQniDqKeiNyLXY3DeA8NhNFH1prSsc/gAaQLeqmONA==", "license": "ISC", "dependencies": { "@fontsource/poppins": "^5.0.14", @@ -3424,9 +3426,9 @@ "license": "ISC" }, "node_modules/@intersect.mbo/pdf-ui": { - "version": "0.7.0-beta-25", - "resolved": "https://registry.npmjs.org/@intersect.mbo/pdf-ui/-/pdf-ui-0.7.0-beta-25.tgz", - "integrity": "sha512-TDeWjJVMvLOR6sgTT6bCoHspZbybiRH0C5OzDDaU1yLSFD7xKx1aW5eAVdG0uzxrO+C0X7ceBGFoN5ucHICdlg==", + "version": "1.0.3-beta", + "resolved": "https://registry.npmjs.org/@intersect.mbo/pdf-ui/-/pdf-ui-1.0.3-beta.tgz", + "integrity": "sha512-zsG3wD3C2k7x3rWPTmrUan22rIuly2ltLMx3lOYN7MIFFnQCeJ1+qXYwnZhvLsgVtzZbAi+gGnDqYggZLgVkoQ==", "dependencies": { "@emurgo/cardano-serialization-lib-asmjs": "^12.0.0-beta.2", "@fontsource/poppins": "^5.0.14", @@ -4445,6 +4447,15 @@ "node": ">=4.0" } }, + "node_modules/@noble/ed25519": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-2.3.0.tgz", + "integrity": "sha512-M7dvXL2B92/M7dw9+gzuydL8qn/jiqNHaoR3Q+cb1q1GHV7uwE17WCyFMG+Y+TZb5izcaXk5TdJRrDUxHXL78A==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -10151,6 +10162,15 @@ "node": ">=4" } }, + "node_modules/cbor-web": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/cbor-web/-/cbor-web-10.0.3.tgz", + "integrity": "sha512-IWHUPUDxRlDzT6Saykzpy/0PckZjcPZcS/WQR1pWm+YX5IniAMB4Cih+AdxUss0CNs/XEBXWJsKfJWitx+1zGg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -20004,6 +20024,16 @@ "dev": true, "license": "MIT" }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/match-sorter": { "version": "6.3.4", "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz", @@ -20064,6 +20094,107 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-math": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz", @@ -20402,6 +20533,127 @@ "micromark-util-types": "^2.0.0" } }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/micromark-extension-math": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", @@ -26385,6 +26637,24 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-math": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz", @@ -26434,6 +26704,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remove-accents": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", diff --git a/govtool/frontend/package.json b/govtool/frontend/package.json index c02cfeab5..ea8747291 100644 --- a/govtool/frontend/package.json +++ b/govtool/frontend/package.json @@ -1,7 +1,7 @@ { "name": "@govtool/frontend", "private": true, - "version": "2.0.20", + "version": "2.0.23", "type": "module", "scripts": { "build": "vite build", @@ -27,11 +27,12 @@ "@emotion/styled": "^11.11.0", "@emurgo/cardano-serialization-lib-asmjs": "^14.1.1", "@hookform/resolvers": "^3.3.1", - "@intersect.mbo/govtool-outcomes-pillar-ui": "1.4.1", + "@intersect.mbo/govtool-outcomes-pillar-ui": "v1.5.1", "@intersect.mbo/intersectmbo.org-icons-set": "^1.0.8", - "@intersect.mbo/pdf-ui": "0.7.0-beta-25", + "@intersect.mbo/pdf-ui": "1.0.3-beta", "@mui/icons-material": "^5.14.3", "@mui/material": "^5.14.4", + "@noble/ed25519": "^2.3.0", "@rollup/plugin-babel": "^6.0.4", "@rollup/pluginutils": "^5.1.0", "@sentry/react": "^7.77.0", @@ -40,6 +41,7 @@ "bech32": "^2.0.0", "blakejs": "^1.2.1", "buffer": "^6.0.3", + "cbor-web": "^10.0.3", "date-fns": "^2.30.0", "esbuild": "^0.25.0", "i18next": "^23.7.19", @@ -57,7 +59,7 @@ "react-query": "^3.39.3", "react-router-dom": "^6.13.0", "rehype-katex": "^7.0.1", - "remark-breaks": "^4.0.0", + "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "storybook-addon-manual-mocks": "^1.0.3", "storybook-addon-module-mock": "^1.3.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/atoms/Link.tsx b/govtool/frontend/src/components/atoms/Link.tsx index 78fc8bccd..c56e61f60 100644 --- a/govtool/frontend/src/components/atoms/Link.tsx +++ b/govtool/frontend/src/components/atoms/Link.tsx @@ -1,56 +1,98 @@ -import { FC } from "react"; +import { forwardRef, MouseEvent } from "react"; import { NavLink } from "react-router-dom"; import { Typography } from "@mui/material"; import { useCardano } from "@context"; +const FONT_SIZE = { + small: 14, + big: 22, +}; + type LinkProps = { dataTestId?: string; isConnectWallet?: boolean; - label: string; + label: React.ReactNode; navTo: string; - onClick?: () => void; + onClick?: (event: MouseEvent) => void; size?: "small" | "big"; }; -export const Link: FC = ({ ...props }) => { - const { - dataTestId, - isConnectWallet, - label, - navTo, - size = "small", - onClick, - } = props; - const { disconnectWallet } = useCardano(); - - const fontSize = { - small: 14, - big: 22, - }[size]; - - return ( - { - if (!isConnectWallet) disconnectWallet(); - if (onClick) onClick(); - }} - > - {({ isActive }) => ( - - {label} - - )} - - ); -}; +export const Link = forwardRef( + ({ ...props }, ref) => { + const { + dataTestId, + isConnectWallet, + label, + navTo, + size = "small", + onClick, + } = props; + const { disconnectWallet } = useCardano(); + + const fontSize = FONT_SIZE[size]; + + return ( + { + if (!isConnectWallet) disconnectWallet(); + if (onClick) onClick(e); + }} + ref={ref} + > + {({ isActive }) => ( + + {label} + + )} + + ); + }, +); + +// This component is used as a placeholder for links that do not navigate anywhere, +// but with the same styling as the Link component. +export const FakeLink = forwardRef>( + (props, ref) => { + const { + dataTestId, + isConnectWallet, + label, + size = "small", + onClick, + } = props; + const { disconnectWallet } = useCardano(); + + const fontSize = FONT_SIZE[size]; + + return ( + { + e.preventDefault(); + if (!isConnectWallet) disconnectWallet(); + if (onClick) onClick(e); + }} + ref={ref} + sx={{ + cursor: "pointer", + fontSize, + fontWeight: 500, + color: "textBlack", + }} + > + {label} + + ); + }, +); diff --git a/govtool/frontend/src/components/molecules/GovernanceActionCardElement.tsx b/govtool/frontend/src/components/molecules/GovernanceActionCardElement.tsx index bbd5a2382..d2c4bf83e 100644 --- a/govtool/frontend/src/components/molecules/GovernanceActionCardElement.tsx +++ b/govtool/frontend/src/components/molecules/GovernanceActionCardElement.tsx @@ -3,9 +3,10 @@ import { Box, Skeleton } from "@mui/material"; import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; import Markdown from "react-markdown"; import remarkMath from "remark-math"; +import remarkGfm from "remark-gfm"; import rehypeKatex from "rehype-katex"; -import remarkBreaks from "remark-breaks"; import "katex/dist/katex.min.css"; +import "./tableMarkdown.css"; import { Typography, Tooltip, CopyButton, TooltipProps } from "@atoms"; import { removeMarkdown } from "@/utils"; @@ -21,6 +22,7 @@ type BaseProps = { marginBottom?: number; isSemiTransparent?: boolean; isValidating?: boolean; + children?: React.ReactNode; }; type VariantProps = BaseProps & { @@ -43,10 +45,11 @@ export const GovernanceActionCardElement = ({ isMarkdown = false, isSemiTransparent = false, isValidating = false, + children, }: VariantProps) => { const { openModal } = useModal(); - if (!text) return null; + if (text == null && children == null) return null; const renderTooltip = () => tooltipProps && ( @@ -111,45 +114,62 @@ export const GovernanceActionCardElement = ({ ); - const renderMarkdownText = ({ children }: PropsWithChildren) => ( + const renderMarkdownText = ({ + children: markdownChildren, + }: PropsWithChildren) => ( - {children} + {markdownChildren} ); const markdownComponents = { - p: (props: PropsWithChildren) => { - const { children } = props; - return renderMarkdownText({ children }); - }, + p: ({ children: markdownChildren }: PropsWithChildren) => + renderMarkdownText({ children: markdownChildren }), + br: () =>
, }; - const renderMarkdown = () => ( - - {text.toString()} - - ); + const renderMarkdown = (markdownText: string | number) => { + const formattedText = markdownText + .toString() + .replace(/\r\n|\r/g, "\n") + .replace( + /\n\n+/g, + (match) => + `\n\n${Array(match.length - 1) + .fill("  \n") + .join("")}\n`, + ) + .split("\n") + .map((line) => `${line} `) + .join("\n"); + + return ( + + {formattedText} + + ); + }; - const renderCopyButton = () => + const renderCopyButton = (copyText: string | number) => isCopyButton && ( - + ); - const renderLinkButton = () => + const renderLinkButton = (linkText: string | number) => isLinkButton && ( openModal({ type: "externalLink", - state: { externalLink: text.toString() }, + state: { externalLink: linkText.toString() }, }) } /> ); + const renderTextOrChildren = () => { + if (text != null) { + return ( + <> + {textVariant === "pill" + ? renderPillText() + : isMarkdown && !isSliderCard + ? renderMarkdown(text) + : renderStandardText()} + {renderCopyButton(text)} + {renderLinkButton(text)} + + ); + } + if (children != null) { + return children; + } + }; + return ( - {textVariant === "pill" - ? renderPillText() - : isMarkdown && !isSliderCard - ? renderMarkdown() - : renderStandardText()} - {renderCopyButton()} - {renderLinkButton()} + {renderTextOrChildren()} )} diff --git a/govtool/frontend/src/components/molecules/GovernanceActionCardTreasuryWithdrawalElement.tsx b/govtool/frontend/src/components/molecules/GovernanceActionCardTreasuryWithdrawalElement.tsx index fe642c989..69d286a3f 100644 --- a/govtool/frontend/src/components/molecules/GovernanceActionCardTreasuryWithdrawalElement.tsx +++ b/govtool/frontend/src/components/molecules/GovernanceActionCardTreasuryWithdrawalElement.tsx @@ -1,10 +1,7 @@ -import { Box } from "@mui/material"; import { useTranslation } from "react-i18next"; -import { Typography, CopyButton } from "@atoms"; import { correctVoteAdaFormat } from "@utils"; - -import { useScreenDimension } from "@/hooks"; +import { GovernanceActionCardElement } from "./GovernanceActionCardElement"; type Props = { receivingAddress: string; @@ -16,87 +13,22 @@ export const GovernanceActionCardTreasuryWithdrawalElement = ({ amount, }: Props) => { const { t } = useTranslation(); - const { isMobile } = useScreenDimension(); + return ( - - - - {t("govActions.receivingAddress")} - - - - {receivingAddress} - - - - - - - - - {t("govActions.amount")} - - - ₳ {correctVoteAdaFormat(amount) ?? 0} - - - + <> + + + ); }; diff --git a/govtool/frontend/src/components/molecules/GovernanceVotedOnCard.tsx b/govtool/frontend/src/components/molecules/GovernanceVotedOnCard.tsx index 59d84bfc9..ca4d5a066 100644 --- a/govtool/frontend/src/components/molecules/GovernanceVotedOnCard.tsx +++ b/govtool/frontend/src/components/molecules/GovernanceVotedOnCard.tsx @@ -56,6 +56,10 @@ export const GovernanceVotedOnCard = ({ bech32Prefix: "gov_action", }); + // When no status provided into the metadataStatus prop, + // we consider it as a valid + const isMetadataValid = metadataStatus === undefined; + return ( :last-child { + margin-bottom: 0; + } + + & tr:nth-child(2n) { + background-color: #d6e2ff80; + } + } +} 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 ( { const { isMobile } = useScreenDimension(); const { t } = useTranslation(); const { proposalId: txHash } = useParams(); - + const [isValidating, setIsValidating] = useState(true); + const [metadataStatus, setMetadataStatus] = useState< + MetadataValidationStatus | undefined + >(); + const [isMetadataValid, setIsMetadataValid] = useState(); const fullProposalId = txHash && getFullGovActionId(txHash, +index); const shortenedGovActionId = txHash && getShortenedGovActionId(txHash, +index); @@ -56,16 +60,12 @@ export const DashboardGovernanceActionDetails = () => { ); useEffect(() => { - if (data?.proposal) { + if (data?.proposal && typeof isMetadataValid !== "boolean") { setExtendedProposal(data.proposal); } - }, [data?.proposal]); + }, [data?.proposal, isMetadataValid]); const vote = (data ?? state)?.vote; - const [isValidating, setIsValidating] = useState(true); - const [metadataStatus, setMetadataStatus] = useState< - MetadataValidationStatus | undefined - >(); const { validateMetadata } = useValidateMutation(); useEffect(() => { @@ -74,7 +74,7 @@ export const DashboardGovernanceActionDetails = () => { const validate = async () => { setIsValidating(true); - const { status, metadata } = await validateMetadata({ + const { status, metadata, valid } = await validateMetadata({ standard: MetadataStandard.CIP108, url: extendedProposal?.url, hash: extendedProposal?.metadataHash ?? "", @@ -92,14 +92,15 @@ export const DashboardGovernanceActionDetails = () => { setMetadataStatus(status); setIsValidating(false); + setIsMetadataValid(valid); }; validate(); - }, [extendedProposal?.url]); + }, [extendedProposal?.url, extendedProposal?.metadataHash]); 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)`, }} > + "childNavItems" in item; + export const DrawerMobile = ({ isConnectButton, isDrawerOpen, setIsDrawerOpen, }: DrawerMobileProps) => { - const { - isProposalDiscussionForumEnabled, - isGovernanceOutcomesPillarEnabled, - } = useFeatureFlag(); const { screenWidth } = useScreenDimension(); const { openModal } = useModal(); const { t } = useTranslation(); @@ -85,18 +85,13 @@ export const DrawerMobile = ({ {NAV_ITEMS.map((navItem) => { - if ( - !isProposalDiscussionForumEnabled && - navItem.dataTestId === "proposed-governance-actions-link" - ) { - return null; - } - - if ( - !isGovernanceOutcomesPillarEnabled && - navItem.dataTestId === "governance-actions-outcomes-link" - ) { - return null; + if (isNavMenuItem(navItem)) { + return ( + setIsDrawerOpen(false)} + navItem={navItem} + /> + ); } return ( @@ -129,3 +124,60 @@ export const DrawerMobile = ({ ); }; + +const MenuNavItem: FC<{ + navItem: NavMenuItem; + closeDrawer: () => void; +}> = ({ closeDrawer, navItem }) => { + const { + isProposalDiscussionForumEnabled, + isGovernanceOutcomesPillarEnabled, + } = useFeatureFlag(); + + const filterChildNavItems = () => { + if (navItem.dataTestId === "governance-actions") { + return (navItem.childNavItems || []).filter((item) => { + if ( + !isProposalDiscussionForumEnabled && + item.dataTestId === "proposed-governance-actions-link" + ) + return false; + if ( + !isGovernanceOutcomesPillarEnabled && + item.dataTestId === "governance-actions-outcomes-link" + ) + return false; + return true; + }); + } + return navItem.childNavItems; + }; + return ( + <> + + + + {filterChildNavItems()?.map((childNavItem) => ( + + { + if (childNavItem.newTabLink) { + openInNewTab(childNavItem.newTabLink); + } + closeDrawer(); + }} + label={childNavItem.label} + size="big" + /> + + ))} + + ); +}; diff --git a/govtool/frontend/src/components/organisms/EditDRepInfoSteps/EditDRepForm.tsx b/govtool/frontend/src/components/organisms/EditDRepInfoSteps/EditDRepForm.tsx index eb3e93246..268e8936d 100644 --- a/govtool/frontend/src/components/organisms/EditDRepInfoSteps/EditDRepForm.tsx +++ b/govtool/frontend/src/components/organisms/EditDRepInfoSteps/EditDRepForm.tsx @@ -4,6 +4,7 @@ import { Box } from "@mui/material"; import { useCardano } from "@context"; import { + defaultEditDRepInfoValues, useEditDRepInfoForm, useGetDRepDetailsQuery, useTranslation, @@ -41,27 +42,24 @@ export const EditDRepForm = ({ useEffect(() => { 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/GovernanceActionDetailsCardData.tsx b/govtool/frontend/src/components/organisms/GovernanceActionDetailsCardData.tsx index 2d7524e4f..b38a2b2d0 100644 --- a/govtool/frontend/src/components/organisms/GovernanceActionDetailsCardData.tsx +++ b/govtool/frontend/src/components/organisms/GovernanceActionDetailsCardData.tsx @@ -1,8 +1,11 @@ -import { useMemo, useState } from "react"; +import { useMemo, useState, useEffect } from "react"; import { Box, Tabs, Tab, styled, Skeleton } from "@mui/material"; import { useLocation } from "react-router-dom"; +import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline"; +import CancelOutlinedIcon from "@mui/icons-material/CancelOutlined"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; -import { CopyButton, ExternalModalButton, Typography } from "@atoms"; +import { CopyButton, ExternalModalButton, Tooltip, Typography } from "@atoms"; import { GovernanceActionCardElement, GovernanceActionDetailsCardLinks, @@ -23,8 +26,10 @@ import { getFullGovActionId, mapArrayToObjectByKeys, encodeCIP129Identifier, + validateSignature, } from "@utils"; import { MetadataValidationStatus, ProposalData } from "@models"; +import { errorRed, successGreen } from "@/consts"; import { GovernanceActionType } from "@/types/governanceAction"; import { useAppContext } from "@/context"; @@ -76,6 +81,8 @@ export const GovernanceActionDetailsCardData = ({ isValidating, proposal: { abstract, + authors, + json: jsonContent, createdDate, createdEpochNo, details, @@ -365,6 +372,45 @@ export const GovernanceActionDetailsCardData = ({ /> )} + + + {(authors ?? []).length <= 0 + ? t("govActions.authors.noDataAvailable") + : (authors ?? []).map((author) => ( + + + {author.name} + + + + + ))} + + @@ -468,3 +514,62 @@ const HardforkDetailsTabContent = ({ ); }; + +const AuthorSignatureStatus = ({ + algorithm, + publicKey, + signature, + jsonContent, +}: { + algorithm?: string; + publicKey?: string; + signature?: string; + jsonContent?: Record; +}) => { + const { t } = useTranslation(); + const [isSignatureValid, setIsSignatureValid] = useState( + null, + ); + + useEffect(() => { + let cancelled = false; + async function checkSignature() { + const args = { + jsonContent, + algorithm, + publicKey, + signature, + }; + const result = await validateSignature(args); + if (!cancelled) setIsSignatureValid(result); + } + checkSignature(); + return () => { + cancelled = true; + }; + }, [algorithm, jsonContent, publicKey, signature]); + + if (isSignatureValid === null) { + return ; + } + return ( + + {isSignatureValid ? ( + + ) : ( + + )} + + ); +}; diff --git a/govtool/frontend/src/components/organisms/Hero.tsx b/govtool/frontend/src/components/organisms/Hero.tsx index e28c0d40a..a69726264 100644 --- a/govtool/frontend/src/components/organisms/Hero.tsx +++ b/govtool/frontend/src/components/organisms/Hero.tsx @@ -1,18 +1,14 @@ import { useMemo } from "react"; -import { useNavigate } from "react-router-dom"; import { Trans } from "react-i18next"; import { Box, Link } from "@mui/material"; +import { ArrowDownward } from "@mui/icons-material"; import { Button, Typography } from "@atoms"; -import { IMAGES, PATHS } from "@consts"; -import { useCardano, useModal } from "@context"; +import { IMAGES } from "@consts"; import { useScreenDimension, useTranslation } from "@hooks"; import { openInNewTab } from "@utils"; export const Hero = () => { - const { isEnabled } = useCardano(); - const { openModal } = useModal(); - const navigate = useNavigate(); const { isMobile, screenWidth } = useScreenDimension(); const { t } = useTranslation(); const IMAGE_SIZE = screenWidth < 640 ? 300 : screenWidth < 860 ? 400 : 600; @@ -33,7 +29,8 @@ export const Hero = () => { return screenWidth / 11; }, [screenWidth]); - const onClickVotingPower = () => openInNewTab("https://docs.gov.tools"); + const onClickVotingPower = () => + openInNewTab("https://docs.cardano.org/about-cardano/governance-overview"); return ( {