diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 3bb7d7c1a..552d997b6 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -272,3 +272,32 @@ jobs: "REACT_APP_USER_VERIFICATION_SERVER_URL=${{ env.REACT_APP_USER_VERIFICATION_SERVER_URL }}" tags: | ${{ env.PRIVATE_DOCKER_REGISTRY_URL }}/${{ env.APP_NAME }}:${{ env.ARTIFACT_VERSION }} + + publish-admin-cli: + runs-on: self-hosted + env: + APP_NAME: voting-admin-app + needs: build-version + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Docker Login + uses: docker/login-action@v2 + with: + registry: ${{ env.PRIVATE_DOCKER_REGISTRY_URL }} + username: ${{ env.PRIVATE_DOCKER_REGISTRY_USER }} + password: ${{ env.PRIVATE_DOCKER_REGISTRY_PASS }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build and Push docker image + uses: docker/build-push-action@v4 + env: + ARTIFACT_VERSION: ${{needs.build-version.outputs.ARTIFACT_VERSION}} + with: + context: backend-services/${{ env.APP_NAME }} + push: true + tags: | + ${{ env.PRIVATE_DOCKER_REGISTRY_URL }}/${{ env.APP_NAME }}:${{ env.ARTIFACT_VERSION }} diff --git a/.github/workflows/voting-admin-app-build.yml b/.github/workflows/voting-admin-app-build.yml new file mode 100644 index 000000000..e69de29bb diff --git a/.gitignore b/.gitignore index a8cb13092..113e10f2d 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ out/ ### Yaci-Store logs/* jpb-settings.xml + +application-prod.properties diff --git a/CHANGELOG.md b/CHANGELOG.md index f013407e4..1e067c4c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## [0.2.37](https://github.com/cardano-foundation/cf-cardano-ballot/compare/v0.2.36...v0.2.37) (2023-09-18) + + +### Features + +* discord fixes. ([#243](https://github.com/cardano-foundation/cf-cardano-ballot/issues/243)) ([4195aef](https://github.com/cardano-foundation/cf-cardano-ballot/commit/4195aef0eb8cf99076584f6f715d595e7c7d4fe8)) +* Discord user-verification flow and integration with Discord Bot. ([#210](https://github.com/cardano-foundation/cf-cardano-ballot/issues/210)) ([f9189fa](https://github.com/cardano-foundation/cf-cardano-ballot/commit/f9189facd37c9dc4579b9490f651f5c91c8ff5f5)) +* prod test - git ignore. ([0b58fd2](https://github.com/cardano-foundation/cf-cardano-ballot/commit/0b58fd2b9fd9fb88b690b97c72a2bccaae2096df)) +* prod test. ([c533b2a](https://github.com/cardano-foundation/cf-cardano-ballot/commit/c533b2a72568577149611c8eb0c10d7f5f3f0b13)) +* shelley account generation ([c53c6ef](https://github.com/cardano-foundation/cf-cardano-ballot/commit/c53c6ef88420c34e7cff2aca3679ed84586f001c)) +* shelley account generation. ([a30499e](https://github.com/cardano-foundation/cf-cardano-ballot/commit/a30499e49d0db72855a5faae5ddd220c43f450b8)) + + +### Bug Fixes + +* ability to run voting-app from command line. ([1c0349b](https://github.com/cardano-foundation/cf-cardano-ballot/commit/1c0349b47f09f1b510e3229f04de9b014fba983b)) +* **admin-cli:** ability to run voting-app from command line. ([af196ca](https://github.com/cardano-foundation/cf-cardano-ballot/commit/af196ca326a08b5e2a750aa124189f83bed030c9)) +* **cip-1694-ui:** support second question on ui ([02a87f3](https://github.com/cardano-foundation/cf-cardano-ballot/commit/02a87f3af48b5243ae75be9d3af31ea5753d446c)) +* **cip-1694-ui:** update to the latest be services ([8057e40](https://github.com/cardano-foundation/cf-cardano-ballot/commit/8057e40401eec41047ad493bf046f25b5fe795c5)) + +## [0.2.36](https://github.com/cardano-foundation/cf-cardano-ballot/compare/v0.2.35...v0.2.36) (2023-09-14) + + +### Features + +* keep memory low even for large number of votes. ([#204](https://github.com/cardano-foundation/cf-cardano-ballot/issues/204)) ([a121628](https://github.com/cardano-foundation/cf-cardano-ballot/commit/a121628b352a2e7b670a09988566d9e10f09509b)) + + +### Bug Fixes + +* Transactional readOnly bug fix. ([#220](https://github.com/cardano-foundation/cf-cardano-ballot/issues/220)) ([52eac44](https://github.com/cardano-foundation/cf-cardano-ballot/commit/52eac44451445492f54887df33954724b5508dca)) + ## [0.2.35](https://github.com/cardano-foundation/cf-cardano-ballot/compare/v0.2.34...v0.2.35) (2023-09-14) diff --git a/LICENSE b/LICENSE index a612ad981..1ed50d492 100644 --- a/LICENSE +++ b/LICENSE @@ -1,373 +1,21 @@ -Mozilla Public License Version 2.0 -================================== - -1. Definitions --------------- - -1.1. "Contributor" - means each individual or legal entity that creates, contributes to - the creation of, or owns Covered Software. - -1.2. "Contributor Version" - means the combination of the Contributions of others (if any) used - by a Contributor and that particular Contributor's Contribution. - -1.3. "Contribution" - means Covered Software of a particular Contributor. - -1.4. "Covered Software" - means Source Code Form to which the initial Contributor has attached - the notice in Exhibit A, the Executable Form of such Source Code - Form, and Modifications of such Source Code Form, in each case - including portions thereof. - -1.5. "Incompatible With Secondary Licenses" - means - - (a) that the initial Contributor has attached the notice described - in Exhibit B to the Covered Software; or - - (b) that the Covered Software was made available under the terms of - version 1.1 or earlier of the License, but not also under the - terms of a Secondary License. - -1.6. "Executable Form" - means any form of the work other than Source Code Form. - -1.7. "Larger Work" - means a work that combines Covered Software with other material, in - a separate file or files, that is not Covered Software. - -1.8. "License" - means this document. - -1.9. "Licensable" - means having the right to grant, to the maximum extent possible, - whether at the time of the initial grant or subsequently, any and - all of the rights conveyed by this License. - -1.10. "Modifications" - means any of the following: - - (a) any file in Source Code Form that results from an addition to, - deletion from, or modification of the contents of Covered - Software; or - - (b) any new file in Source Code Form that contains any Covered - Software. - -1.11. "Patent Claims" of a Contributor - means any patent claim(s), including without limitation, method, - process, and apparatus claims, in any patent Licensable by such - Contributor that would be infringed, but for the grant of the - License, by the making, using, selling, offering for sale, having - made, import, or transfer of either its Contributions or its - Contributor Version. - -1.12. "Secondary License" - means either the GNU General Public License, Version 2.0, the GNU - Lesser General Public License, Version 2.1, the GNU Affero General - Public License, Version 3.0, or any later versions of those - licenses. - -1.13. "Source Code Form" - means the form of the work preferred for making modifications. - -1.14. "You" (or "Your") - means an individual or a legal entity exercising rights under this - License. For legal entities, "You" includes any entity that - controls, is controlled by, or is under common control with You. For - purposes of this definition, "control" means (a) the power, direct - or indirect, to cause the direction or management of such entity, - whether by contract or otherwise, or (b) ownership of more than - fifty percent (50%) of the outstanding shares or beneficial - ownership of such entity. - -2. License Grants and Conditions --------------------------------- - -2.1. Grants - -Each Contributor hereby grants You a world-wide, royalty-free, -non-exclusive license: - -(a) under intellectual property rights (other than patent or trademark) - Licensable by such Contributor to use, reproduce, make available, - modify, display, perform, distribute, and otherwise exploit its - Contributions, either on an unmodified basis, with Modifications, or - as part of a Larger Work; and - -(b) under Patent Claims of such Contributor to make, use, sell, offer - for sale, have made, import, and otherwise transfer either its - Contributions or its Contributor Version. - -2.2. Effective Date - -The licenses granted in Section 2.1 with respect to any Contribution -become effective for each Contribution on the date the Contributor first -distributes such Contribution. - -2.3. Limitations on Grant Scope - -The licenses granted in this Section 2 are the only rights granted under -this License. No additional rights or licenses will be implied from the -distribution or licensing of Covered Software under this License. -Notwithstanding Section 2.1(b) above, no patent license is granted by a -Contributor: - -(a) for any code that a Contributor has removed from Covered Software; - or - -(b) for infringements caused by: (i) Your and any other third party's - modifications of Covered Software, or (ii) the combination of its - Contributions with other software (except as part of its Contributor - Version); or - -(c) under Patent Claims infringed by Covered Software in the absence of - its Contributions. - -This License does not grant any rights in the trademarks, service marks, -or logos of any Contributor (except as may be necessary to comply with -the notice requirements in Section 3.4). - -2.4. Subsequent Licenses - -No Contributor makes additional grants as a result of Your choice to -distribute the Covered Software under a subsequent version of this -License (see Section 10.2) or under the terms of a Secondary License (if -permitted under the terms of Section 3.3). - -2.5. Representation - -Each Contributor represents that the Contributor believes its -Contributions are its original creation(s) or it has sufficient rights -to grant the rights to its Contributions conveyed by this License. - -2.6. Fair Use - -This License is not intended to limit any rights You have under -applicable copyright doctrines of fair use, fair dealing, or other -equivalents. - -2.7. Conditions - -Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted -in Section 2.1. - -3. Responsibilities -------------------- - -3.1. Distribution of Source Form - -All distribution of Covered Software in Source Code Form, including any -Modifications that You create or to which You contribute, must be under -the terms of this License. You must inform recipients that the Source -Code Form of the Covered Software is governed by the terms of this -License, and how they can obtain a copy of this License. You may not -attempt to alter or restrict the recipients' rights in the Source Code -Form. - -3.2. Distribution of Executable Form - -If You distribute Covered Software in Executable Form then: - -(a) such Covered Software must also be made available in Source Code - Form, as described in Section 3.1, and You must inform recipients of - the Executable Form how they can obtain a copy of such Source Code - Form by reasonable means in a timely manner, at a charge no more - than the cost of distribution to the recipient; and - -(b) You may distribute such Executable Form under the terms of this - License, or sublicense it under different terms, provided that the - license for the Executable Form does not attempt to limit or alter - the recipients' rights in the Source Code Form under this License. - -3.3. Distribution of a Larger Work - -You may create and distribute a Larger Work under terms of Your choice, -provided that You also comply with the requirements of this License for -the Covered Software. If the Larger Work is a combination of Covered -Software with a work governed by one or more Secondary Licenses, and the -Covered Software is not Incompatible With Secondary Licenses, this -License permits You to additionally distribute such Covered Software -under the terms of such Secondary License(s), so that the recipient of -the Larger Work may, at their option, further distribute the Covered -Software under the terms of either this License or such Secondary -License(s). - -3.4. Notices - -You may not remove or alter the substance of any license notices -(including copyright notices, patent notices, disclaimers of warranty, -or limitations of liability) contained within the Source Code Form of -the Covered Software, except that You may alter any license notices to -the extent required to remedy known factual inaccuracies. - -3.5. Application of Additional Terms - -You may choose to offer, and to charge a fee for, warranty, support, -indemnity or liability obligations to one or more recipients of Covered -Software. However, You may do so only on Your own behalf, and not on -behalf of any Contributor. You must make it absolutely clear that any -such warranty, support, indemnity, or liability obligation is offered by -You alone, and You hereby agree to indemnify every Contributor for any -liability incurred by such Contributor as a result of warranty, support, -indemnity or liability terms You offer. You may include additional -disclaimers of warranty and limitations of liability specific to any -jurisdiction. - -4. Inability to Comply Due to Statute or Regulation ---------------------------------------------------- - -If it is impossible for You to comply with any of the terms of this -License with respect to some or all of the Covered Software due to -statute, judicial order, or regulation then You must: (a) comply with -the terms of this License to the maximum extent possible; and (b) -describe the limitations and the code they affect. Such description must -be placed in a text file included with all distributions of the Covered -Software under this License. Except to the extent prohibited by statute -or regulation, such description must be sufficiently detailed for a -recipient of ordinary skill to be able to understand it. - -5. Termination --------------- - -5.1. The rights granted under this License will terminate automatically -if You fail to comply with any of its terms. However, if You become -compliant, then the rights granted under this License from a particular -Contributor are reinstated (a) provisionally, unless and until such -Contributor explicitly and finally terminates Your grants, and (b) on an -ongoing basis, if such Contributor fails to notify You of the -non-compliance by some reasonable means prior to 60 days after You have -come back into compliance. Moreover, Your grants from a particular -Contributor are reinstated on an ongoing basis if such Contributor -notifies You of the non-compliance by some reasonable means, this is the -first time You have received notice of non-compliance with this License -from such Contributor, and You become compliant prior to 30 days after -Your receipt of the notice. - -5.2. If You initiate litigation against any entity by asserting a patent -infringement claim (excluding declaratory judgment actions, -counter-claims, and cross-claims) alleging that a Contributor Version -directly or indirectly infringes any patent, then the rights granted to -You by any and all Contributors for the Covered Software under Section -2.1 of this License shall terminate. - -5.3. In the event of termination under Sections 5.1 or 5.2 above, all -end user license agreements (excluding distributors and resellers) which -have been validly granted by You or Your distributors under this License -prior to termination shall survive termination. - -************************************************************************ -* * -* 6. Disclaimer of Warranty * -* ------------------------- * -* * -* Covered Software is provided under this License on an "as is" * -* basis, without warranty of any kind, either expressed, implied, or * -* statutory, including, without limitation, warranties that the * -* Covered Software is free of defects, merchantable, fit for a * -* particular purpose or non-infringing. The entire risk as to the * -* quality and performance of the Covered Software is with You. * -* Should any Covered Software prove defective in any respect, You * -* (not any Contributor) assume the cost of any necessary servicing, * -* repair, or correction. This disclaimer of warranty constitutes an * -* essential part of this License. No use of any Covered Software is * -* authorized under this License except under this disclaimer. * -* * -************************************************************************ - -************************************************************************ -* * -* 7. Limitation of Liability * -* -------------------------- * -* * -* Under no circumstances and under no legal theory, whether tort * -* (including negligence), contract, or otherwise, shall any * -* Contributor, or anyone who distributes Covered Software as * -* permitted above, be liable to You for any direct, indirect, * -* special, incidental, or consequential damages of any character * -* including, without limitation, damages for lost profits, loss of * -* goodwill, work stoppage, computer failure or malfunction, or any * -* and all other commercial damages or losses, even if such party * -* shall have been informed of the possibility of such damages. This * -* limitation of liability shall not apply to liability for death or * -* personal injury resulting from such party's negligence to the * -* extent applicable law prohibits such limitation. Some * -* jurisdictions do not allow the exclusion or limitation of * -* incidental or consequential damages, so this exclusion and * -* limitation may not apply to You. * -* * -************************************************************************ - -8. Litigation -------------- - -Any litigation relating to this License may be brought only in the -courts of a jurisdiction where the defendant maintains its principal -place of business and such litigation shall be governed by laws of that -jurisdiction, without reference to its conflict-of-law provisions. -Nothing in this Section shall prevent a party's ability to bring -cross-claims or counter-claims. - -9. Miscellaneous ----------------- - -This License represents the complete agreement concerning the subject -matter hereof. If any provision of this License is held to be -unenforceable, such provision shall be reformed only to the extent -necessary to make it enforceable. Any law or regulation which provides -that the language of a contract shall be construed against the drafter -shall not be used to construe this License against a Contributor. - -10. Versions of the License ---------------------------- - -10.1. New Versions - -Mozilla Foundation is the license steward. Except as provided in Section -10.3, no one other than the license steward has the right to modify or -publish new versions of this License. Each version will be given a -distinguishing version number. - -10.2. Effect of New Versions - -You may distribute the Covered Software under the terms of the version -of the License under which You originally received the Covered Software, -or under the terms of any subsequent version published by the license -steward. - -10.3. Modified Versions - -If you create software not governed by this License, and you want to -create a new license for such software, you may create and use a -modified version of this License if you rename the license and remove -any references to the name of the license steward (except to note that -such modified license differs from this License). - -10.4. Distributing Source Code Form that is Incompatible With Secondary -Licenses - -If You choose to distribute Source Code Form that is Incompatible With -Secondary Licenses under the terms of this version of the License, the -notice described in Exhibit B of this License must be attached. - -Exhibit A - Source Code Form License Notice -------------------------------------------- - - This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. - -If it is not possible or desirable to put the notice in a particular -file, then You may include the notice in a location (such as a LICENSE -file in a relevant directory) where a recipient would be likely to look -for such a notice. - -You may add additional accurate notices of copyright ownership. - -Exhibit B - "Incompatible With Secondary Licenses" Notice ---------------------------------------------------------- - - This Source Code Form is "Incompatible With Secondary Licenses", as - defined by the Mozilla Public License, v. 2.0. +MIT License + +Copyright (c) 2023 Cardano Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/backend-services/user-verification-service/build.gradle.kts b/backend-services/user-verification-service/build.gradle.kts index 5034067a5..04929c7bf 100644 --- a/backend-services/user-verification-service/build.gradle.kts +++ b/backend-services/user-verification-service/build.gradle.kts @@ -6,8 +6,8 @@ plugins { java id("org.springframework.boot") version "3.1.3" id("io.spring.dependency-management") version "1.1.3" - id("org.graalvm.buildtools.native") version "0.9.26" - id("org.flywaydb.flyway") version "9.22.0" + id("org.graalvm.buildtools.native") version "0.9.27" + id("org.flywaydb.flyway") version "9.22.1" id("cz.habarta.typescript-generator") version "3.2.1263" id("com.github.ben-manes.versions") version "0.48.0" id("com.gorylenko.gradle-git-properties") version "2.4.1" @@ -41,6 +41,7 @@ dependencies { testCompileOnly("org.springframework.boot:spring-boot-starter-test") implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-web") testImplementation("org.springframework.boot:spring-boot-starter-test") @@ -66,13 +67,15 @@ dependencies { implementation("com.bloxbean.cardano:cardano-client-address:0.5.0-beta3") - implementation("software.amazon.awssdk:sns:2.20.136") + implementation("software.amazon.awssdk:sns:2.20.149") implementation("io.vavr:vavr:0.10.4") runtimeOnly("org.postgresql:postgresql") - // spring-boot overridden dependencies: + implementation("org.cardanofoundation:cip30-data-signature-parser:0.0.10") + + // spring-boot overridden dependencies: runtimeOnly("com.h2database:h2:2.2.222") // GraalVM compatibility } diff --git a/backend-services/user-verification-service/scripts/cip_30_sign.sc b/backend-services/user-verification-service/scripts/cip_30_sign.sc new file mode 100644 index 000000000..a39ef6443 --- /dev/null +++ b/backend-services/user-verification-service/scripts/cip_30_sign.sc @@ -0,0 +1,58 @@ +// brew install amm +// amm cip_30_sign.sc + +import $ivy.`com.bloxbean.cardano:cardano-client-lib:0.5.0-beta2` +import $ivy.`com.bloxbean.cardano:cardano-client-cip30:0.5.0-beta2` + +import $ivy.`com.lihaoyi:requests_3:0.8.0` +import $ivy.`com.fasterxml.jackson.core:jackson-core:2.15.2` + +import $ivy.`org.slf4j:slf4j-simple:2.0.9` + +import com.bloxbean.cardano.client.account._ +import com.bloxbean.cardano.client.common.model._ + +import com.bloxbean.cardano.client.cip.cip30._ + +import com.bloxbean.cardano.client.address._ + +import com.fasterxml.jackson.databind._ + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +val mapper = new ObjectMapper() + +val orgMnemonic = "ocean sad mixture disease faith once celery mind clay hidden brush brown you sponsor dawn good claim gloom market world online twist laptop thrive" + +val organiserAccount = new Account(Networks.testnet(), orgMnemonic) + +val logger = LoggerFactory.getLogger(getClass()); + +def signDiscordCIP30(): Unit = { + val stakeAddress = organiserAccount.stakeAddress() + val stakeAddressAccount = new Address(stakeAddress) + + val input = s"938c2cc0dcc05f2b68c4287040cfcf72|dupa.jasiu" + + println(input) + + val cip30Result = CIP30DataSigner.INSTANCE.signData( + stakeAddressAccount.getBytes(), + input.getBytes("UTF-8"), + organiserAccount.stakeHdKeyPair().getPrivateKey().getKeyData(), + organiserAccount.stakeHdKeyPair().getPublicKey().getKeyData() + ); + + val output = s""" + Signature: ${cip30Result.signature()} + Public-Key: ${cip30Result.key()} +""".stripMargin + + println(output) +} + +@main +def main() = { + signDiscordCIP30() +} diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/UserVerificationApp.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/UserVerificationApp.java index 0d8ee184a..d50934e34 100644 --- a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/UserVerificationApp.java +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/UserVerificationApp.java @@ -11,18 +11,19 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ImportRuntimeHints; -import org.springframework.core.io.ClassPathResource; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.transaction.annotation.EnableTransactionManagement; import static org.springframework.aot.hint.ExecutableMode.INVOKE; -@SpringBootApplication(exclude = { SecurityAutoConfiguration.class, ErrorMvcAutoConfiguration.class }) +@SpringBootApplication(exclude = { SecurityAutoConfiguration.class, ErrorMvcAutoConfiguration.class, UserDetailsServiceAutoConfiguration.class }) @EnableJpaRepositories("org.cardano.foundation.voting.repository") @EntityScan(basePackages = "org.cardano.foundation.voting.domain.entity") @ComponentScan(basePackages = { @@ -34,6 +35,7 @@ }) @EnableScheduling @EnableTransactionManagement +@EnableWebSecurity @Slf4j @ImportRuntimeHints(UserVerificationApp.Hints.class) public class UserVerificationApp { @@ -55,8 +57,7 @@ static class Hints implements RuntimeHintsRegistrar { @SneakyThrows public void registerHints(RuntimeHints hints, ClassLoader classLoader) { hints.reflection().registerMethod(TimedAspect.class.getMethod("timedMethod", ProceedingJoinPoint.class), INVOKE); - hints.resources().registerResource(new ClassPathResource("db/migration/h2/V0__user_verification_service_init.sql")); - hints.resources().registerResource(new ClassPathResource("db/migration/postgresql/V0__user_verification_service_init.sql")); + hints.resources().registerPattern("*.sql"); } } diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/config/SpringSecurityConfiguration.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/config/SpringSecurityConfiguration.java new file mode 100644 index 000000000..779cd484a --- /dev/null +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/config/SpringSecurityConfiguration.java @@ -0,0 +1,118 @@ +package org.cardano.foundation.voting.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.cors.CorsConfiguration; +import org.zalando.problem.spring.web.advice.security.SecurityProblemSupport; + +import java.util.Arrays; +import java.util.List; + +import static org.springframework.http.HttpMethod.*; + +@Configuration +@Import(SecurityProblemSupport.class) +public class SpringSecurityConfiguration { + + @Autowired + private SecurityProblemSupport problemSupport; + + @Value("${cors.allowed.origins:http://localhost:3000}") + private String allowedUrls; + + @Value("${discord.bot.username:discord_bot}") + private String discordBotUsername; + + @Value("${discord.bot.password}") + private String discordBotPassword; + + @ConditionalOnProperty( //to make sure it is active if console is enabled + value="spring.h2.console.enabled", + havingValue = "true", + matchIfMissing = false) + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> web.ignoring().requestMatchers(new AntPathRequestMatcher("/h2-console/**")); + } + + @Bean + public AuthenticationEntryPoint authenticationEntryPoint() { + var basicAuthenticationEntryPoint = new BasicAuthenticationEntryPoint(); + basicAuthenticationEntryPoint.setRealmName("DISCORD"); + + return basicAuthenticationEntryPoint; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .sessionManagement(AbstractHttpConfigurer::disable) + .cors(c -> c.configurationSource(request -> { + var cors = new CorsConfiguration(); + cors.setAllowedMethods(List.of("GET", "POST", "PUT")); + cors.setAllowedHeaders(List.of("*")); + + var urls = Arrays.stream(allowedUrls.split(",")).toList(); + + cors.setAllowedOrigins(urls); + + return cors; + })) + .csrf(AbstractHttpConfigurer::disable) + .anonymous(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests + .requestMatchers(new AntPathRequestMatcher("/api/sms/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/api/discord/user-verification/is-verified/**", GET.name())).hasRole("BOT") + .requestMatchers(new AntPathRequestMatcher("/api/discord/user-verification/start-verification", POST.name())).hasRole("BOT") + .requestMatchers(new AntPathRequestMatcher("/api/discord/user-verification/start-verification", PUT.name())).hasRole("BOT") + .requestMatchers(new AntPathRequestMatcher("/api/discord/user-verification/check-verification", POST.name())).permitAll() + + .requestMatchers(new AntPathRequestMatcher("/api/user-verification/verified/**", GET.name())).permitAll() + .anyRequest().denyAll() + ) + .rememberMe(AbstractHttpConfigurer::disable) + .anonymous(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .logout(AbstractHttpConfigurer::disable) + + .httpBasic(c -> c.authenticationEntryPoint(authenticationEntryPoint())) + .exceptionHandling(c -> c.authenticationEntryPoint(problemSupport) + .accessDeniedHandler(problemSupport)); + + return http.build(); + } + + @Bean + public InMemoryUserDetailsManager userDetailsService() { + UserDetails user = User + .withUsername(discordBotUsername) + .password(passwordEncoder().encode(discordBotPassword)) + .roles("BOT") + .build(); + + return new InMemoryUserDetailsManager(user); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(8); + } + +} \ No newline at end of file diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/IsVerifiedRequest.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/IsVerifiedRequest.java index 1a0c9fbcd..64913fd3c 100644 --- a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/IsVerifiedRequest.java +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/IsVerifiedRequest.java @@ -10,11 +10,10 @@ @AllArgsConstructor public class IsVerifiedRequest { - @NotBlank - private String stakeAddress; - @NotBlank private String eventId; + @NotBlank + private String stakeAddress; } diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/IsVerifiedResponse.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/IsVerifiedResponse.java index d19c5c5e4..f467f7bd3 100644 --- a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/IsVerifiedResponse.java +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/IsVerifiedResponse.java @@ -1,10 +1,13 @@ package org.cardano.foundation.voting.domain; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; +import lombok.ToString; @AllArgsConstructor @Getter +@ToString public class IsVerifiedResponse { private boolean isVerified; diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/VerificationStatus.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/VerificationStatus.java new file mode 100644 index 000000000..5f1cf80c1 --- /dev/null +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/VerificationStatus.java @@ -0,0 +1,6 @@ +package org.cardano.foundation.voting.domain; + +public enum VerificationStatus { + PENDING, + VERIFIED, +} diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/DiscordCheckVerificationRequest.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/discord/DiscordCheckVerificationRequest.java similarity index 83% rename from backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/DiscordCheckVerificationRequest.java rename to backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/discord/DiscordCheckVerificationRequest.java index 369664e3b..96651bfe5 100644 --- a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/DiscordCheckVerificationRequest.java +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/discord/DiscordCheckVerificationRequest.java @@ -1,4 +1,4 @@ -package org.cardano.foundation.voting.domain; +package org.cardano.foundation.voting.domain.discord; import jakarta.validation.constraints.NotBlank; import lombok.Builder; @@ -15,11 +15,14 @@ public class DiscordCheckVerificationRequest { @NotBlank - private String secret; + private String eventId; @NotBlank private String stakeAddress; + @NotBlank + private String secret; + @NotBlank protected String coseSignature; diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/DiscordStartVerificationRequest.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/discord/DiscordStartVerificationRequest.java similarity index 76% rename from backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/DiscordStartVerificationRequest.java rename to backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/discord/DiscordStartVerificationRequest.java index aefb15f22..470a4a370 100644 --- a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/DiscordStartVerificationRequest.java +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/discord/DiscordStartVerificationRequest.java @@ -1,4 +1,4 @@ -package org.cardano.foundation.voting.domain; +package org.cardano.foundation.voting.domain.discord; import jakarta.validation.constraints.NotBlank; import lombok.Builder; @@ -13,9 +13,9 @@ public class DiscordStartVerificationRequest { @NotBlank - private String secret; + private String discordIdHash; @NotBlank - private String hashedDiscordId; + private String secret; } diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/discord/DiscordStartVerificationResponse.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/discord/DiscordStartVerificationResponse.java new file mode 100644 index 000000000..aca7419d1 --- /dev/null +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/discord/DiscordStartVerificationResponse.java @@ -0,0 +1,19 @@ +package org.cardano.foundation.voting.domain.discord; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import org.cardano.foundation.voting.domain.VerificationStatus; + +@Builder +@Getter +@AllArgsConstructor +public class DiscordStartVerificationResponse { + + private String eventId; + + private String discordIdHash; + + private VerificationStatus status; + +} diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/entity/DiscordUserVerification.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/entity/DiscordUserVerification.java new file mode 100644 index 000000000..794ce2914 --- /dev/null +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/entity/DiscordUserVerification.java @@ -0,0 +1,78 @@ +package org.cardano.foundation.voting.domain.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; +import lombok.extern.slf4j.Slf4j; +import org.cardano.foundation.voting.domain.VerificationStatus; + +import javax.annotation.Nullable; +import java.time.LocalDateTime; +import java.util.Optional; + +@Entity +@Table(name = "discord_user_verification") +@Slf4j +@NoArgsConstructor +@SuperBuilder +@AllArgsConstructor +public class DiscordUserVerification extends AbstractTimestampEntity { + + @Id + @Column(name = "id", nullable = false) + @Getter + @Setter + private String discordIdHash; + + @Column(name = "event_id", nullable = false) + @Getter + @Setter + private String eventId; + + @Column(name = "stake_address") + @Nullable + private String stakeAddress; + + @Column(name = "secret_code", nullable = false) + @Getter + @Setter + private String secretCode; + + @Column(name = "status", nullable = false) + @Enumerated(EnumType.STRING) + @Getter + @Setter + private VerificationStatus status; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "expires_at", nullable = false) + @Getter + @Setter + private LocalDateTime expiresAt; + + public Optional getStakeAddress() { + return Optional.ofNullable(stakeAddress); + } + + public void setStakeAddress(Optional stakeAddress) { + this.stakeAddress = stakeAddress.orElse(null); + } + + @Override + public String toString() { + return "DiscordUserVerification{" + + "discordIdHash='" + discordIdHash + '\'' + + ", stakeAddress='" + stakeAddress + '\'' + + ", eventId='" + eventId + '\'' + + ", verificationCode='" + secretCode + '\'' + + ", expiresAt=" + expiresAt + + ", status=" + status + + ", createdAt=" + createdAt + + ", updatedAt=" + updatedAt + + '}'; + } + +} diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/entity/UserVerification.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/entity/SMSUserVerification.java similarity index 61% rename from backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/entity/UserVerification.java rename to backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/entity/SMSUserVerification.java index bd2113d69..fc5c02968 100644 --- a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/entity/UserVerification.java +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/entity/SMSUserVerification.java @@ -1,25 +1,23 @@ package org.cardano.foundation.voting.domain.entity; import jakarta.persistence.*; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.SuperBuilder; import lombok.extern.slf4j.Slf4j; +import org.cardano.foundation.voting.domain.VerificationStatus; -import javax.annotation.Nullable; import java.time.LocalDateTime; -import java.util.Optional; @Entity -@Table(name = "user_verification") +@Table(name = "sms_user_verification") @Slf4j -@SuperBuilder @NoArgsConstructor @Getter @Setter -public class UserVerification extends AbstractTimestampEntity { +@SuperBuilder +public class SMSUserVerification extends AbstractTimestampEntity { @Id @Column(name = "id", nullable = false) @@ -42,48 +40,15 @@ public class UserVerification extends AbstractTimestampEntity { @Column(name = "status", nullable = false) @Enumerated(EnumType.STRING) - @Builder.Default - @Getter - @Setter - private Status status = Status.NOT_REQUESTED; - - @Column(name = "provider", nullable = false) - @Enumerated(EnumType.STRING) - @Getter - @Setter - private Provider provider; - - @Column(name = "channel", nullable = false) - @Enumerated(EnumType.STRING) - @Getter - @Setter - private Channel channel; + private VerificationStatus status; @Temporal(TemporalType.TIMESTAMP) @Column(name = "expires_at", nullable = false) - @Builder.Default - @Getter - @Setter - private LocalDateTime expiresAt = LocalDateTime.now(); // TODO clock - - public enum Channel { - SMS, - } - - public enum Status { - NOT_REQUESTED, - PENDING, - VERIFIED, - } - - public enum Provider { - TWILIO, - AWS_SNS - } + private LocalDateTime expiresAt; @Override public String toString() { - return "UserVerification{" + + return "SMSUserVerification{" + "id='" + id + '\'' + ", stakeAddress='" + stakeAddress + '\'' + ", eventId='" + eventId + '\'' + @@ -92,10 +57,9 @@ public String toString() { ", expiresAt=" + expiresAt + ", phoneNumberHash='" + phoneNumberHash + '\'' + ", status=" + status + - ", provider=" + provider + - ", channel=" + channel + ", createdAt=" + createdAt + ", updatedAt=" + updatedAt + '}'; } + } diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/SMSCheckVerificationRequest.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/sms/SMSCheckVerificationRequest.java similarity index 91% rename from backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/SMSCheckVerificationRequest.java rename to backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/sms/SMSCheckVerificationRequest.java index 49bb8a886..710eb7f8a 100644 --- a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/SMSCheckVerificationRequest.java +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/sms/SMSCheckVerificationRequest.java @@ -1,4 +1,4 @@ -package org.cardano.foundation.voting.domain; +package org.cardano.foundation.voting.domain.sms; import jakarta.validation.constraints.NotBlank; import lombok.Builder; diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/SMSStartVerificationRequest.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/sms/SMSStartVerificationRequest.java similarity index 90% rename from backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/SMSStartVerificationRequest.java rename to backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/sms/SMSStartVerificationRequest.java index 425986780..5bc5f1c5c 100644 --- a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/SMSStartVerificationRequest.java +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/sms/SMSStartVerificationRequest.java @@ -1,4 +1,4 @@ -package org.cardano.foundation.voting.domain; +package org.cardano.foundation.voting.domain.sms; import jakarta.validation.constraints.NotBlank; import lombok.Builder; diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/SMSStartVerificationResponse.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/sms/SMSStartVerificationResponse.java similarity index 87% rename from backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/SMSStartVerificationResponse.java rename to backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/sms/SMSStartVerificationResponse.java index d7c4b2424..5cc87a410 100644 --- a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/SMSStartVerificationResponse.java +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/domain/sms/SMSStartVerificationResponse.java @@ -1,4 +1,4 @@ -package org.cardano.foundation.voting.domain; +package org.cardano.foundation.voting.domain.sms; import java.time.LocalDateTime; diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/jobs/CleanupAllForFinishedEventsJob.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/jobs/CleanupAllForFinishedEventsJob.java new file mode 100644 index 000000000..e7672fd40 --- /dev/null +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/jobs/CleanupAllForFinishedEventsJob.java @@ -0,0 +1,54 @@ +package org.cardano.foundation.voting.jobs; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.cardano.foundation.voting.client.ChainFollowerClient; +import org.cardano.foundation.voting.service.discord.DiscordUserVerificationService; +import org.cardano.foundation.voting.service.sms.SMSUserVerificationService; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +@RequiredArgsConstructor +public class CleanupAllForFinishedEventsJob implements Runnable { + + private final ChainFollowerClient chainFollowerClient; + + private final SMSUserVerificationService smsUserVerificationService; + + private final DiscordUserVerificationService discordUserVerificationService; + + @Scheduled(cron = "${finished.verifications.cleanup.job.cron}") + public void run() { + log.info("Cleaning up verified phones for event..."); + + var allEventsE = chainFollowerClient.findAllEvents(); + + if (allEventsE.isEmpty()) { + log.warn("No events found in ledger follower, skipping cleanup job."); + return; + } + + var allEvents = allEventsE.get(); + + allEvents.forEach(eventSummary -> { + if (eventSummary.finished()) { + + log.info("Event:{} is finished, removing all sms verifications...", eventSummary.id()); + smsUserVerificationService.findAllForEvent(eventSummary.id()).forEach(userVerification -> { + log.info("Removing historical user sms verification... since eventId:{} is finished.", eventSummary.id()); + smsUserVerificationService.removeUserVerification(userVerification); + }); + + log.info("Event:{} is finished, removing all discord verifications...", eventSummary.id()); + discordUserVerificationService.findAllForEvent(eventSummary.id()).forEach(userVerification -> { + log.info("Removing historical user discord verification... since eventId:{} is finished.", eventSummary.id()); + discordUserVerificationService.removeUserVerification(userVerification); + }); + + } + }); + } + +} diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/jobs/CleanupVerifiedPhonesForEventJob.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/jobs/CleanupVerifiedPhonesForEventJob.java deleted file mode 100644 index cedd8961a..000000000 --- a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/jobs/CleanupVerifiedPhonesForEventJob.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.cardano.foundation.voting.jobs; - -import lombok.extern.slf4j.Slf4j; -import org.cardano.foundation.voting.client.ChainFollowerClient; -import org.cardano.foundation.voting.service.verify.SMSUserVerificationService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; - -@Service -@Slf4j -public class CleanupVerifiedPhonesForEventJob implements Runnable { - - @Autowired - private ChainFollowerClient chainFollowerClient; - - @Autowired - private SMSUserVerificationService smsUserVerificationService; - - @Scheduled(cron = "${finished.verifications.cleanup.job.cron}") - public void run() { - log.info("Cleaning up verified phones for event..."); - - var allEventsE = chainFollowerClient.findAllEvents(); - - if (allEventsE.isEmpty()) { - log.warn("No events found in ledger follower, skipping cleanup job."); - return; - } - - var allEvents = allEventsE.get(); - - allEvents.forEach(eventSummary -> { - smsUserVerificationService.findAllForEvent(eventSummary.id()).forEach(userVerification -> { - if (eventSummary.finished()) { - log.info("Removing historical user verification... since eventId:{} is finished.", eventSummary.id()); - smsUserVerificationService.removeUserVerification(userVerification); - } - }); - }); - } - -} diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/jobs/PendingVerificationCleanupJob.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/jobs/PendingVerificationCleanupJob.java new file mode 100644 index 000000000..692b53b01 --- /dev/null +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/jobs/PendingVerificationCleanupJob.java @@ -0,0 +1,80 @@ +package org.cardano.foundation.voting.jobs; + +import lombok.extern.slf4j.Slf4j; +import org.cardano.foundation.voting.client.ChainFollowerClient; +import org.cardano.foundation.voting.service.discord.DiscordUserVerificationService; +import org.cardano.foundation.voting.service.sms.SMSUserVerificationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.time.Clock; +import java.time.LocalDateTime; + +@Service +@Slf4j +public class PendingVerificationCleanupJob implements Runnable { + + @Autowired + private ChainFollowerClient chainFollowerClient; + + @Autowired + private SMSUserVerificationService smsUserVerificationService; + + @Autowired + private DiscordUserVerificationService discordUserVerificationService; + + @Autowired + private Clock clock; + + @Value("${pending.verification.expiration.time.hours}") + private int pendingVerificationExpirationTimeHours; + + @Scheduled(cron = "${pending.verification.phone.cleanup.job.cron}") + public void run() { + log.info("Running pending phone cleanup job..."); + + var allEventsE = chainFollowerClient.findAllEvents(); + + if (allEventsE.isEmpty()) { + log.warn("No events found in ledger follower, skipping pending phone cleanup job"); + return; + } + + var allEvents = allEventsE.get(); + + allEvents.forEach(eventSummary -> { + var id = eventSummary.id(); + + log.info("Cleaning up pending phone verifications for event: {}", id); + smsUserVerificationService.findAllPending(id).forEach(userVerification -> { + var now = LocalDateTime.now(clock); + + boolean expiredVerification = now.isAfter(userVerification.getCreatedAt() + .plusHours(pendingVerificationExpirationTimeHours)); + + if (expiredVerification) { + log.info("Deleting expired pending user verification: {}", userVerification); + + smsUserVerificationService.removeUserVerification(userVerification); + } + }); + + log.info("Cleaning up pending discord verifications for event: {}", id); + discordUserVerificationService.findAllPending(id).forEach(userVerification -> { + var now = LocalDateTime.now(clock); + + boolean expiredVerification = now.isAfter(userVerification.getCreatedAt() + .plusHours(pendingVerificationExpirationTimeHours)); + + if (expiredVerification) { + log.info("Deleting expired pending user verification: {}", userVerification); + discordUserVerificationService.removeUserVerification(userVerification); + } + }); + + }); + } + +} diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/jobs/PendingVerificationPhoneCleanupJob.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/jobs/PendingVerificationPhoneCleanupJob.java deleted file mode 100644 index 863b17f9b..000000000 --- a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/jobs/PendingVerificationPhoneCleanupJob.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.cardano.foundation.voting.jobs; - -import lombok.extern.slf4j.Slf4j; -import org.cardano.foundation.voting.client.ChainFollowerClient; -import org.cardano.foundation.voting.service.verify.SMSUserVerificationService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; - -import java.time.Clock; -import java.time.LocalDateTime; - -@Service -@Slf4j -public class PendingVerificationPhoneCleanupJob implements Runnable { - - @Autowired - private ChainFollowerClient chainFollowerClient; - - @Autowired - private SMSUserVerificationService smsUserVerificationService; - - @Autowired - private Clock clock; - - @Value("${pending.verification.phone.expiration.time.hours}") - private int pendingVerificationPhoneExpirationTimeHours; - - @Scheduled(cron = "${pending.verification.phone.cleanup.job.cron}") - public void run() { - log.info("Running pending phone cleanup job..."); - - var allEventsE = chainFollowerClient.findAllEvents(); - - if (allEventsE.isEmpty()) { - log.warn("No events found in ledger follower, skipping pending phone cleanup job"); - return; - } - - var allEvents = allEventsE.get(); - - allEvents.forEach(eventSummary -> { - smsUserVerificationService.findAllPending(eventSummary.id()).forEach(userVerification -> { - var now = LocalDateTime.now(clock); - - if (now.isAfter(userVerification.getCreatedAt().plusHours(pendingVerificationPhoneExpirationTimeHours))) { - log.info("Deleting expired pending user verification: {}", userVerification); - - smsUserVerificationService.removeUserVerification(userVerification); - } - }); - }); - } - -} diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/repository/DiscordUserVerificationRepository.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/repository/DiscordUserVerificationRepository.java new file mode 100644 index 000000000..a6ed43f28 --- /dev/null +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/repository/DiscordUserVerificationRepository.java @@ -0,0 +1,38 @@ +package org.cardano.foundation.voting.repository; + +import org.cardano.foundation.voting.domain.entity.DiscordUserVerification; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface DiscordUserVerificationRepository extends JpaRepository { + + @Query("SELECT uv FROM DiscordUserVerification uv WHERE uv.status = 'VERIFIED' AND uv.eventId = :eventId AND uv.stakeAddress = :stakeAddress") + List findCompletedVerifications(@Param("eventId") String eventId, + @Param("stakeAddress") String stakeAddress + ); + + @Query("SELECT uv FROM DiscordUserVerification uv WHERE uv.status = 'VERIFIED' AND uv.eventId = :eventId AND uv.discordIdHash = :discordIdHash") + Optional findCompletedVerificationBasedOnDiscordUserHash(@Param("eventId") String eventId, + @Param("discordIdHash") String discordIdHash + ); + + @Query("SELECT uv FROM DiscordUserVerification uv WHERE uv.status = 'PENDING'" + + " AND uv.eventId = :eventId" + + " AND uv.discordIdHash = :discordIdHash") + Optional findPendingVerificationBasedOnDiscordUserHash(@Param("eventId") String eventId, + @Param("discordIdHash") String discordIdHash + ); + + @Query("SELECT uv FROM DiscordUserVerification uv WHERE uv.eventId = :eventId") + List findAllForEvent(String eventId); + + @Query("SELECT uv FROM DiscordUserVerification uv WHERE uv.eventId = :eventId AND uv.status = 'PENDING'") + List findAllPending(String eventId); + +} diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/repository/SMSUserVerificationRepository.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/repository/SMSUserVerificationRepository.java new file mode 100644 index 000000000..4fad3e898 --- /dev/null +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/repository/SMSUserVerificationRepository.java @@ -0,0 +1,39 @@ +package org.cardano.foundation.voting.repository; + +import org.cardano.foundation.voting.domain.entity.SMSUserVerification; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface SMSUserVerificationRepository extends JpaRepository { + + @Query("SELECT uv FROM SMSUserVerification uv WHERE uv.eventId = :eventId") + List findAllByEventId(@Param("eventId") String eventId); + + @Query("SELECT uv FROM SMSUserVerification uv WHERE uv.status = 'VERIFIED' AND uv.eventId = :eventId AND uv.phoneNumberHash = :phoneNumberHash") + List findAllCompletedPerPhone(@Param("eventId") String eventId, @Param("phoneNumberHash") String phoneNumberHash); + + @Query("SELECT uv FROM SMSUserVerification uv WHERE uv.status = 'VERIFIED' AND uv.eventId = :eventId AND uv.stakeAddress = :stakeAddress") + List findAllCompletedPerStake(@Param("eventId") String eventId, @Param("stakeAddress") String stakeAddress); + + @Query("SELECT uv FROM SMSUserVerification uv WHERE uv.status = 'PENDING' AND uv.eventId = :eventId") + List findAllPending(@Param("eventId") String eventId); + + @Query("SELECT COUNT(*) FROM SMSUserVerification uv WHERE uv.status = 'PENDING' AND uv.eventId = :eventId AND uv.stakeAddress = :stakeAddress AND uv.phoneNumberHash = :phoneNumberHash") + int findPendingPerStakeAddressPerPhoneCount(@Param("eventId") String eventId, @Param("stakeAddress") String stakeAddress, @Param("phoneNumberHash") String phoneNumberHash); + + @Query("SELECT uv FROM SMSUserVerification uv WHERE uv.status = 'PENDING'" + + " AND uv.eventId = :eventId" + + " AND uv.stakeAddress = :stakeAddress" + + " AND uv.requestId = :requestId") + Optional findPendingVerificationsByEventIdAndStakeAddressAndRequestId(@Param("eventId") String eventId, + @Param("stakeAddress") String stakeAddress, + @Param("requestId") String requestId + ); + +} diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/repository/UserVerificationRepository.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/repository/UserVerificationRepository.java deleted file mode 100644 index 189f57d4f..000000000 --- a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/repository/UserVerificationRepository.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.cardano.foundation.voting.repository; - -import org.cardano.foundation.voting.domain.entity.UserVerification; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface UserVerificationRepository extends JpaRepository { - - @Query("SELECT uv FROM UserVerification uv WHERE uv.eventId = :eventId") - List findAllByEventId(@Param("eventId") String eventId); - - @Query("SELECT uv FROM UserVerification uv WHERE uv.status = 'VERIFIED' AND uv.eventId = :eventId AND uv.phoneNumberHash = :phoneNumberHash") - List findAllCompletedPerPhone(@Param("eventId") String eventId, @Param("phoneNumberHash") String phoneNumberHash); - - @Query("SELECT uv FROM UserVerification uv WHERE uv.status = 'VERIFIED' AND uv.eventId = :eventId AND uv.stakeAddress = :stakeAddress") - List findAllCompletedPerStake(@Param("eventId") String eventId, @Param("stakeAddress") String stakeAddress); - - @Query("SELECT uv FROM UserVerification uv WHERE uv.status = 'PENDING' AND uv.eventId = :eventId") - List findAllPending(@Param("eventId") String eventId); - - @Query("SELECT COUNT(*) FROM UserVerification uv WHERE uv.status = 'PENDING' AND uv.eventId = :eventId AND uv.stakeAddress = :stakeAddress AND uv.phoneNumberHash = :phoneNumberHash") - int findPendingPerStakeAddressPerPhoneCount(@Param("eventId") String eventId, @Param("stakeAddress") String stakeAddress, @Param("phoneNumberHash") String phoneNumberHash); - - @Query("SELECT uv FROM UserVerification uv WHERE uv.status = 'PENDING'" + - " AND uv.eventId = :eventId" + - " AND uv.stakeAddress = :stakeAddress" + - " AND uv.requestId = :requestId") - Optional findPendingVerificationsByEventIdAndStakeAddressAndRequestId(@Param("eventId") String eventId, - @Param("stakeAddress") String stakeAddress, - @Param("requestId") String requestId - ); - -} diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/resource/DiscordUserVerificationResource.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/resource/DiscordUserVerificationResource.java index 16d3fcf44..6d33bbc9a 100644 --- a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/resource/DiscordUserVerificationResource.java +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/resource/DiscordUserVerificationResource.java @@ -2,53 +2,84 @@ import io.micrometer.core.annotation.Timed; import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.cardano.foundation.voting.domain.DiscordCheckVerificationRequest; -import org.cardano.foundation.voting.domain.DiscordStartVerificationRequest; +import org.cardano.foundation.voting.domain.discord.DiscordCheckVerificationRequest; +import org.cardano.foundation.voting.domain.discord.DiscordStartVerificationRequest; +import org.cardano.foundation.voting.service.discord.DiscordUserVerificationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.zalando.problem.Problem; -import static org.springframework.web.bind.annotation.RequestMethod.GET; -import static org.springframework.web.bind.annotation.RequestMethod.POST; +import static org.springframework.web.bind.annotation.RequestMethod.*; +import static org.zalando.problem.Status.BAD_REQUEST; @RestController @RequestMapping("/api/discord/user-verification") @Slf4j -@RequiredArgsConstructor public class DiscordUserVerificationResource { - @RequestMapping(value = "/is-verified/{hashedDiscordId}", method = GET, produces = "application/json") - @Timed(value = "resource.discord.isVerified", histogram = true) - public ResponseEntity isDiscordUserVerified(@PathVariable("hashedDiscordId") String hashedDiscordId) { - log.info("Received isDiscordUserVerified hashedDiscordId: {}", hashedDiscordId); + @Autowired + private DiscordUserVerificationService discordUserVerificationService; + + @Value("${discord.bot.eventId.binding}") + private String discordBotEventIdBinding; - // TODO + @RequestMapping(value = "/is-verified/{discordIdHash}", method = GET, produces = "application/json") + @Timed(value = "resource.discord.isVerified", histogram = true) + public ResponseEntity isDiscordUserVerified(@PathVariable("discordIdHash") String discordIdHash) { + log.info("Received isDiscordUserVerified request discordIdHash: {}", discordIdHash); - return ResponseEntity.ok().build(); + return discordUserVerificationService.isVerifiedBasedOnDiscordIdHash(discordBotEventIdBinding, discordIdHash) + .fold(problem -> { + return ResponseEntity.status(problem.getStatus().getStatusCode()).body(problem); + }, + userVerification -> { + return ResponseEntity.ok().body(userVerification); + } + ); } - @RequestMapping(value = "/start-verification", method = POST, produces = "application/json") + @RequestMapping(value = "/start-verification", method = { POST, PUT }, produces = "application/json") @Timed(value = "resource.discord.startVerification", histogram = true) public ResponseEntity startVerification(@RequestBody @Valid DiscordStartVerificationRequest startVerificationRequest) { log.info("Received discord startVerification request: {}", startVerificationRequest); - // TODO - - return ResponseEntity.ok().build(); + return discordUserVerificationService.startVerification(discordBotEventIdBinding, startVerificationRequest) + .fold(problem -> { + return ResponseEntity.status(problem.getStatus().getStatusCode()).body(problem); + }, + userVerification -> { + return ResponseEntity.ok().body(userVerification); + } + ); } - @RequestMapping(value = "/check-verification", method = POST, produces = "application/json") + @RequestMapping(value = "/check-verification", method = { POST }, produces = "application/json") @Timed(value = "resource.discord.checkVerification", histogram = true) public ResponseEntity checkVerification(@RequestBody @Valid DiscordCheckVerificationRequest checkVerificationRequest) { log.info("Received discord checkVerification request: {}", checkVerificationRequest); - // TODO + if (!checkVerificationRequest.getEventId().equals(discordBotEventIdBinding)) { + return ResponseEntity.badRequest(). + body(Problem.builder().withTitle("EVENT_ID_AND_DISCORD_ID_BOT_MISMATCH") + .withDetail("Event id and discord id bot mismatch!") + .withStatus(BAD_REQUEST) + .build()); + } - return ResponseEntity.ok().build(); + return discordUserVerificationService.checkVerification(checkVerificationRequest) + .fold(problem -> { + return ResponseEntity.status(problem.getStatus().getStatusCode()).body(problem); + }, + userVerification -> { + return ResponseEntity.ok().body(userVerification); + } + ); } } diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/resource/ExceptionResource.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/resource/ExceptionResource.java index e7fcaedfd..80a8e2cad 100644 --- a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/resource/ExceptionResource.java +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/resource/ExceptionResource.java @@ -2,9 +2,10 @@ import org.springframework.web.bind.annotation.ControllerAdvice; import org.zalando.problem.spring.web.advice.ProblemHandling; +import org.zalando.problem.spring.web.advice.security.SecurityAdviceTrait; // needed by Zalando Problem spring web // https://www.baeldung.com/problem-spring-web @ControllerAdvice -public class ExceptionResource implements ProblemHandling { +public class ExceptionResource implements ProblemHandling, SecurityAdviceTrait { } diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/resource/SMSUserVerificationResource.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/resource/SMSUserVerificationResource.java index 9bb3d0c2a..ab9faac90 100644 --- a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/resource/SMSUserVerificationResource.java +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/resource/SMSUserVerificationResource.java @@ -4,9 +4,9 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.cardano.foundation.voting.domain.SMSCheckVerificationRequest; -import org.cardano.foundation.voting.domain.SMSStartVerificationRequest; -import org.cardano.foundation.voting.service.verify.SMSUserVerificationService; +import org.cardano.foundation.voting.domain.sms.SMSCheckVerificationRequest; +import org.cardano.foundation.voting.domain.sms.SMSStartVerificationRequest; +import org.cardano.foundation.voting.service.sms.SMSUserVerificationService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -15,6 +15,7 @@ import java.util.Objects; import static org.springframework.web.bind.annotation.RequestMethod.POST; +import static org.springframework.web.bind.annotation.RequestMethod.PUT; @RestController @RequestMapping("/api/sms/user-verification") @@ -24,7 +25,7 @@ public class SMSUserVerificationResource { private final SMSUserVerificationService smsUserVerificationService; - @RequestMapping(value = "/start-verification", method = POST, produces = "application/json") + @RequestMapping(value = "/start-verification", method = { PUT, POST }, produces = "application/json") @Timed(value = "resource.sms.startVerification", histogram = true) public ResponseEntity startVerification(@RequestBody @Valid SMSStartVerificationRequest startVerificationRequest) { log.info("Received SMS startVerification request: {}", startVerificationRequest); @@ -35,7 +36,7 @@ public ResponseEntity startVerification(@RequestBody @Valid SMSStartVerificat ); } - @RequestMapping(value = "/check-verification", method = POST, produces = "application/json") + @RequestMapping(value = "/check-verification", method = { POST }, produces = "application/json") @Timed(value = "resource.sms.checkVerification", histogram = true) public ResponseEntity checkVerification(@RequestBody @Valid SMSCheckVerificationRequest checkVerificationRequest) { log.info("Received SMS checkVerification request: {}", checkVerificationRequest); diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/resource/UserVerificationResource.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/resource/UserVerificationResource.java index db895edbf..ec76001c7 100644 --- a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/resource/UserVerificationResource.java +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/resource/UserVerificationResource.java @@ -1,17 +1,23 @@ package org.cardano.foundation.voting.resource; import io.micrometer.core.annotation.Timed; +import io.vavr.control.Either; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.cardano.foundation.voting.domain.IsVerifiedRequest; -import org.cardano.foundation.voting.service.verify.SMSUserVerificationService; +import org.cardano.foundation.voting.domain.IsVerifiedResponse; +import org.cardano.foundation.voting.service.common.UserVerificationService; +import org.cardano.foundation.voting.utils.CompletableFutures; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.zalando.problem.Problem; -import java.util.Objects; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import static java.util.concurrent.TimeUnit.SECONDS; import static org.springframework.web.bind.annotation.RequestMethod.GET; @RestController @@ -20,17 +26,18 @@ @RequiredArgsConstructor public class UserVerificationResource { - private final SMSUserVerificationService smsUserVerificationService; + private final UserVerificationService userVerificationService; @RequestMapping(value = "/verified/{eventId}/{stakeAddress}", method = GET, produces = "application/json") @Timed(value = "resource.isVerified", histogram = true) - public ResponseEntity isVerified(@PathVariable("eventId") String eventId, @PathVariable("stakeAddress") String stakeAddress) { - var isVerifiedRequest = new IsVerifiedRequest(stakeAddress, eventId); - - log.info("Received isVerified request: {}", isVerifiedRequest); - - return smsUserVerificationService.isVerified(isVerifiedRequest) - .fold(problem -> ResponseEntity.status(Objects.requireNonNull(problem.getStatus()).getStatusCode()).body(problem), + public ResponseEntity isVerified(@PathVariable("eventId") String eventId, + @PathVariable("stakeAddress") String stakeAddress) { + var isVerifiedRequest = new IsVerifiedRequest(eventId, stakeAddress); + + return userVerificationService.isVerified(isVerifiedRequest) + .fold(problem -> { + return ResponseEntity.status(problem.getStatus().getStatusCode()).body(problem); + }, isVerifiedResponse -> { return ResponseEntity.ok().body(isVerifiedResponse); } diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/common/DefaultUserVerificationService.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/common/DefaultUserVerificationService.java new file mode 100644 index 000000000..addce8922 --- /dev/null +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/common/DefaultUserVerificationService.java @@ -0,0 +1,64 @@ +package org.cardano.foundation.voting.service.common; + +import io.vavr.control.Either; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.cardano.foundation.voting.domain.IsVerifiedRequest; +import org.cardano.foundation.voting.domain.IsVerifiedResponse; +import org.cardano.foundation.voting.service.discord.DiscordUserVerificationService; +import org.cardano.foundation.voting.service.sms.SMSUserVerificationService; +import org.cardano.foundation.voting.utils.CompletableFutures; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.zalando.problem.Problem; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import static java.util.concurrent.TimeUnit.SECONDS; + +@Service +@Slf4j +@RequiredArgsConstructor +public class DefaultUserVerificationService implements UserVerificationService { + + private final SMSUserVerificationService smsUserVerificationService; + + private final DiscordUserVerificationService discordUserVerificationService; + + @Override + @Transactional(readOnly = true) + public Either isVerified(IsVerifiedRequest isVerifiedRequest) { + log.info("Received isVerified request: {}", isVerifiedRequest); + + CompletableFuture> smsVerificationFuture = CompletableFuture.supplyAsync(() -> { + return smsUserVerificationService.isVerified(isVerifiedRequest); + }); + + CompletableFuture> discordVerificationFuture = CompletableFuture.supplyAsync(() -> { + return discordUserVerificationService.isVerifiedBasedOnStakeAddress(isVerifiedRequest); + }); + + var allFutures = CompletableFutures.anyResultsOf(List.of(smsVerificationFuture, discordVerificationFuture)); + + List> allResponses = allFutures.orTimeout(30, SECONDS) + .join(); + + var successCount = allResponses.stream().filter(Either::isRight).count(); + + if (successCount != 2) { + var problem = allResponses.stream().filter(Either::isLeft).findFirst().orElseThrow().getLeft(); + + return Either.left(problem); + } + + var successes = allResponses.stream().filter(Either::isRight).toList().stream().map(Either::get).toList(); + + var isVerified = successes.stream().reduce((a, b) -> { + return new IsVerifiedResponse(a.isVerified() || b.isVerified()); + }).orElse(new IsVerifiedResponse(false)); + + return Either.right(isVerified); + } + +} diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/common/UserVerificationService.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/common/UserVerificationService.java new file mode 100644 index 000000000..faf6ec481 --- /dev/null +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/common/UserVerificationService.java @@ -0,0 +1,12 @@ +package org.cardano.foundation.voting.service.common; + +import io.vavr.control.Either; +import org.cardano.foundation.voting.domain.IsVerifiedRequest; +import org.cardano.foundation.voting.domain.IsVerifiedResponse; +import org.zalando.problem.Problem; + +public interface UserVerificationService { + + Either isVerified(IsVerifiedRequest isVerifiedRequest); + +} diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/discord/DefaultDiscordUserVerificationService.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/discord/DefaultDiscordUserVerificationService.java new file mode 100644 index 000000000..a61a93c1b --- /dev/null +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/discord/DefaultDiscordUserVerificationService.java @@ -0,0 +1,321 @@ +package org.cardano.foundation.voting.service.discord; + +import io.vavr.control.Either; +import lombok.extern.slf4j.Slf4j; +import org.cardano.foundation.voting.client.ChainFollowerClient; +import org.cardano.foundation.voting.domain.CardanoNetwork; +import org.cardano.foundation.voting.domain.IsVerifiedRequest; +import org.cardano.foundation.voting.domain.IsVerifiedResponse; +import org.cardano.foundation.voting.domain.discord.DiscordCheckVerificationRequest; +import org.cardano.foundation.voting.domain.discord.DiscordStartVerificationRequest; +import org.cardano.foundation.voting.domain.discord.DiscordStartVerificationResponse; +import org.cardano.foundation.voting.domain.entity.DiscordUserVerification; +import org.cardano.foundation.voting.repository.DiscordUserVerificationRepository; +import org.cardano.foundation.voting.utils.StakeAddress; +import org.cardanofoundation.cip30.AddressFormat; +import org.cardanofoundation.cip30.CIP30Verifier; +import org.cardanofoundation.cip30.MessageFormat; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.zalando.problem.Problem; + +import java.time.Clock; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.cardano.foundation.voting.domain.VerificationStatus.PENDING; +import static org.cardano.foundation.voting.domain.VerificationStatus.VERIFIED; +import static org.zalando.problem.Status.BAD_REQUEST; + +@Service +@Slf4j +public class DefaultDiscordUserVerificationService implements DiscordUserVerificationService { + + @Autowired + private ChainFollowerClient chainFollowerClient; + + @Autowired + private DiscordUserVerificationRepository userVerificationRepository; + + @Autowired + private Clock clock; + + @Value("${validation.expiration.time.minutes}") + private int validationExpirationTimeMinutes; + + @Autowired + private CardanoNetwork network; + + @Override + @Transactional + public Either startVerification(String eventId, DiscordStartVerificationRequest startVerificationRequest) { + var discordIdHash = startVerificationRequest.getDiscordIdHash(); + + var maybeCompletedVerificationBasedOnDiscordUserHash = userVerificationRepository + .findCompletedVerificationBasedOnDiscordUserHash(eventId, discordIdHash); + + if (maybeCompletedVerificationBasedOnDiscordUserHash.isPresent()) { + return Either.left(Problem.builder() + .withTitle("USER_ALREADY_VERIFIED") + .withDetail("User already verified.") + .withStatus(BAD_REQUEST) + .build()); + } + + var eventDetails = chainFollowerClient.findEventById(eventId); + + if (eventDetails.isEmpty()) { + log.error("event error:{}", eventDetails.getLeft()); + + return Either.left(eventDetails.getLeft()); + } + + var maybeEvent = eventDetails.get(); + if (maybeEvent.isEmpty()) { + log.warn("Event not found:{}", eventId); + + return Either.left(Problem.builder() + .withTitle("EVENT_NOT_FOUND") + .withDetail("Event not found, eventId:" + eventId) + .withStatus(BAD_REQUEST) + .build()); + } + + var event = maybeEvent.orElseThrow(); + var createdAt = LocalDateTime.now(clock); + var expiresAt = createdAt.plusMinutes(validationExpirationTimeMinutes); + + var discordUserVerification = DiscordUserVerification.builder() + .discordIdHash(discordIdHash) + .eventId(eventId) + .secretCode(startVerificationRequest.getSecret()) + .createdAt(createdAt) + .expiresAt(expiresAt) + .status(PENDING) + .build(); + + if (event.finished()) { + log.warn("Event already finished:{}", eventId); + + return Either.left(Problem.builder() + .withTitle("EVENT_ALREADY_FINISHED") + .withDetail("Event already finished, eventId:" + eventId) + .withStatus(BAD_REQUEST) + .build()); + } + + var saved = userVerificationRepository.saveAndFlush(discordUserVerification); + + return Either.right(DiscordStartVerificationResponse.builder() + .eventId(eventId) + .discordIdHash(saved.getDiscordIdHash()) + .status(saved.getStatus()) + .build() + ); + } + + @Override + @Transactional + public Either checkVerification(DiscordCheckVerificationRequest checkVerificationRequest) { + var eventId = checkVerificationRequest.getEventId(); + var eventDetails = chainFollowerClient.findEventById(eventId); + + if (eventDetails.isEmpty()) { + log.error("event error:{}", eventDetails.getLeft()); + + return Either.left(eventDetails.getLeft()); + } + + var maybeEvent = eventDetails.get(); + if (maybeEvent.isEmpty()) { + log.warn("Event not found:{}", eventId); + + return Either.left(Problem.builder() + .withTitle("EVENT_NOT_FOUND") + .withDetail("Event not found, eventId:" + eventId) + .withStatus(BAD_REQUEST) + .build()); + } + + var event = maybeEvent.orElseThrow(); + + if (event.finished()) { + log.warn("Event already finished:{}", eventId); + + return Either.left(Problem.builder() + .withTitle("EVENT_ALREADY_FINISHED") + .withDetail("Event already finished, eventId:" + eventId) + .withStatus(BAD_REQUEST) + .build()); + } + + var requestStakeAddress = checkVerificationRequest.getStakeAddress(); + var requestSecret = checkVerificationRequest.getSecret(); + + var coseSignature = checkVerificationRequest.getCoseSignature(); + var cosePublicKey = checkVerificationRequest.getCosePublicKey(); + + var cip30Verifier = new CIP30Verifier(coseSignature, cosePublicKey); + var cip30VerificationResult = cip30Verifier.verify(); + + if (!cip30VerificationResult.isValid()) { + return Either.left(Problem.builder() + .withTitle("INVALID_CIP-30-SIGNATURE") + .withDetail("Invalid CIP-30 signature") + .withStatus(BAD_REQUEST) + .build() + ); + } + + var msg = cip30VerificationResult.getMessage(MessageFormat.TEXT); + var items = msg.split("\\|"); + + if (items.length != 2) { + return Either.left(Problem.builder() + .withTitle("INVALID_CIP-30-SIGNATURE") + .withDetail("Invalid CIP-30 signature, invalid signed message.") + .withStatus(BAD_REQUEST) + .build() + ); + } + var discordIdHash = items[0]; + var cip30Secret = items[1]; + + if (!requestSecret.equals(cip30Secret)) { + return Either.left(Problem.builder() + .withTitle("SECRET_MISMATCH") + .withDetail("Request Secret and CIP-30 secret mismatch.") + .withStatus(BAD_REQUEST) + .build() + ); + } + + var maybeAddress = cip30VerificationResult.getAddress(AddressFormat.TEXT); + + if (maybeAddress.isEmpty()) { + return Either.left(Problem.builder() + .withTitle("INVALID_CIP-30-SIGNATURE") + .withDetail("Invalid CIP-30 signature, must have asdress in CIP-30 signature.") + .withStatus(BAD_REQUEST) + .build() + ); + } + + var address = maybeAddress.orElseThrow(); + + if (!requestStakeAddress.equals(address)) { + return Either.left(Problem.builder() + .withTitle("ADDRESS_MISMATCH") + .withDetail(String.format("Address mismatch, requestStakeAddress: %s, address: %s", requestStakeAddress, address)) + .withStatus(BAD_REQUEST) + .build() + ); + } + + var stakeAddressCheckE = StakeAddress.checkStakeAddress(network, requestStakeAddress); + + if (stakeAddressCheckE.isEmpty()) { + return Either.left(stakeAddressCheckE.getLeft()); + } + + var maybeCompletedVerificationBasedOnDiscordUserHash = userVerificationRepository + .findCompletedVerificationBasedOnDiscordUserHash(eventId, discordIdHash); + + if (maybeCompletedVerificationBasedOnDiscordUserHash.isPresent()) { + return Either.left(Problem.builder() + .withTitle("USER_ALREADY_VERIFIED") + .withDetail("User already verified.") + .withStatus(BAD_REQUEST) + .with("discordIdHash", discordIdHash) + .build() + ); + } + + var maybePendingVerification = userVerificationRepository.findPendingVerificationBasedOnDiscordUserHash(eventId, discordIdHash); + + if (maybePendingVerification.isEmpty()) { + return Either.left(Problem.builder() + .withTitle("NO_PENDING_VERIFICATION") + .withDetail("No pending verification found for discordIdHash:" + discordIdHash) + .withStatus(BAD_REQUEST) + .with("discordIdHash", discordIdHash) + .build() + ); + } + + var pendingVerification = maybePendingVerification.get(); + boolean isSecretCodeMatch = pendingVerification.getSecretCode().equals(cip30Secret) + && pendingVerification.getSecretCode().equals(requestSecret); + + if (!isSecretCodeMatch) { + return Either.left(Problem.builder() + .withTitle("AUTH_FAILED") + .withDetail("Invalid secret and / or discordIdHash.") + .withStatus(BAD_REQUEST) + .build() + ); + } + + var pendingUserVerification = maybePendingVerification.orElseThrow(); + + var now = LocalDateTime.now(clock); + + var isCodeExpired = now.isAfter(pendingUserVerification.getExpiresAt()); + if (isCodeExpired) { + return Either.left(Problem.builder() + .withTitle("VERIFICATION_EXPIRED") + .withDetail(String.format("Secret code: %s expired for requestStakeAddress: %s and discordHashId:%s", cip30Secret, requestStakeAddress, discordIdHash)) + .withStatus(BAD_REQUEST) + .with("discordIdHash", discordIdHash) + .with("requestStakeAddress", requestStakeAddress) + .build()); + } + + pendingUserVerification.setStakeAddress(Optional.of(requestStakeAddress)); + pendingUserVerification.setUpdatedAt(now); + pendingUserVerification.setStatus(VERIFIED); + + return Either.right(new IsVerifiedResponse(true)); + } + + @Override + @Transactional(readOnly = true) + public Either isVerifiedBasedOnStakeAddress(IsVerifiedRequest isVerifiedRequest) { + var isVerified = userVerificationRepository.findCompletedVerifications(isVerifiedRequest.getEventId(), isVerifiedRequest.getStakeAddress()) + .stream().findFirst() + .map(uv -> new IsVerifiedResponse(true)).orElse(new IsVerifiedResponse(false)); + + return Either.right(isVerified); + } + + @Override + @Transactional(readOnly = true) + public Either isVerifiedBasedOnDiscordIdHash(String eventId, String discordIdHash) { + var isVerified = userVerificationRepository.findCompletedVerificationBasedOnDiscordUserHash(eventId, discordIdHash) + .map(uv -> new IsVerifiedResponse(true)).orElse(new IsVerifiedResponse(false)); + + return Either.right(isVerified); + } + + @Override + @Transactional + public void removeUserVerification(DiscordUserVerification userVerification) { + userVerificationRepository.delete(userVerification); + } + + @Override + @Transactional(readOnly = true) + public List findAllForEvent(String eventId) { + return userVerificationRepository.findAllForEvent(eventId); + } + + @Override + @Transactional(readOnly = true) + public List findAllPending(String eventId) { + return userVerificationRepository.findAllPending(eventId); + } + +} diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/discord/DiscordUserVerificationService.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/discord/DiscordUserVerificationService.java new file mode 100644 index 000000000..45645f0d0 --- /dev/null +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/discord/DiscordUserVerificationService.java @@ -0,0 +1,30 @@ +package org.cardano.foundation.voting.service.discord; + +import io.vavr.control.Either; +import org.cardano.foundation.voting.domain.IsVerifiedRequest; +import org.cardano.foundation.voting.domain.IsVerifiedResponse; +import org.cardano.foundation.voting.domain.discord.DiscordCheckVerificationRequest; +import org.cardano.foundation.voting.domain.discord.DiscordStartVerificationRequest; +import org.cardano.foundation.voting.domain.discord.DiscordStartVerificationResponse; +import org.cardano.foundation.voting.domain.entity.DiscordUserVerification; +import org.zalando.problem.Problem; + +import java.util.List; + +public interface DiscordUserVerificationService { + + Either startVerification(String eventId, DiscordStartVerificationRequest startVerificationRequest); + + Either checkVerification(DiscordCheckVerificationRequest checkVerificationRequest); + + Either isVerifiedBasedOnStakeAddress(IsVerifiedRequest isVerifiedRequest); + + Either isVerifiedBasedOnDiscordIdHash(String eventId, String discordIdHash); + + void removeUserVerification(DiscordUserVerification userVerification); + + List findAllForEvent(String eventId); + + List findAllPending(String eventId); + +} diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/pass/CodeGenService.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/pass/CodeGenService.java index 3dc886f8f..03dbcbf5f 100644 --- a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/pass/CodeGenService.java +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/pass/CodeGenService.java @@ -12,7 +12,7 @@ public class CodeGenService { private final static SecureRandom SECURE_RANDOM = new SecureRandom(); public String generateRandomCode() { - var randomVerificationCode = SECURE_RANDOM.nextInt(100000, 999999); + var randomVerificationCode = SECURE_RANDOM.nextInt(100_000, 999_999); return String.valueOf(randomVerificationCode); } diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/sms/AWSSNSService.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/sms/AWSSNSService.java index cc907e681..9bf1e7d97 100644 --- a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/sms/AWSSNSService.java +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/sms/AWSSNSService.java @@ -12,9 +12,10 @@ public class AWSSNSService implements SMSService { private final AWSSNSClient awssnsClient; @Override - public Either publishTextMessage(String message, Phonenumber.PhoneNumber phoneNumber) { + public Either publishTextMessage(String message, + Phonenumber.PhoneNumber phoneNumber) { return awssnsClient.publishTextMessage(message, phoneNumber) - .map(r -> new SMSVerificationResponse(r.messageId())); + .map(publishResponse -> new SMSVerificationResponse(publishResponse.messageId())); } } diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/verify/DefaultSMSSMSUserVerificationService.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/sms/DefaultSMSSMSUserVerificationService.java similarity index 83% rename from backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/verify/DefaultSMSSMSUserVerificationService.java rename to backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/sms/DefaultSMSSMSUserVerificationService.java index b5f9266aa..5e072cb64 100644 --- a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/verify/DefaultSMSSMSUserVerificationService.java +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/sms/DefaultSMSSMSUserVerificationService.java @@ -1,4 +1,4 @@ -package org.cardano.foundation.voting.service.verify; +package org.cardano.foundation.voting.service.sms; import com.google.i18n.phonenumbers.NumberParseException; import com.google.i18n.phonenumbers.PhoneNumberUtil; @@ -6,11 +6,16 @@ import io.vavr.control.Either; import lombok.extern.slf4j.Slf4j; import org.cardano.foundation.voting.client.ChainFollowerClient; -import org.cardano.foundation.voting.domain.*; -import org.cardano.foundation.voting.domain.entity.UserVerification; -import org.cardano.foundation.voting.repository.UserVerificationRepository; +import org.cardano.foundation.voting.domain.CardanoNetwork; +import org.cardano.foundation.voting.domain.IsVerifiedRequest; +import org.cardano.foundation.voting.domain.IsVerifiedResponse; +import org.cardano.foundation.voting.domain.entity.SMSUserVerification; +import org.cardano.foundation.voting.domain.sms.SMSCheckVerificationRequest; +import org.cardano.foundation.voting.domain.sms.SMSStartVerificationRequest; +import org.cardano.foundation.voting.domain.sms.SMSStartVerificationResponse; +import org.cardano.foundation.voting.domain.SaltHolder; +import org.cardano.foundation.voting.repository.SMSUserVerificationRepository; import org.cardano.foundation.voting.service.pass.CodeGenService; -import org.cardano.foundation.voting.service.sms.SMSService; import org.cardano.foundation.voting.utils.StakeAddress; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -28,10 +33,8 @@ import static com.bloxbean.cardano.client.crypto.Blake2bUtil.blake2bHash256; import static com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.cardano.foundation.voting.domain.entity.UserVerification.Channel.SMS; -import static org.cardano.foundation.voting.domain.entity.UserVerification.Provider.AWS_SNS; -import static org.cardano.foundation.voting.domain.entity.UserVerification.Status.PENDING; -import static org.cardano.foundation.voting.domain.entity.UserVerification.Status.VERIFIED; +import static org.cardano.foundation.voting.domain.VerificationStatus.PENDING; +import static org.cardano.foundation.voting.domain.VerificationStatus.VERIFIED; import static org.zalando.problem.Status.BAD_REQUEST; @Service @@ -44,9 +47,8 @@ public class DefaultSMSSMSUserVerificationService implements SMSUserVerification @Autowired private SMSService smsService; - @Autowired - private UserVerificationRepository userVerificationRepository; + private SMSUserVerificationRepository smsUserVerificationRepository; @Autowired private SaltHolder saltHolder; @@ -80,17 +82,17 @@ public Either startVerification(SMSStartV return Either.left(stakeAddressCheckE.getLeft()); } - var activeEventE = chainFollowerClient.findEventById(eventId); + var eventDetails = chainFollowerClient.findEventById(eventId); - if (activeEventE.isEmpty()) { - log.error("Active event error:{}", activeEventE.getLeft()); + if (eventDetails.isEmpty()) { + log.error("event error:{}", eventDetails.getLeft()); - return Either.left(activeEventE.getLeft()); + return Either.left(eventDetails.getLeft()); } - var maybeEvent = activeEventE.get(); + var maybeEvent = eventDetails.get(); if (maybeEvent.isEmpty()) { - log.warn("Active event not found:{}", eventId); + log.warn("Event not found:{}", eventId); return Either.left(Problem.builder() .withTitle("EVENT_NOT_FOUND") @@ -111,7 +113,7 @@ public Either startVerification(SMSStartV .build()); } - var maybeUserVerificationStakeAddress = userVerificationRepository.findAllCompletedPerStake( + var maybeUserVerificationStakeAddress = smsUserVerificationRepository.findAllCompletedPerStake( eventId, stakeAddress ).stream().findFirst(); @@ -145,7 +147,7 @@ public Either startVerification(SMSStartV var formattedPhoneStr = PhoneNumberUtil.getInstance().format(phoneNum, INTERNATIONAL); var phoneHash = saltedPhoneHash(formattedPhoneStr); - var maybeUserVerificationPhoneHash = userVerificationRepository.findAllCompletedPerPhone( + var maybeUserVerificationPhoneHash = smsUserVerificationRepository.findAllCompletedPerPhone( eventId, phoneHash ).stream().findFirst(); @@ -166,7 +168,7 @@ public Either startVerification(SMSStartV } } - int pendingPerStakeAddressCount = userVerificationRepository.findPendingPerStakeAddressPerPhoneCount(eventId, stakeAddress, phoneHash); + int pendingPerStakeAddressCount = smsUserVerificationRepository.findPendingPerStakeAddressPerPhoneCount(eventId, stakeAddress, phoneHash); if (pendingPerStakeAddressCount >= maxPendingVerificationAttempts) { return Either.left(Problem.builder() .withTitle("MAX_VERIFICATION_ATTEMPTS_REACHED") @@ -176,7 +178,8 @@ public Either startVerification(SMSStartV } var randomVerificationCode = codeGenService.generateRandomCode(); - var textMsg = String.format("%s. %s", randomVerificationCode, friendlyCustomName).trim(); + // TODO transaction service based on phone number prefix? + var textMsg = String.format("Auth Code: %s. %s", randomVerificationCode, friendlyCustomName).trim(); var smsVerificationResponseE = smsService.publishTextMessage(textMsg, phoneNum); @@ -190,11 +193,9 @@ public Either startVerification(SMSStartV var now = LocalDateTime.now(clock); - var newUserVerification = UserVerification.builder() + var newUserVerification = SMSUserVerification.builder() .id(UUID.randomUUID().toString()) .eventId(eventId) - .channel(SMS) - .provider(AWS_SNS) .status(PENDING) .phoneNumberHash(phoneHash) .stakeAddress(stakeAddress) @@ -203,7 +204,7 @@ public Either startVerification(SMSStartV .expiresAt(now.plusMinutes(validationExpirationTimeMinutes)) .build(); - var saved = userVerificationRepository.saveAndFlush(newUserVerification); + var saved = smsUserVerificationRepository.saveAndFlush(newUserVerification); var startVerificationResponse = new SMSStartVerificationResponse( saved.getEventId(), @@ -217,7 +218,7 @@ public Either startVerification(SMSStartV } @Override - @Transactional(readOnly = true) + @Transactional public Either checkVerification(SMSCheckVerificationRequest checkVerificationRequest) { String eventId = checkVerificationRequest.getEventId(); String stakeAddress = checkVerificationRequest.getStakeAddress(); @@ -237,7 +238,7 @@ public Either checkVerification(SMSCheckVerificatio var maybeEvent = activeEventE.get(); if (maybeEvent.isEmpty()) { - log.error("Active event not found:{}", eventId); + log.error("Event not found:{}", eventId); return Either.left(Problem.builder() .withTitle("EVENT_NOT_FOUND") @@ -258,7 +259,7 @@ public Either checkVerification(SMSCheckVerificatio .build()); } - var maybeUserVerification = userVerificationRepository.findAllCompletedPerStake( + var maybeUserVerification = smsUserVerificationRepository.findAllCompletedPerStake( eventId, stakeAddress ).stream().findFirst(); @@ -277,7 +278,7 @@ public Either checkVerification(SMSCheckVerificatio } } - var maybePendingRequest = userVerificationRepository.findPendingVerificationsByEventIdAndStakeAddressAndRequestId( + var maybePendingRequest = smsUserVerificationRepository.findPendingVerificationsByEventIdAndStakeAddressAndRequestId( eventId, stakeAddress, checkVerificationRequest.getRequestId() @@ -326,7 +327,7 @@ public Either checkVerification(SMSCheckVerificatio pendingUserVerification.setStatus(VERIFIED); pendingUserVerification.setUpdatedAt(now); - var saved = userVerificationRepository.saveAndFlush(pendingUserVerification); + var saved = smsUserVerificationRepository.saveAndFlush(pendingUserVerification); return Either.right(new IsVerifiedResponse(saved.getStatus() == VERIFIED)); } @@ -344,7 +345,7 @@ public Either isVerified(IsVerifiedRequest isVerifi var maybeEvent = activeEventE.get(); if (maybeEvent.isEmpty()) { - log.error("Active event not found:{}", isVerifiedRequest.getEventId()); + log.error("Event not found:{}", isVerifiedRequest.getEventId()); return Either.left(Problem.builder() .withTitle("EVENT_NOT_FOUND") @@ -365,7 +366,7 @@ public Either isVerified(IsVerifiedRequest isVerifi .build()); } - var maybeUserVerification = userVerificationRepository.findAllCompletedPerStake( + var maybeUserVerification = smsUserVerificationRepository.findAllCompletedPerStake( isVerifiedRequest.getEventId(), isVerifiedRequest.getStakeAddress() ).stream().findFirst(); @@ -386,20 +387,20 @@ public Either isVerified(IsVerifiedRequest isVerifi @Override @Transactional - public void removeUserVerification(UserVerification userVerification) { - userVerificationRepository.delete(userVerification); + public void removeUserVerification(SMSUserVerification SMSUserVerification) { + smsUserVerificationRepository.delete(SMSUserVerification); } @Override @Transactional(readOnly = true) - public List findAllForEvent(String eventId) { - return userVerificationRepository.findAllByEventId(eventId); + public List findAllForEvent(String eventId) { + return smsUserVerificationRepository.findAllByEventId(eventId); } @Override @Transactional(readOnly = true) - public List findAllPending(String eventId) { - return userVerificationRepository.findAllPending(eventId); + public List findAllPending(String eventId) { + return smsUserVerificationRepository.findAllPending(eventId); } private static Optional isValidNumber(String userEnteredPhoneNumber) { diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/sms/SMSService.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/sms/SMSService.java index 5b3b8a83e..5d03069f2 100644 --- a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/sms/SMSService.java +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/sms/SMSService.java @@ -6,6 +6,7 @@ public interface SMSService { - Either publishTextMessage(String message, Phonenumber.PhoneNumber phoneNumber); + Either publishTextMessage(String message, + Phonenumber.PhoneNumber phoneNumber); } diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/sms/SMSUserVerificationService.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/sms/SMSUserVerificationService.java new file mode 100644 index 000000000..763adb271 --- /dev/null +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/sms/SMSUserVerificationService.java @@ -0,0 +1,27 @@ +package org.cardano.foundation.voting.service.sms; + +import io.vavr.control.Either; +import org.cardano.foundation.voting.domain.*; +import org.cardano.foundation.voting.domain.entity.SMSUserVerification; +import org.cardano.foundation.voting.domain.sms.SMSCheckVerificationRequest; +import org.cardano.foundation.voting.domain.sms.SMSStartVerificationRequest; +import org.cardano.foundation.voting.domain.sms.SMSStartVerificationResponse; +import org.zalando.problem.Problem; + +import java.util.List; + +public interface SMSUserVerificationService { + + Either startVerification(SMSStartVerificationRequest SMSStartVerificationRequest); + + Either checkVerification(SMSCheckVerificationRequest checkVerificationRequest); + + Either isVerified(IsVerifiedRequest isVerifiedRequest); + + void removeUserVerification(SMSUserVerification SMSUserVerification); + + List findAllForEvent(String eventId); + + List findAllPending(String eventId); + +} diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/verify/SMSUserVerificationService.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/verify/SMSUserVerificationService.java deleted file mode 100644 index 4eb6b364a..000000000 --- a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/service/verify/SMSUserVerificationService.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.cardano.foundation.voting.service.verify; - -import io.vavr.control.Either; -import org.cardano.foundation.voting.domain.*; -import org.cardano.foundation.voting.domain.entity.UserVerification; -import org.zalando.problem.Problem; - -import java.util.List; - -public interface SMSUserVerificationService { - - Either startVerification(SMSStartVerificationRequest SMSStartVerificationRequest); - - Either checkVerification(SMSCheckVerificationRequest checkVerificationRequest); - - Either isVerified(IsVerifiedRequest isVerifiedRequest); - - void removeUserVerification(UserVerification userVerification); - - List findAllForEvent(String eventId); - - List findAllPending(String eventId); - -} diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/utils/CompletableFutures.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/utils/CompletableFutures.java new file mode 100644 index 000000000..27d2bfbf9 --- /dev/null +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/utils/CompletableFutures.java @@ -0,0 +1,20 @@ +package org.cardano.foundation.voting.utils; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public final class CompletableFutures { + + public static CompletableFuture> anyResultsOf(List> completableFutures) { + var allFutures = CompletableFuture + .anyOf(completableFutures.toArray(new CompletableFuture[completableFutures.size()])); + + return allFutures.thenApply( + future -> { + return completableFutures.stream() + .map(CompletableFuture::join) + .toList(); + }); + } + +} \ No newline at end of file diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/utils/MoreAddress.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/utils/MoreAddress.java deleted file mode 100644 index 3efb1e009..000000000 --- a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/utils/MoreAddress.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.cardano.foundation.voting.utils; - -import com.bloxbean.cardano.client.address.Address; -import com.bloxbean.cardano.client.common.model.Networks; - -public final class MoreAddress { - - public static boolean isMainnet(String stakeAddress) { - var addr = new Address(stakeAddress); - - return addr.getNetwork().equals(Networks.mainnet()); - } - - public static boolean isTestnet(String stakeAddress) { - return !isMainnet(stakeAddress); - } - -} diff --git a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/utils/StakeAddress.java b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/utils/StakeAddress.java index 6c37d5fa2..a3303e6c0 100644 --- a/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/utils/StakeAddress.java +++ b/backend-services/user-verification-service/src/main/java/org/cardano/foundation/voting/utils/StakeAddress.java @@ -1,18 +1,27 @@ package org.cardano.foundation.voting.utils; import com.bloxbean.cardano.client.address.Address; +import com.bloxbean.cardano.client.common.model.Networks; import io.vavr.control.Either; import lombok.extern.slf4j.Slf4j; import org.cardano.foundation.voting.domain.CardanoNetwork; import org.zalando.problem.Problem; -import static org.cardano.foundation.voting.utils.MoreAddress.isMainnet; -import static org.cardano.foundation.voting.utils.MoreAddress.isTestnet; import static org.zalando.problem.Status.BAD_REQUEST; @Slf4j public final class StakeAddress { + public static boolean isMainnet(String stakeAddress) { + var addr = new Address(stakeAddress); + + return addr.getNetwork().equals(Networks.mainnet()); + } + + public static boolean isTestnet(String stakeAddress) { + return !isMainnet(stakeAddress); + } + public static Either checkStakeAddress(CardanoNetwork network, String stakeAddress) { return checkIfAddressIsStakeAddress(network, stakeAddress) .flatMap(isStakeAddress -> { diff --git a/backend-services/user-verification-service/src/main/resources/application.properties b/backend-services/user-verification-service/src/main/resources/application.properties index fd9521971..1c49ce840 100644 --- a/backend-services/user-verification-service/src/main/resources/application.properties +++ b/backend-services/user-verification-service/src/main/resources/application.properties @@ -58,12 +58,19 @@ aws.sns.region=${AWS_SNS_REGION:eu-west-1} friendly.custom.name=${FRIENDLY_CUSTOM_NAME:Cardano Summit 2023} validation.expiration.time.minutes=${EXPIRATION_TIME_MINUTES:15} -pending.verification.phone.expiration.time.hours=${PENDING_VERIFICATION_PHONE_EXPIRATION_TIME_HOURS:24} +pending.verification.expiration.time.hours=${PENDING_VERIFICATION_EXPIRATION_TIME_HOURS:24} max.pending.verification.attempts=${MAX_PENDING_VERIFICATION_ATTEMPTS:5} # disable in production... spring.h2.console.enabled=${H2_CONSOLE_ENABLED:true} phone.number.salt=${SALT:67274569c9671a4ae3f753b9647ca719} +discord.bot.eventId.binding=${DISCORD_BOT_EVENT_ID_BINDING:CF_SUMMIT_2023_3A33} + +discord.bot.username=${DISCORD_BOT_USERNAME:discord_bot} +discord.bot.password=${DISCORD_BOT_PASSWORD:test} + +spring.task.scheduling.pool.size=${SCHEDULING_POOL_SIZE:5} + +spring.jackson.default-property-inclusion=non_null -spring.jackson.default-property-inclusion=non_null \ No newline at end of file diff --git a/backend-services/user-verification-service/src/main/resources/db/migration/h2/V0__user_verification_service_init.sql b/backend-services/user-verification-service/src/main/resources/db/migration/h2/V0__user_verification_service_init.sql index d31e3fde1..28b1bcd6b 100644 --- a/backend-services/user-verification-service/src/main/resources/db/migration/h2/V0__user_verification_service_init.sql +++ b/backend-services/user-verification-service/src/main/resources/db/migration/h2/V0__user_verification_service_init.sql @@ -1,6 +1,6 @@ -DROP TABLE IF EXISTS user_verification; +DROP TABLE IF EXISTS sms_user_verification; -CREATE TABLE user_verification ( +CREATE TABLE sms_user_verification ( id VARCHAR(255) NOT NULL, stake_address VARCHAR(255) NOT NULL, event_id VARCHAR(255) NOT NULL, @@ -10,25 +10,50 @@ CREATE TABLE user_verification ( verification_code VARCHAR(255) NOT NULL, status VARCHAR(255) NOT NULL, - provider VARCHAR(255) NOT NULL, - channel VARCHAR(255) NOT NULL, + + expires_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, - expires_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, - - CONSTRAINT pk_user PRIMARY KEY (id) + CONSTRAINT pk_sms_user PRIMARY KEY (id) ); -create index idx_event on user_verification(event_id); +create index idx_sms_event on sms_user_verification(event_id); + +create index idx_sms_status on sms_user_verification(event_id, status); + +create index idx_sms_stake_address_status on sms_user_verification(event_id, stake_address, status); + +create index idx_sms_status_phone_hash on sms_user_verification(event_id, status, phone_number_hash); + +create index idx_sms_stake_address_status_phone_hash on sms_user_verification(event_id, stake_address, status, phone_number_hash); + +create index idx_sms_stake_address_status_req_id on sms_user_verification(event_id, stake_address, status, request_id); + +CREATE TABLE discord_user_verification ( + id VARCHAR(255) NOT NULL, -- discord id hash + + event_id VARCHAR(255) NOT NULL, + + stake_address VARCHAR(255), -- nullable since it is set on check-verification request + + secret_code VARCHAR(255) NOT NULL, -create index idx_status on user_verification(event_id, status); + status VARCHAR(255) NOT NULL, + + expires_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + + CONSTRAINT pk_discord_id_hash PRIMARY KEY (id) +); -create index idx_stake_address_status on user_verification(event_id, stake_address, status); +create index idx_discord_stake_event_id on discord_user_verification(event_id); -create index idx_status_phone_hash on user_verification(event_id, status, phone_number_hash); +create index idx_discord_event_id_status on discord_user_verification(event_id, status); -create index idx_stake_address_status_phone_hash on user_verification(event_id, stake_address, status, phone_number_hash); +create index idx_discord_stake_address_status on discord_user_verification(event_id, stake_address, status); -create index idx_stake_address_status_req_id on user_verification(event_id, stake_address, status, request_id); +create index idx_discord_status_event_discord_id_hash on discord_user_verification(event_id, status, id); diff --git a/backend-services/user-verification-service/src/main/resources/db/migration/postgresql/V0__user_verification_service_init.sql b/backend-services/user-verification-service/src/main/resources/db/migration/postgresql/V0__user_verification_service_init.sql index d31e3fde1..28b1bcd6b 100644 --- a/backend-services/user-verification-service/src/main/resources/db/migration/postgresql/V0__user_verification_service_init.sql +++ b/backend-services/user-verification-service/src/main/resources/db/migration/postgresql/V0__user_verification_service_init.sql @@ -1,6 +1,6 @@ -DROP TABLE IF EXISTS user_verification; +DROP TABLE IF EXISTS sms_user_verification; -CREATE TABLE user_verification ( +CREATE TABLE sms_user_verification ( id VARCHAR(255) NOT NULL, stake_address VARCHAR(255) NOT NULL, event_id VARCHAR(255) NOT NULL, @@ -10,25 +10,50 @@ CREATE TABLE user_verification ( verification_code VARCHAR(255) NOT NULL, status VARCHAR(255) NOT NULL, - provider VARCHAR(255) NOT NULL, - channel VARCHAR(255) NOT NULL, + + expires_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, - expires_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, - - CONSTRAINT pk_user PRIMARY KEY (id) + CONSTRAINT pk_sms_user PRIMARY KEY (id) ); -create index idx_event on user_verification(event_id); +create index idx_sms_event on sms_user_verification(event_id); + +create index idx_sms_status on sms_user_verification(event_id, status); + +create index idx_sms_stake_address_status on sms_user_verification(event_id, stake_address, status); + +create index idx_sms_status_phone_hash on sms_user_verification(event_id, status, phone_number_hash); + +create index idx_sms_stake_address_status_phone_hash on sms_user_verification(event_id, stake_address, status, phone_number_hash); + +create index idx_sms_stake_address_status_req_id on sms_user_verification(event_id, stake_address, status, request_id); + +CREATE TABLE discord_user_verification ( + id VARCHAR(255) NOT NULL, -- discord id hash + + event_id VARCHAR(255) NOT NULL, + + stake_address VARCHAR(255), -- nullable since it is set on check-verification request + + secret_code VARCHAR(255) NOT NULL, -create index idx_status on user_verification(event_id, status); + status VARCHAR(255) NOT NULL, + + expires_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + + CONSTRAINT pk_discord_id_hash PRIMARY KEY (id) +); -create index idx_stake_address_status on user_verification(event_id, stake_address, status); +create index idx_discord_stake_event_id on discord_user_verification(event_id); -create index idx_status_phone_hash on user_verification(event_id, status, phone_number_hash); +create index idx_discord_event_id_status on discord_user_verification(event_id, status); -create index idx_stake_address_status_phone_hash on user_verification(event_id, stake_address, status, phone_number_hash); +create index idx_discord_stake_address_status on discord_user_verification(event_id, stake_address, status); -create index idx_stake_address_status_req_id on user_verification(event_id, stake_address, status, request_id); +create index idx_discord_status_event_discord_id_hash on discord_user_verification(event_id, status, id); diff --git a/backend-services/voting-admin-app/Dockerfile b/backend-services/voting-admin-app/Dockerfile new file mode 100644 index 000000000..b76582743 --- /dev/null +++ b/backend-services/voting-admin-app/Dockerfile @@ -0,0 +1,9 @@ +FROM openjdk:20-jdk-slim AS build +WORKDIR /app +COPY . /app +RUN ./gradlew clean build + +FROM openjdk:20-jdk-slim AS runtime +WORKDIR /app +COPY --from=build /app/build/libs/*SNAPSHOT.jar /app/app.jar +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/backend-services/voting-admin-app/README.md b/backend-services/voting-admin-app/README.md index f352bb34b..87667f2d7 100644 --- a/backend-services/voting-admin-app/README.md +++ b/backend-services/voting-admin-app/README.md @@ -2,3 +2,15 @@ # Application Description Application to be used by the organisers to create events and proposals. + +# Run in docker + +``` +docker run -it --rm \ + -e SPRING_PROFILES_ACTIVE=prod \ + -e CARDANO_NETWORK=main \ + -e BLOCKFROST_URL=https://blockfrost-api.pro.dandelion-mainnet.eu-west-1.metadata.dev.cf-deployments.org \ + -e BLOCKFROST_PASSWORD=password \ + -e TX_SUBMIT_URL=https://submit-api.pro.dandelion-mainnet.eu-west-1.metadata.dev.cf-deployments.org/api/submit/tx \ + -e ORGANISER_MNEMONIC_PHRASE="_CHANGE_ME_" \ + pro.registry.gitlab.metadata.dev.cf-deployments.org/base-infrastructure/docker-registry/voting-admin-app diff --git a/backend-services/voting-admin-app/build.gradle.kts b/backend-services/voting-admin-app/build.gradle.kts index 4bedf1917..50c635947 100644 --- a/backend-services/voting-admin-app/build.gradle.kts +++ b/backend-services/voting-admin-app/build.gradle.kts @@ -1,10 +1,13 @@ plugins { java - id("io.spring.dependency-management") version "1.1.0" + id("io.spring.dependency-management") version "1.1.3" + id("org.graalvm.buildtools.native") version "0.9.27" + id("com.github.ben-manes.versions") version "0.48.0" + id("org.springframework.boot") version "3.1.2" } group = "org.cardano.foundation" -version = "0.0.1-SNAPSHOT" +version = "1.0.0-SNAPSHOT" java.sourceCompatibility = JavaVersion.VERSION_17 configurations { @@ -15,12 +18,12 @@ configurations { repositories { mavenCentral() - mavenLocal() maven { url = uri("https://repo.spring.io/milestone") } } +extra["springShellVersion"] = "3.1.2" + dependencies { - implementation("org.springframework.boot:spring-boot-starter") implementation("org.springframework.shell:spring-shell-starter") testImplementation("org.springframework.boot:spring-boot-starter-test") @@ -30,14 +33,14 @@ dependencies { testCompileOnly("org.projectlombok:lombok:1.18.28") testAnnotationProcessor("org.projectlombok:lombok:1.18.28") - implementation("com.bloxbean.cardano:cardano-client-crypto:0.5.0-beta2") - implementation("com.bloxbean.cardano:cardano-client-address:0.5.0-beta2") - implementation("com.bloxbean.cardano:cardano-client-metadata:0.5.0-beta2") - implementation("com.bloxbean.cardano:cardano-client-quicktx:0.5.0-beta2") - implementation("com.bloxbean.cardano:cardano-client-backend-blockfrost:0.5.0-beta2") - implementation("com.bloxbean.cardano:cardano-client-cip30:0.5.0-beta2") + implementation("com.bloxbean.cardano:cardano-client-crypto:0.5.0-beta3") + implementation("com.bloxbean.cardano:cardano-client-address:0.5.0-beta3") + implementation("com.bloxbean.cardano:cardano-client-metadata:0.5.0-beta3") + implementation("com.bloxbean.cardano:cardano-client-quicktx:0.5.0-beta3") + implementation("com.bloxbean.cardano:cardano-client-backend-blockfrost:0.5.0-beta3") + implementation("com.bloxbean.cardano:cardano-client-cip30:0.5.0-beta3") - implementation("com.nimbusds:nimbus-jose-jwt:9.31") + implementation("com.nimbusds:nimbus-jose-jwt:9.34") implementation("com.google.crypto.tink:tink:1.10.0") implementation("io.blockfrost:blockfrost-java:0.1.3") @@ -45,7 +48,7 @@ dependencies { dependencyManagement { imports { - mavenBom("org.springframework.shell:spring-shell-dependencies:3.1.0") + mavenBom("org.springframework.shell:spring-shell-dependencies:${property("springShellVersion")}") } } diff --git a/backend-services/voting-admin-app/src/main/java/org/cardano/foundation/voting/config/CardanoClientLibConfig.java b/backend-services/voting-admin-app/src/main/java/org/cardano/foundation/voting/config/CardanoClientLibConfig.java index bbd03cd6a..d29945fdb 100644 --- a/backend-services/voting-admin-app/src/main/java/org/cardano/foundation/voting/config/CardanoClientLibConfig.java +++ b/backend-services/voting-admin-app/src/main/java/org/cardano/foundation/voting/config/CardanoClientLibConfig.java @@ -3,6 +3,7 @@ import com.bloxbean.cardano.client.account.Account; import com.bloxbean.cardano.client.backend.api.BackendService; import com.bloxbean.cardano.client.backend.blockfrost.service.BFBackendService; +import com.bloxbean.cardano.client.common.model.Network; import com.bloxbean.cardano.client.common.model.Networks; import lombok.extern.slf4j.Slf4j; import org.cardano.foundation.voting.domain.CardanoNetwork; @@ -21,16 +22,21 @@ public BackendService backendService(@Value("${blockfrost.url}") String blockfro return new BFBackendService(blockfrostUrl, blockfrostApiKey); } + @Bean + public Network cardanoNetwork(CardanoNetwork cardanoNetwork) { + return switch(cardanoNetwork) { + case MAIN -> Networks.mainnet(); + case PREPROD -> Networks.preprod(); + case PREVIEW -> Networks.preview(); + case DEV -> Networks.testnet(); + }; + } + @Bean @Qualifier("organiser_account") - public Account organiserAccount(CardanoNetwork cardanoNetwork, + public Account organiserAccount(Network network, @Value("${organiser.account.mnemonic}" ) String organiserMnemonic) { - var organiserAccount = switch(cardanoNetwork) { - case MAIN -> new Account(Networks.mainnet(), organiserMnemonic); - case PREPROD -> new Account(Networks.preprod(), organiserMnemonic); - case PREVIEW -> new Account(Networks.preview(), organiserMnemonic); - case DEV -> new Account(Networks.testnet(), organiserMnemonic); - }; + var organiserAccount = new Account(Networks.mainnet(), organiserMnemonic); log.info("Organiser's address:{}, stakeAddress:{}", organiserAccount.baseAddress(), organiserAccount.stakeAddress()); diff --git a/backend-services/voting-admin-app/src/main/java/org/cardano/foundation/voting/service/transaction_submit/MetadataSerialiser.java b/backend-services/voting-admin-app/src/main/java/org/cardano/foundation/voting/service/transaction_submit/MetadataSerialiser.java index 6d151b8c1..d937bfd28 100644 --- a/backend-services/voting-admin-app/src/main/java/org/cardano/foundation/voting/service/transaction_submit/MetadataSerialiser.java +++ b/backend-services/voting-admin-app/src/main/java/org/cardano/foundation/voting/service/transaction_submit/MetadataSerialiser.java @@ -18,7 +18,8 @@ @Service public class MetadataSerialiser { - public MetadataMap serialise(CreateEventCommand createEventCommand, long slot) { + public MetadataMap serialise(CreateEventCommand createEventCommand, + long slot) { var map = MetadataBuilder.createMap(); map.put("type", EVENT_REGISTRATION.name()); @@ -28,6 +29,7 @@ public MetadataMap serialise(CreateEventCommand createEventCommand, long slot) { map.put("votingEventType", createEventCommand.getVotingEventType().name()); map.put("schemaVersion", createEventCommand.getSchemaVersion().getSemVer()); map.put("creationSlot", BigInteger.valueOf(slot)); + map.put("votesHashAlgo", "BLAKE2b-256"); if (List.of(STAKE_BASED, BALANCE_BASED).contains(createEventCommand.getVotingEventType())) { map.put("startEpoch", BigInteger.valueOf(createEventCommand.getStartEpoch().orElseThrow())); diff --git a/backend-services/voting-admin-app/src/main/java/org/cardano/foundation/voting/shell/AdminKeysGenerationCommands.java b/backend-services/voting-admin-app/src/main/java/org/cardano/foundation/voting/shell/AdminKeysGenerationCommands.java index 60a424c5a..f55d087cb 100644 --- a/backend-services/voting-admin-app/src/main/java/org/cardano/foundation/voting/shell/AdminKeysGenerationCommands.java +++ b/backend-services/voting-admin-app/src/main/java/org/cardano/foundation/voting/shell/AdminKeysGenerationCommands.java @@ -1,5 +1,8 @@ package org.cardano.foundation.voting.shell; +import com.bloxbean.cardano.client.account.Account; +import com.bloxbean.cardano.client.common.model.Network; +import com.bloxbean.cardano.client.common.model.Networks; import com.bloxbean.cardano.client.util.HexUtil; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.jwk.gen.OctetKeyPairGenerator; @@ -19,32 +22,56 @@ @RequiredArgsConstructor public class AdminKeysGenerationCommands { - @ShellMethod(key = "generate-jwt-keys", value = "Generates JWT keys.") + private final Network network; + + @ShellMethod(key = "admin-generate-new-account", value = "Generates new shelley address and staking key.") + public String generateNewAccount() { + val newAccount = new Account(network); + + var sb = new StringBuilder(); + + sb + .append("Account created.").append("\n").append("\n") + .append("Network: ").append(network.toString()).append("\n") + .append("Address: ").append(newAccount.baseAddress()).append("\n") + .append("StakeAddress: ").append(newAccount.stakeAddress()).append("\n") + .append("Mnemonic: ").append(newAccount.mnemonic()).append("\n") + .append("\n"); + + return sb.toString(); + } + + @ShellMethod(key = "admin-generate-jwt-keys", value = "Generates JWT keys.") public String generateJWTAdminKeys() throws JOSEException { val key = new OctetKeyPairGenerator(Ed25519) .generate(); val keyJson = key.toJSONString(); - log.info("JWT key as json:" + keyJson); + var sb = new StringBuilder(); - String keyHex = HexUtil.encodeHexString(keyJson.getBytes(UTF_8)); + sb.append("JWT admin keys generated.").append("\n").append("\n"); - log.info("JWT key as hex:" + keyHex); + sb.append("JWT key (json): ").append(key.toJSONString()).append("\n"); - return "Created JWT key (hex): " + keyHex; + sb.append("JWT key (hex): ").append(HexUtil.encodeHexString(keyJson.getBytes(UTF_8))).append("\n"); + + return sb.toString(); } - @ShellMethod(key = "generate-salt", value = "Generates salt.") + @ShellMethod(key = "admin-generate-salt", value = "Generates salt.") public String generateSalt() { String randomUUID = UUID.randomUUID().toString(); - log.info("UUID: " + randomUUID); - var uuidSanitized = randomUUID.replace("-", ""); - log.info("UUID (sanitized) (HEX): " + uuidSanitized); - return "Created salt key (hex): " + uuidSanitized; + var sb = new StringBuilder(); + + sb.append("Salt generated.").append("\n").append("\n"); + + sb.append("Salt key (hex): ").append(uuidSanitized).append("\n"); + + return sb.toString(); } } diff --git a/backend-services/voting-admin-app/src/main/java/org/cardano/foundation/voting/shell/CardanoSummit2023ProdCommands.java b/backend-services/voting-admin-app/src/main/java/org/cardano/foundation/voting/shell/CardanoSummit2023ProdCommands.java new file mode 100644 index 000000000..b78fa44e0 --- /dev/null +++ b/backend-services/voting-admin-app/src/main/java/org/cardano/foundation/voting/shell/CardanoSummit2023ProdCommands.java @@ -0,0 +1,581 @@ +package org.cardano.foundation.voting.shell; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.cardano.foundation.voting.domain.CardanoNetwork; +import org.cardano.foundation.voting.domain.CreateCategoryCommand; +import org.cardano.foundation.voting.domain.CreateEventCommand; +import org.cardano.foundation.voting.domain.Proposal; +import org.cardano.foundation.voting.service.transaction_submit.L1SubmissionService; +import org.springframework.core.annotation.Order; +import org.springframework.shell.standard.ShellComponent; +import org.springframework.shell.standard.ShellMethod; +import org.springframework.shell.standard.ShellOption; + +import java.util.HashSet; +import java.util.List; +import java.util.Optional; + +import static org.cardano.foundation.voting.domain.CardanoNetwork.MAIN; +import static org.cardano.foundation.voting.domain.SchemaVersion.V1; +import static org.cardano.foundation.voting.domain.VotingEventType.USER_BASED; + +@ShellComponent +@Slf4j +@RequiredArgsConstructor +public class CardanoSummit2023ProdCommands { + + private final static String EVENT_NAME = "CF_SUMMIT_2023"; + + private final L1SubmissionService l1SubmissionService; + + private final CardanoNetwork network; + + @ShellMethod(key = "01_create-cf-summit03-event-prod", value = "Create a CF-Summit 2023 voting event on a PROD network.") + @Order(1) + public String createCFSummit2023Event() { + if (network != MAIN) { + return "This command can only be run on MAIN network!"; + } + + log.info("Creating CF-Summit 2023 on a MAIN network..."); + + long startSlot = 39093360L + 172800L; // 15-09-2023 11:17 UTC plus 2 days + long endSlot = startSlot + 518400; // 518400 = 6 days in slots + + CreateEventCommand createEventCommand = CreateEventCommand.builder() + .id(EVENT_NAME + "_" + "TEST1") + .startSlot(Optional.of(startSlot)) + .endSlot(Optional.of(endSlot)) + .votingPowerAsset(Optional.empty()) + .organisers("CF") + .votingEventType(USER_BASED) + .schemaVersion(V1) + .allowVoteChanging(false) + .highLevelEventResultsWhileVoting(true) + .highLevelCategoryResultsWhileVoting(true) + .categoryResultsWhileVoting(false) + .proposalsRevealSlot(Optional.of(endSlot + 172800)) + .build(); + + l1SubmissionService.submitEvent(createEventCommand); + + return "Created CF-Summit 2023 event: " + createEventCommand; + } + + @ShellMethod(key = "02_create-ambassador-category-prod", value = "Create a CF-Summit 2023 Ambassador category on a PROD network.") + @Order(2) + public String createAmbassadorCategory(@ShellOption String event) { + if (network != MAIN) { + return "This command can only be run on a PROD network!"; + } + + log.info("Creating CF-Summit 2023 Ambassador category on a PROD network..."); + + Proposal n1 = Proposal.builder() + .id("63123e7f-dfc3-481e-bb9d-fed1d9f6e9b9") + .build(); + + Proposal n2 = Proposal.builder() + .id("0299d93e-93f2-4bc8-9b40-6dd09343c443") + .build(); + + Proposal n3 = Proposal.builder() + .id("fd477fac-ad16-4d2a-91a4-0a4288d3d7aa") + .build(); + + Proposal n4 = Proposal.builder() + .id("0b755eaf-a588-441f-a9dd-50c4aa478a90") + .build(); + + Proposal n5 = Proposal.builder() + .id("2c94cd2e-2ad9-4425-af01-27210afca1e3") + .build(); + + Proposal n6 = Proposal.builder() + .id("e7d4df4a-8305-4ed8-9e42-6f67442d796e") + .build(); + + List allProposals = List.of(n1, n2, n3, n4, n5, n6); + + CreateCategoryCommand createCategoryCommand = CreateCategoryCommand.builder() + .id("AMBASSADOR") + .event(event) + .gdprProtection(true) + .schemaVersion(V1) + .proposals(allProposals) + .build(); + + if (allProposals.size() != new HashSet<>(allProposals).size()) { + throw new RuntimeException("Duplicate proposals detected!"); + } + + l1SubmissionService.submitCategory(createCategoryCommand); + + return "Created CF-Summit 2023 category: " + createCategoryCommand; + } + + @ShellMethod(key = "03_create-blockchain-for-good-category-prod", value = "Create a CF-Summit 2023 Best Blockchain for Good category on a PROD network.") + @Order(3) + public String createBlockchainForGoodCategory(@ShellOption String event) { + if (network != MAIN) { + return "This command can only be run on a PROD network!"; + } + + log.info("Creating CF-Summit 2023 Blockchain For Good category on a PROD network..."); + + Proposal n1 = Proposal.builder() + .id("633199b6-ab4c-49bc-afd8-e8c675d145d0") + .build(); + + Proposal n2 = Proposal.builder() + .id("d4d33796-7372-410a-b640-7dde093f20e5") + .build(); + + Proposal n3 = Proposal.builder() + .id("2e24b92f-1a34-4799-9eb4-a489be2b63c6") + .build(); + + Proposal n4 = Proposal.builder() + .id("4cbeb976-20ba-4c20-bdc1-f21bf28c17fd") + .build(); + + Proposal n5 = Proposal.builder() + .id("71f4f082-4512-4e7f-adc6-6092d1b3aa14") + .build(); + + Proposal n6 = Proposal.builder() + .id("07d4a8b3-dbfc-412c-9931-a5252db9082d") + .build(); + + List allProposals = List.of(n1, n2, n3, n4, n5, n6); + CreateCategoryCommand createCategoryCommand = CreateCategoryCommand.builder() + .id("BLOCKCHAIN_FOR_GOOD") + .event(event) + .gdprProtection(true) + .schemaVersion(V1) + .proposals(allProposals) + .build(); + + if (allProposals.size() != new HashSet<>(allProposals).size()) { + throw new RuntimeException("Duplicate proposals detected!"); + } + + l1SubmissionService.submitCategory(createCategoryCommand); + + return "Created CF-Summit 2023 category: " + createCategoryCommand; + } + + @ShellMethod(key = "04_create-cips-category-prod", value = "Create a CF-Summit 2023 CIPs category on a PROD network.") + @Order(4) + public String createCIPsCategory(@ShellOption String event) { + if (network != MAIN) { + return "This command can only be run on a PROD network!"; + } + + log.info("Creating CF-Summit 2023 CIPs category on a PROD network..."); + + Proposal n1 = Proposal.builder() + .id("4cf1ea70-87dd-45ee-85a0-2d29720725f7") + .build(); + + Proposal n2 = Proposal.builder() + .id("d5116b52-1e82-4c7f-95e5-2a74a9f75492") + .build(); + + Proposal n3 = Proposal.builder() + .id("69ee264f-9d02-48ef-98f1-541ffb28756f") + .build(); + + Proposal n4 = Proposal.builder() + .id("70a88fb6-a87f-4ebd-8719-31c461118f3d") + .build(); + + Proposal n5 = Proposal.builder() + .id("eaafc7a1-8944-4faf-91dd-5ceefa51e8db") + .build(); + + Proposal n6 = Proposal.builder() + .id("9cdbde0e-ffd4-4d22-ae3c-17cb63dc89fd") + .build(); + + List allProposals = List.of(n1, n2, n3, n4, n5, n6); + CreateCategoryCommand createCategoryCommand = CreateCategoryCommand.builder() + .id("CIPS") + .event(event) + .gdprProtection(true) + .schemaVersion(V1) + .proposals(allProposals) + .build(); + + if (allProposals.size() != new HashSet<>(allProposals).size()) { + throw new RuntimeException("Duplicate proposals detected!"); + } + + l1SubmissionService.submitCategory(createCategoryCommand); + + return "Created CF-Summit 2023 category: " + createCategoryCommand; + } + + @ShellMethod(key = "05_create-defi-dex-category-prod", value = "Create a CF-Summit 2023 DeFi / DEX category on a PROD network.") + @Order(5) + public String createDeFiDEXCategory(@ShellOption String event) { + if (network != MAIN) { + return "This command can only be run on a PROD network!"; + } + + log.info("Creating CF-Summit 2023 Best DeFi / DEX category on a PROD network..."); + + Proposal n1 = Proposal.builder() + .id("f71c438d-8247-4064-a5aa-4d21b54c2a5d") + .build(); + + Proposal n2 = Proposal.builder() + .id("a5f92606-6e15-4f91-8443-7030eb02a274") + .build(); + + Proposal n3 = Proposal.builder() + .id("e9d72191-bda4-437e-af4b-2f979bad5c7f") + .build(); + + Proposal n4 = Proposal.builder() + .id("8cf20a27-8bc8-49f3-8133-ef76e899e1c1") + .build(); + + Proposal n5 = Proposal.builder() + .id("91871e20-f9aa-422f-9213-3722ac47c1c6") + .build(); + + Proposal n6 = Proposal.builder() + .id("c7c7ca58-af7e-4f27-a170-070d76707580") + .build(); + + List allProposals = List.of(n1, n2, n3, n4, n5, n6); + + CreateCategoryCommand createCategoryCommand = CreateCategoryCommand.builder() + .id("BEST_DEFI_DEX") + .event(event) + .gdprProtection(true) + .schemaVersion(V1) + .proposals(allProposals) + .build(); + + if (allProposals.size() != new HashSet<>(allProposals).size()) { + throw new RuntimeException("Duplicate proposals detected!"); + } + + l1SubmissionService.submitCategory(createCategoryCommand); + + return "Created CF-Summit 2023 category: " + createCategoryCommand; + } + + @ShellMethod(key = "06_create-developer-or-developer-tools-category-prod", value = "Create a CF-Summit 2023 Developer or Developer Tools category on a PROD network.") + @Order(6) + public String createBestDeveloperOrDeveloperTools(@ShellOption String event) { + if (network != MAIN) { + return "This command can only be run on a PROD network!"; + } + + log.info("Creating CF-Summit 2023 Developer or Developer Tools category on a PROD network..."); + + Proposal n1 = Proposal.builder() + .id("f4d6055f-964e-43b4-bc23-83141ca04f9f") + .build(); + + Proposal n2 = Proposal.builder() + .id("a00e6d3e-1b06-48f0-b2a0-4f3784e6226c") + .build(); + + Proposal n3 = Proposal.builder() + .id("58bb4e29-5124-473b-80e1-c5c8ffa57dbb") + .build(); + + Proposal n4 = Proposal.builder() + .id("a30267e1-314c-4801-aa95-b03dd4d6856e") + .build(); + + Proposal n5 = Proposal.builder() + .id("ec34567c-2012-4e3b-94ee-8778a6e33a04") + .build(); + + Proposal n6 = Proposal.builder() + .id("204a8d71-adb1-4fff-b59d-5fb391d0078d") + .build(); + + List allProposals = List.of(n1, n2, n3, n4, n5, n6); + CreateCategoryCommand createCategoryCommand = CreateCategoryCommand.builder() + .id("BEST_DEVELOPER_OR_DEVELOPER_TOOLS") + .event(event) + .gdprProtection(true) + .schemaVersion(V1) + .proposals(allProposals) + .build(); + + if (allProposals.size() != new HashSet<>(allProposals).size()) { + throw new RuntimeException("Duplicate proposals detected!"); + } + + l1SubmissionService.submitCategory(createCategoryCommand); + + return "Created CF-Summit 2023 category: " + createCategoryCommand; + } + + @ShellMethod(key = "07_create-educational-influencer-category-prod", value = "Create a CF-Summit 2023 Educational Influencer category on a PROD network.") + @Order(7) + public String createEducationalInfluencer(@ShellOption String event) { + if (network != MAIN) { + return "This command can only be run on a PROD network!"; + } + + log.info("Creating CF-Summit 2023 Educational Influence category on a PROD network..."); + + Proposal n1 = Proposal.builder() + .id("88af463f-0d9c-4738-baef-bdb80f2c374e") + .build(); + + Proposal n2 = Proposal.builder() + .id("65cf347e-129a-459b-b192-55ae37e03160") + .build(); + + Proposal n3 = Proposal.builder() + .id("1e609753-a83e-4ff6-9cf8-dd90803f0368") + .build(); + + Proposal n4 = Proposal.builder() + .id("9b91f3ed-42be-4650-8e43-6d7a416f9591") + .build(); + + Proposal n5 = Proposal.builder() + .id("702efda6-ceec-413e-8f33-aa206962850c") + .build(); + + Proposal n6 = Proposal.builder() + .id("e0b5f280-95f4-42aa-8ecb-00b95c2896a4") + .build(); + + List allProposals = List.of(n1, n2, n3, n4, n5, n6); + + CreateCategoryCommand createCategoryCommand = CreateCategoryCommand.builder() + .id("EDUCATIONAL_INFLUENCER") + .event(event) + .gdprProtection(true) + .schemaVersion(V1) + .proposals(allProposals) + .build(); + + if (allProposals.size() != new HashSet<>(allProposals).size()) { + throw new RuntimeException("Duplicate proposals detected!"); + } + + l1SubmissionService.submitCategory(createCategoryCommand); + + return "Created CF-Summit 2023 category: " + createCategoryCommand; + } + + @ShellMethod(key = "08_create-marketplace-category-prod", value = "Create a CF-Summit 2023 Marketplace category on a PROD network.") + @Order(8) + public String createMarketPlaceCategory(@ShellOption String event) { + if (network != MAIN) { + return "This command can only be run on a PROD network!"; + } + + log.info("Creating CF-Summit 2023 Marketplace category on a PROD network..."); + + Proposal n1 = Proposal.builder() + .id("33038f64-fff9-44cc-a8e5-d4f5896c8ff6") + .build(); + + Proposal n2 = Proposal.builder() + .id("953c7970-6f9d-41a0-8556-b83ff7b481fe") + .build(); + + Proposal n3 = Proposal.builder() + .id("107fc947-85f0-442e-b56f-9c10e8b5631a") + .build(); + + Proposal n4 = Proposal.builder() + .id("0752dc99-19fa-4f4c-96c4-25ca3a66a12f") + .build(); + + Proposal n5 = Proposal.builder() + .id("5b2145cd-8740-4254-942f-889eb3671640") + .build(); + + Proposal n6 = Proposal.builder() + .id("01991af2-3bc9-4818-a745-db7683c8fe37") + .build(); + + List allProposals = List.of(n1, n2, n3, n4, n5, n6); + + CreateCategoryCommand createCategoryCommand = CreateCategoryCommand.builder() + .id("MARKETPLACE") + .event(event) + .gdprProtection(true) + .schemaVersion(V1) + .proposals(allProposals) + .build(); + + if (allProposals.size() != new HashSet<>(allProposals).size()) { + throw new RuntimeException("Duplicate proposals detected!"); + } + + l1SubmissionService.submitCategory(createCategoryCommand); + + return "Created CF-Summit 2023 category: " + createCategoryCommand; + } + + @ShellMethod(key = "09_create-most-impactful-sspo-category-prod", value = "Create a CF-Summit 2023 Most Impactful SSPO category on a PROD network.") + @Order(9) + public String createMostImpactfulSPOCategory(@ShellOption String event) { + if (network != MAIN) { + return "This command can only be run on a MAIN network!"; + } + + log.info("Creating CF-Summit 2023 Most Impactful SSPO category on a PROD network..."); + + Proposal n1 = Proposal.builder() + .id("6e16cdae-7696-4c41-a5f2-de373a17f488") + .build(); + + Proposal n2 = Proposal.builder() + .id("46ea36b8-15b6-4d31-8c29-946342595756") + .build(); + + Proposal n3 = Proposal.builder() + .id("e3a130e7-45c9-47c5-a121-fbdebc6c3e9f") + .build(); + + Proposal n4 = Proposal.builder() + .id("cfe477d2-e7eb-46a7-a8ee-f721da2de399") + .build(); + + Proposal n5 = Proposal.builder() + .id("6ee41116-b60c-41d2-974c-c3de31b71a83") + .build(); + + Proposal n6 = Proposal.builder() + .id("e4895e4b-b25a-43d5-9bd0-1dcd23954faa") + .build(); + + List allProposals = List.of(n1, n2, n3, n4, n5, n6); + CreateCategoryCommand createCategoryCommand = CreateCategoryCommand.builder() + .id("MOST_IMPACTFUL_SSPO") + .event(event) + .gdprProtection(true) + .schemaVersion(V1) + .proposals(allProposals) + .build(); + + if (allProposals.size() != new HashSet<>(allProposals).size()) { + throw new RuntimeException("Duplicate proposals detected!"); + } + + l1SubmissionService.submitCategory(createCategoryCommand); + + return "Created CF-Summit 2023 category: " + createCategoryCommand; + } + + @ShellMethod(key = "10_create-nft-project-category-prod", value = "Create a CF-Summit 2023 NFT Project category on a PROD network.") + @Order(10) + public String createNFTProjectCategory(@ShellOption String event) { + if (network != MAIN) { + return "This command can only be run on a PROD network!"; + } + + log.info("Creating CF-Summit 2023 NFT Project category on a PROD network..."); + + Proposal n1 = Proposal.builder() + .id("623405b4-a845-4130-b406-b4cf4a1a985d") + .build(); + + Proposal n2 = Proposal.builder() + .id("9074cf60-d413-4c20-a344-a3f894d2e6c0") + .build(); + + Proposal n3 = Proposal.builder() + .id("1bae816a-b943-4148-ac58-c4081ef8cac5") + .build(); + + Proposal n4 = Proposal.builder() + .id("c0f06200-b04c-4e08-b00b-e050cdcc205c") + .build(); + + Proposal n5 = Proposal.builder() + .id("02bd8150-91cd-499d-b94c-c0e7b5fd5dc4") + .build(); + + Proposal n6 = Proposal.builder() + .id("50d31468-a915-4284-9e3f-e0c6f5c1c90c") + .build(); + + List allProposals = List.of(n1, n2, n3, n4, n5, n6); + CreateCategoryCommand createCategoryCommand = CreateCategoryCommand.builder() + .id("NFT_PROJECT") + .event(event) + .gdprProtection(true) + .schemaVersion(V1) + .proposals(allProposals) + .build(); + + if (allProposals.size() != new HashSet<>(allProposals).size()) { + throw new RuntimeException("Duplicate proposals detected!"); + } + + l1SubmissionService.submitCategory(createCategoryCommand); + + return "Created CF-Summit 2023 category: " + createCategoryCommand; + } + + @ShellMethod(key = "11_create-ssi-category-prod", value = "Create a CF-Summit 2023 SSI category on a PROD network.") + @Order(11) + public String createSSICategory(@ShellOption String event) { + if (network != MAIN) { + return "This command can only be run on a PROD network!"; + } + + log.info("Creating CF-Summit 2023 SSI category on a PROD network..."); + + Proposal n1 = Proposal.builder() + .id("a910a8af-f63b-4190-90fb-1409fd110526") + .build(); + + Proposal n2 = Proposal.builder() + .id("0a70b72d-1394-4bdd-bf93-e79ceb0c40a6") + .build(); + + Proposal n3 = Proposal.builder() + .id("f37bf063-15fc-4959-a6a4-0349a7613ede") + .build(); + + Proposal n4 = Proposal.builder() + .id("57f93799-5123-4ad0-a13f-a7c70387a756") + .build(); + + Proposal n5 = Proposal.builder() + .id("a355c7f7-c3be-46c5-bcbf-52e628323c88") + .build(); + + Proposal n6 = Proposal.builder() + .id("159fb64e-9613-4b5b-8618-a1c458cf9cf8") + .build(); + + List allProposals = List.of(n1, n2, n3, n4, n5, n6); + + CreateCategoryCommand createCategoryCommand = CreateCategoryCommand.builder() + .id("SSI") + .event(event) + .gdprProtection(true) + .schemaVersion(V1) + .proposals(allProposals) + .build(); + + if (allProposals.size() != new HashSet<>(allProposals).size()) { + throw new RuntimeException("Duplicate proposals detected!"); + } + + l1SubmissionService.submitCategory(createCategoryCommand); + + return "Created CF-Summit 2023 category: " + createCategoryCommand; + } + +} diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/VotingApp.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/VotingApp.java index 4b8f1858b..73c7e7663 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/VotingApp.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/VotingApp.java @@ -19,6 +19,7 @@ import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.core.io.ClassPathResource; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.transaction.annotation.EnableTransactionManagement; @@ -37,6 +38,7 @@ @EnableScheduling @Slf4j @ImportRuntimeHints(VotingApp.Hints.class) +@EnableAsync public class VotingApp { public static void main(String[] args) { diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/config/ThreadPoolsConfig.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/config/ThreadPoolsConfig.java deleted file mode 100644 index 94d775f56..000000000 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/config/ThreadPoolsConfig.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.cardano.foundation.voting.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; - -import java.util.concurrent.Executor; - -@Configuration -public class ThreadPoolsConfig { - - @Bean(name = "taskExecutor") - public Executor taskExecutor() { - ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); - threadPoolTaskExecutor.setQueueCapacity(100); - - threadPoolTaskExecutor.setCorePoolSize(2); - - threadPoolTaskExecutor.setMaxPoolSize(5); - - return threadPoolTaskExecutor; - } - - @Bean(name = "asyncExecutor") - public Executor threadPoolTaskExecutor() { - ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); - threadPoolTaskExecutor.setQueueCapacity(100); - - threadPoolTaskExecutor.setCorePoolSize(2); - - threadPoolTaskExecutor.setMaxPoolSize(5); - - return threadPoolTaskExecutor; - } - -} diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/domain/L1MerkleCommitment.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/domain/L1MerkleCommitment.java index 8bb3c71fc..5bd5c2ccd 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/domain/L1MerkleCommitment.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/domain/L1MerkleCommitment.java @@ -1,9 +1,11 @@ package org.cardano.foundation.voting.domain; -import org.cardano.foundation.voting.domain.entity.Vote; +import org.cardano.foundation.voting.repository.VoteRepository; import org.cardanofoundation.merkle.MerkleElement; import java.util.List; // L1 Merkle commitment for an event -public record L1MerkleCommitment(List votes, MerkleElement root, String eventId) { } \ No newline at end of file +public record L1MerkleCommitment(List signedVotes, + MerkleElement root, + String eventId) { } \ No newline at end of file diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/domain/VoteSerialisations.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/domain/VoteSerialisations.java new file mode 100644 index 000000000..558fb8425 --- /dev/null +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/domain/VoteSerialisations.java @@ -0,0 +1,26 @@ +package org.cardano.foundation.voting.domain; + +import org.cardano.foundation.voting.repository.VoteRepository; +import org.cardanofoundation.cip30.CIP30Verifier; + +import java.util.Optional; +import java.util.function.Function; + +import static com.bloxbean.cardano.client.crypto.Blake2bUtil.blake2bHash256; + +public final class VoteSerialisations { + + public static final Function VOTE_SERIALISER = createSerialiserFunction(); + + public static Function createSerialiserFunction() { + return vote -> { + var cip30Verifier = new CIP30Verifier(vote.getCoseSignature(), vote.getCosePublicKey()); + var verificationResult = cip30Verifier.verify(); + + var bytes = Optional.ofNullable(verificationResult.getMessage()).orElse(new byte[0]); + + return blake2bHash256(bytes); + }; + } + +} diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/domain/entity/Vote.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/domain/entity/Vote.java index 625c70706..2b76eb49a 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/domain/entity/Vote.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/domain/entity/Vote.java @@ -9,11 +9,9 @@ import lombok.NoArgsConstructor; import lombok.ToString; import lombok.extern.slf4j.Slf4j; -import org.cardanofoundation.cip30.CIP30Verifier; import javax.annotation.Nullable; import java.util.Optional; -import java.util.function.Function; @NoArgsConstructor @AllArgsConstructor @@ -24,8 +22,6 @@ @ToString(exclude = { "coseSignature", "cosePublicKey"} ) public class Vote extends AbstractTimestampEntity { - public static final Function VOTE_SERIALISER = createSerialiserFunction(); - @Id @Column(name = "id", nullable = false) private String id; @@ -130,19 +126,4 @@ public void setVotedAtSlot(long votedAtSlot) { this.votedAtSlot = votedAtSlot; } - private static Function createSerialiserFunction() { - return vote -> { - var cip30Verifier = new CIP30Verifier(vote.getCoseSignature(), vote.getCosePublicKey()); - var verificationResult = cip30Verifier.verify(); - - if (!verificationResult.isValid()) { - log.info("Verifying vote failed: {}", verificationResult.getMessage()); - - return new byte[0]; - } - - return Optional.ofNullable(verificationResult.getMessage()).orElse(new byte[0]); - }; - } - } diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/jobs/VoteCommitmentJob.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/jobs/VoteCommitmentJob.java index aa4e5dd16..303d0bbe1 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/jobs/VoteCommitmentJob.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/jobs/VoteCommitmentJob.java @@ -1,35 +1,33 @@ package org.cardano.foundation.voting.jobs; +import io.micrometer.core.annotation.Timed; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.cardano.foundation.voting.service.merkle_tree.VoteCommitmentService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.util.StopWatch; @Slf4j @Service +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "vote.commitment", value = "enabled", havingValue = "true") public class VoteCommitmentJob implements Runnable { - @Autowired - private VoteCommitmentService voteCommitmentService; - - @Value("${vote.commitment.enabled}") - private boolean isVoteCommitmentEnabled; + private final VoteCommitmentService voteCommitmentService; @Override @Scheduled(cron = "${vote.commitment.cron.expression}") + @Timed(value = "vote.commitment.cron.job", histogram = true) public void run() { - if (!isVoteCommitmentEnabled) { - log.info("L1 votes commitment disabled on this instance."); - return; - } - log.info("Starting VoteCommitmentJob..."); + var startStop = new StopWatch(); startStop.start(); + voteCommitmentService.processVotesForAllEvents(); + startStop.stop(); log.info("VoteCommitmentJob completed, running time:{} secs", startStop.getTotalTimeSeconds()); diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/repository/VoteRepository.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/repository/VoteRepository.java index 4e6143c1b..9ea472c89 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/repository/VoteRepository.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/repository/VoteRepository.java @@ -14,7 +14,7 @@ public interface VoteRepository extends JpaRepository { @Query("SELECT v FROM Vote v WHERE v.eventId = :eventId ORDER BY v.votedAtSlot, v.createdAt DESC") - List findAllByEventId(@Param("eventId") String eventId); + List findAllCompactVotesByEventId(@Param("eventId") String eventId); Optional findByEventIdAndCategoryIdAndVoterStakingAddress(String eventId, String categoryId, String voterStakeAddress); @@ -63,4 +63,12 @@ interface CategoryLevelStats { } + interface CompactVote { + + String getCoseSignature(); + + Optional getCosePublicKey(); + + } + } diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/auth/Headers.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/resource/Headers.java similarity index 83% rename from backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/auth/Headers.java rename to backend-services/voting-app/src/main/java/org/cardano/foundation/voting/resource/Headers.java index c8235e442..9869de119 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/auth/Headers.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/resource/Headers.java @@ -1,4 +1,4 @@ -package org.cardano.foundation.voting.service.auth; +package org.cardano.foundation.voting.resource; public final class Headers { diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/resource/LeaderboardResource.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/resource/LeaderboardResource.java index c484f462b..621ec045d 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/resource/LeaderboardResource.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/resource/LeaderboardResource.java @@ -15,7 +15,7 @@ import static org.springframework.web.bind.annotation.RequestMethod.GET; import static org.springframework.web.bind.annotation.RequestMethod.HEAD; -import static org.cardano.foundation.voting.service.auth.Headers.XForceLeaderBoardResults; +import static org.cardano.foundation.voting.resource.Headers.XForceLeaderBoardResults; @RestController @RequestMapping("/api/leaderboard") diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/resource/VoteResource.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/resource/VoteResource.java index 504637f78..3687bafa4 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/resource/VoteResource.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/resource/VoteResource.java @@ -27,8 +27,6 @@ public class VoteResource { @RequestMapping(value = "/cast", method = POST, produces = "application/json") @Timed(value = "resource.vote.cast", histogram = true) public ResponseEntity castVote(Authentication authentication) { - log.info("Casting vote..."); - return voteService.castVote((Web3AuthenticationToken) authentication) .fold(problem -> { log.warn("Vote cast failed, problem:{}", problem); diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/auth/web3/Web3Filter.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/auth/web3/Web3Filter.java index 21042da2a..c1da390a7 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/auth/web3/Web3Filter.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/auth/web3/Web3Filter.java @@ -28,8 +28,8 @@ import java.util.Optional; import static org.cardano.foundation.voting.domain.Role.VOTER; -import static org.cardano.foundation.voting.service.auth.Headers.XCIP93PublicKey; -import static org.cardano.foundation.voting.service.auth.Headers.XCIP93Signature; +import static org.cardano.foundation.voting.resource.Headers.XCIP93PublicKey; +import static org.cardano.foundation.voting.resource.Headers.XCIP93Signature; import static org.cardano.foundation.voting.service.auth.LoginSystem.CIP93; import static org.cardano.foundation.voting.utils.MoreNumber.isNumeric; import static org.cardanofoundation.cip30.MessageFormat.TEXT; @@ -128,6 +128,7 @@ protected void doFilterInternal(HttpServletRequest request, .build(); sendBackProblem(response, problem); + return; } diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/leader_board/DefaultLeaderBoardService.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/leader_board/DefaultLeaderBoardService.java index 7d7f75949..e6d8883f7 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/leader_board/DefaultLeaderBoardService.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/leader_board/DefaultLeaderBoardService.java @@ -41,7 +41,6 @@ private Either isHighLevelEventLeaderboardAvailable(ChainFollo return Either.right(eventDetails.proposalsReveal()); } - private Either isHighLevelCategoryLeaderboardAvailable(ChainFollowerClient.EventDetailsResponse eventDetails, boolean forceLeaderboard) { if (forceLeaderboard) { @@ -70,7 +69,8 @@ private Either isCategoryLeaderboardAvailable(ChainFollowerCli } @Override - public Either isHighLevelEventLeaderboardAvailable(String event, boolean forceLeaderboard) { + public Either isHighLevelEventLeaderboardAvailable(String event, + boolean forceLeaderboard) { var eventDetailsE = chainFollowerClient.getEventDetails(event); if (eventDetailsE.isEmpty()) { return Either.left(Problem.builder() @@ -159,7 +159,6 @@ public Either getEventLeaderboard(String even var votes = voteRepository.getHighLevelEventStats(event); if (votes.isEmpty()) { - Leaderboard.ByEventStats.ByEventStatsBuilder byEventStatsBuilder = Leaderboard.ByEventStats.builder() .event(eventDetails.id()) .totalVotesCount(0L); @@ -293,15 +292,15 @@ public Either getCategoryLeader Map proposalResultsMap = votes.stream() .collect(toMap(VoteRepository.CategoryLevelStats::getProposalId, v -> { - var totalVotesCount = Optional.ofNullable(v.getTotalVoteCount()).orElse(0L); - var totalVotingPower = Optional.ofNullable(v.getTotalVotingPower()).map(String::valueOf).orElse("0"); + var totalVotesCount = Optional.ofNullable(v.getTotalVoteCount()).orElse(0L); + var totalVotingPower = Optional.ofNullable(v.getTotalVotingPower()).map(String::valueOf).orElse("0"); - var b = Leaderboard.Votes.builder(); - b.votes(totalVotesCount); + var b = Leaderboard.Votes.builder(); + b.votes(totalVotesCount); - switch (eventDetails.votingEventType()) { - case BALANCE_BASED, STAKE_BASED -> b.votingPower(totalVotingPower); - } + switch (eventDetails.votingEventType()) { + case BALANCE_BASED, STAKE_BASED -> b.votingPower(totalVotingPower); + } return b.build(); })); diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/merkle_tree/VoteCommitmentService.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/merkle_tree/VoteCommitmentService.java index d513bec6d..b0378c6b2 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/merkle_tree/VoteCommitmentService.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/merkle_tree/VoteCommitmentService.java @@ -1,74 +1,71 @@ package org.cardano.foundation.voting.service.merkle_tree; import io.vavr.Value; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.cardano.foundation.voting.client.ChainFollowerClient; import org.cardano.foundation.voting.domain.L1MerkleCommitment; +import org.cardano.foundation.voting.domain.L1SubmissionData; import org.cardano.foundation.voting.domain.entity.VoteMerkleProof; +import org.cardano.foundation.voting.service.json.JsonService; import org.cardano.foundation.voting.service.transaction_submit.L1SubmissionService; import org.cardano.foundation.voting.service.vote.VoteService; +import org.cardanofoundation.cip30.CIP30Verifier; import org.cardanofoundation.merkle.MerkleTree; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.scheduling.annotation.Async; -import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.stereotype.Service; import org.springframework.util.StopWatch; import java.util.List; import static com.bloxbean.cardano.client.util.HexUtil.encodeHexString; -import static org.cardano.foundation.voting.domain.entity.Vote.VOTE_SERIALISER; +import static org.cardano.foundation.voting.domain.VoteSerialisations.VOTE_SERIALISER; +import static org.cardano.foundation.voting.domain.VoteSerialisations.createSerialiserFunction; +import static org.cardanofoundation.cip30.MessageFormat.TEXT; @Service @Slf4j -@EnableAsync +@RequiredArgsConstructor public class VoteCommitmentService { - @Autowired - private VoteService voteService; + private final VoteService voteService; - @Autowired - private L1SubmissionService l1SubmissionService; + private final L1SubmissionService l1SubmissionService; - @Autowired - private ChainFollowerClient chainFollowerClient; + private final ChainFollowerClient chainFollowerClient; - @Autowired - private VoteMerkleProofService voteMerkleProofService; + private final VoteMerkleProofService voteMerkleProofService; - @Autowired - private MerkleProofSerdeService merkleProofSerdeService; + private final MerkleProofSerdeService merkleProofSerdeService; + + private final JsonService jsonService; - @Async("asyncExecutor") public void processVotesForAllEvents() { - var l1MerkleCommitments = getL1MerkleCommitments(); + var l1MerkleCommitments = getValidL1MerkleCommitments(); if (l1MerkleCommitments.isEmpty()) { log.info("No l1 commitments to process."); return; } - // Event maybe active but it makes no sense spamming L1 when there are no votes to process - if (l1MerkleCommitments.stream().allMatch(l1MerkleCommitment -> l1MerkleCommitment.votes().isEmpty())) { - log.info("No votes to process."); + // Event maybe active but it makes no sense spamming L1 when there are no signedVotes to process + if (l1MerkleCommitments.stream().allMatch(l1MerkleCommitment -> l1MerkleCommitment.signedVotes().isEmpty())) { + log.info("No signedVotes to process."); return; } var l1TransactionDataE = l1SubmissionService.submitMerkleCommitments(l1MerkleCommitments); if (l1TransactionDataE.isEmpty()) { - var issue = l1TransactionDataE.swap().get(); + var problem = l1TransactionDataE.swap().get(); - log.error("Transaction submission failed, issue:{}, will try to submit again in some time...", issue.toString()); + log.error("Transaction submission failed, problem:{}, will try to submit again in some time...", problem.toString()); return; } var l1SubmissionData = l1TransactionDataE.get(); - var l1TransactionHash = l1SubmissionData.txHash(); - var l1TransactionSlot = l1SubmissionData.slot(); - generateAndStoreMerkleProofs(l1MerkleCommitments, l1TransactionHash, l1TransactionSlot); + generateAndStoreMerkleProofs(l1MerkleCommitments, l1SubmissionData); } - private List getL1MerkleCommitments() { + private List getValidL1MerkleCommitments() { var activeEventsE = chainFollowerClient.findAllActiveEvents(); if (activeEventsE.isEmpty()) { var issue = activeEventsE.swap().get(); @@ -81,22 +78,32 @@ private List getL1MerkleCommitments() { return activeEvents.stream() .map(event -> { - // TODO caching or paging or both? Maybe we use Redis??? - log.info("Loading votes from db for active event:{}", event.id()); + // TODO caching or paging or both or neither? Maybe we use Redis??? + log.info("Loading signedVotes from db for active event:{}", event.id()); var stopWatch = new StopWatch(); stopWatch.start(); - var allVotes = voteService.findAll(event.id()); + + var votes = voteService.findAllCompactVotesByEventId(event.id()) + .stream() + .filter(signedWeb3Request -> { + var cip30Result = new CIP30Verifier(signedWeb3Request.getCoseSignature(), signedWeb3Request.getCosePublicKey()); + + return cip30Result.verify().isValid(); + }) + .toList(); + stopWatch.stop(); - log.info("Loaded votes, count:{}, time: {} secs", allVotes.size(), stopWatch.getTotalTimeSeconds()); + log.info("Loaded signedVotes, count:{}, time: {} secs", votes.size(), stopWatch.getTotalTimeSeconds()); - var root = MerkleTree.fromList(allVotes, VOTE_SERIALISER); + var root = MerkleTree.fromList(votes, VOTE_SERIALISER); - return new L1MerkleCommitment(allVotes, root, event.id()); + return new L1MerkleCommitment(votes, root, event.id()); }) .toList(); } - private void generateAndStoreMerkleProofs(List l1MerkleCommitments, String l1TransactionHash, long l1TransactionSlot) { + private void generateAndStoreMerkleProofs(List l1MerkleCommitments, + L1SubmissionData l1SubmissionData) { log.info("Storing vote merkle proofs..."); for (var l1MerkleCommitment : l1MerkleCommitments) { @@ -106,24 +113,43 @@ private void generateAndStoreMerkleProofs(List l1MerkleCommi var storeProofsStartStop = new StopWatch(); storeProofsStartStop.start(); - for (var vote : l1MerkleCommitment.votes()) { - var maybeMerkleProof = MerkleTree.getProof(root, vote, VOTE_SERIALISER).map(Value::toJavaList); + for (var vote : l1MerkleCommitment.signedVotes()) { + var maybeMerkleProof = MerkleTree.getProof(root, vote, createSerialiserFunction()).map(Value::toJavaList); if (maybeMerkleProof.isEmpty()) { - log.error("Merkle proof is empty for vote: {}, this should never ever happen", vote.getId()); - throw new RuntimeException("Merkle proof is empty for vote: " + vote.getId()); + log.error("Merkle proof is empty for vote: {}, this should never ever happen", vote.getCoseSignature()); + throw new RuntimeException("Merkle proof is empty for vote: " + vote.getCoseSignature()); } var proofItems = maybeMerkleProof.orElseThrow(); var merkleRootHash = encodeHexString(root.itemHash()); var proofItemsJson = merkleProofSerdeService.serialiseAsString(proofItems); + var cip30Verifier = new CIP30Verifier(vote.getCoseSignature(), vote.getCosePublicKey()); + + var cip30VerificationResult = cip30Verifier.verify(); + if (!cip30VerificationResult.isValid()) { + log.error("Invalid CIP 30 signature for vote:{}", vote.getCoseSignature()); + continue; + } + + var voteSignedJsonPayload = cip30VerificationResult.getMessage(TEXT); + + var cip93EnvelopeE = jsonService.decodeCIP93VoteEnvelope(voteSignedJsonPayload); + + if (cip93EnvelopeE.isEmpty()) { + log.error("Invalid voteSignedJsonPayload for vote:{}", vote.getCoseSignature()); + continue; + } + + var voteEnvelopeCIP93Envelope = cip93EnvelopeE.get(); + var voteMerkleProof = VoteMerkleProof.builder() - .voteId(vote.getId()) - .eventId(vote.getEventId()) + .voteId(voteEnvelopeCIP93Envelope.getData().getId()) + .eventId(voteEnvelopeCIP93Envelope.getData().getEvent()) .rootHash(merkleRootHash) - .absoluteSlot(l1TransactionSlot) + .absoluteSlot(l1SubmissionData.slot()) .proofItemsJson(proofItemsJson) - .l1TransactionHash(l1TransactionHash) + .l1TransactionHash(l1SubmissionData.txHash()) .invalidated(false) .build(); @@ -131,7 +157,7 @@ private void generateAndStoreMerkleProofs(List l1MerkleCommi } storeProofsStartStop.stop(); - log.info("Storing merkle proofs: {}, completed for event: {}, time: {} secs", l1MerkleCommitment.votes().size(), l1MerkleCommitment.eventId(), storeProofsStartStop.getTotalTimeSeconds()); + log.info("Storing merkle proofs: {}, completed for event: {}, time: {} secs", l1MerkleCommitment.signedVotes().size(), l1MerkleCommitment.eventId(), storeProofsStartStop.getTotalTimeSeconds()); } log.info("Storing vote merkle proofs for all events completed."); diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/transaction_submit/L1TransactionCreator.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/transaction_submit/L1TransactionCreator.java index 6857077b4..84346e251 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/transaction_submit/L1TransactionCreator.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/transaction_submit/L1TransactionCreator.java @@ -7,7 +7,6 @@ import com.bloxbean.cardano.client.cip.cip30.CIP30DataSigner; import com.bloxbean.cardano.client.cip.cip30.DataSignature; import com.bloxbean.cardano.client.common.cbor.CborSerializationUtil; -import com.bloxbean.cardano.client.crypto.Blake2bUtil; import com.bloxbean.cardano.client.function.helper.SignerProviders; import com.bloxbean.cardano.client.metadata.Metadata; import com.bloxbean.cardano.client.metadata.MetadataBuilder; @@ -30,6 +29,7 @@ import java.util.List; +import static com.bloxbean.cardano.client.crypto.Blake2bUtil.blake2bHash224; import static org.cardano.foundation.voting.domain.OnChainEventType.COMMITMENTS; @Service @@ -67,12 +67,13 @@ public Either submitMerkleCommitments(List } @SneakyThrows - protected Metadata serialiseMetadata(MetadataMap childMetadata, OnChainEventType onChainEventType) { + protected Metadata serialiseMetadata(MetadataMap childMetadata, + OnChainEventType onChainEventType) { var stakeAddress = organiserAccount.stakeAddress(); var stakeAddressAccount = new Address(stakeAddress); var data = CborSerializationUtil.serialize(childMetadata.getMap()); - var hashedData = Blake2bUtil.blake2bHash224(data); + var hashedData = blake2bHash224(data); DataSignature dataSignature = CIP30DataSigner.INSTANCE.signData( stakeAddressAccount.getBytes(), diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/transaction_submit/MetadataSerialiser.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/transaction_submit/MetadataSerialiser.java index 27988192a..5a5581a62 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/transaction_submit/MetadataSerialiser.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/transaction_submit/MetadataSerialiser.java @@ -25,7 +25,7 @@ public MetadataMap serialise(List l1MerkleCommitments, long var l1CommitmentMap = MetadataBuilder.createMap(); for (var l1MerkleCommitment : l1MerkleCommitments) { - if (l1MerkleCommitment.votes().isEmpty()) { + if (l1MerkleCommitment.signedVotes().isEmpty()) { continue; } diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/vote/DefaultVoteService.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/vote/DefaultVoteService.java index 3f06bf59e..c3e7853b4 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/vote/DefaultVoteService.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/vote/DefaultVoteService.java @@ -3,7 +3,6 @@ import com.nimbusds.jwt.SignedJWT; import io.micrometer.core.annotation.Timed; import io.vavr.control.Either; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.cardano.foundation.voting.client.ChainFollowerClient; @@ -58,8 +57,8 @@ public class DefaultVoteService implements VoteService { @Override @Transactional(readOnly = true) - public List findAll(String eventId) { - return voteRepository.findAllByEventId(eventId); + public List findAllCompactVotesByEventId(String eventId) { + return voteRepository.findAllCompactVotesByEventId(eventId); } @Transactional(readOnly = true) diff --git a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/vote/VoteService.java b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/vote/VoteService.java index 54716a889..dbe3cf2a7 100644 --- a/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/vote/VoteService.java +++ b/backend-services/voting-app/src/main/java/org/cardano/foundation/voting/service/vote/VoteService.java @@ -3,7 +3,7 @@ import io.vavr.control.Either; import org.cardano.foundation.voting.domain.VoteReceipt; import org.cardano.foundation.voting.domain.entity.Vote; -import org.cardano.foundation.voting.domain.web3.SignedWeb3Request; +import org.cardano.foundation.voting.repository.VoteRepository; import org.cardano.foundation.voting.service.auth.jwt.JwtAuthenticationToken; import org.cardano.foundation.voting.service.auth.web3.Web3AuthenticationToken; import org.zalando.problem.Problem; @@ -12,10 +12,7 @@ public interface VoteService { - /** - * Return true if the slot is within permissible range - */ - List findAll(String eventId); + List findAllCompactVotesByEventId(String eventId); Either isVoteCastingStillPossible(String eventId, String voteId); diff --git a/backend-services/voting-app/src/main/resources/application.properties b/backend-services/voting-app/src/main/resources/application.properties index d2f3ea73e..5b4ed39e6 100644 --- a/backend-services/voting-app/src/main/resources/application.properties +++ b/backend-services/voting-app/src/main/resources/application.properties @@ -50,9 +50,9 @@ cardano.tx.submit.api.url=${TX_SUBMIT_URL:https://submit-api.pro.dandelion-prepr transaction.submission.timeout.minutes=${TRANSACTION_SUBMISSION_TIMEOUT_MINUTES:5} transaction.submission.sleep.seconds=${TRANSACTION_SUBMISSION_SLEEP_SECONDS:10} -# run every 15 minutes, in prod should be more like every 30 minutes -vote.commitment.cron.expression=0 */15 * * * ? -#vote.commitment.cron.expression=0 0/30 * * * ? +# run every 25 minutes (dev setup), in prod should be more like every 30 minutes +#vote.commitment.cron.expression=0 */15 * * * ? +vote.commitment.cron.expression=0 0/25 * * * ? vote.commitment.enabled=${VOTE_COMMITMENT_ENABLED:true} # value very specific to the network @@ -60,7 +60,7 @@ expiration.slot.buffer=${EXPIRATION_SLOT_BUFFER:300} server.port=9091 -spring.task.scheduling.pool.size=${SCHEDULING_POOL_SIZE:5} +spring.task.scheduling.pool.size=${SCHEDULING_POOL_SIZE:1} # must be false in production(!) leaderboard.force.results=${LEADERBOARD_FORCE_RESULTS:true} diff --git a/backend-services/voting-ledger-follower-app/build.gradle.kts b/backend-services/voting-ledger-follower-app/build.gradle.kts index f11c285de..80cdb1f9c 100644 --- a/backend-services/voting-ledger-follower-app/build.gradle.kts +++ b/backend-services/voting-ledger-follower-app/build.gradle.kts @@ -61,6 +61,7 @@ dependencies { implementation("com.querydsl:querydsl-jpa") annotationProcessor("com.querydsl:querydsl-apt") + implementation("com.bloxbean.cardano:cardano-client-crypto:0.5.0-beta3") implementation("com.bloxbean.cardano:cardano-client-backend-blockfrost:0.5.0-beta3") implementation("com.bloxbean.cardano:yaci-store-spring-boot-starter:0.0.12-beta3") diff --git a/backend-services/voting-ledger-follower-app/src/main/java/org/cardano/foundation/voting/service/account/DefaultAccountService.java b/backend-services/voting-ledger-follower-app/src/main/java/org/cardano/foundation/voting/service/account/DefaultAccountService.java index c84375152..a90ad9950 100644 --- a/backend-services/voting-ledger-follower-app/src/main/java/org/cardano/foundation/voting/service/account/DefaultAccountService.java +++ b/backend-services/voting-ledger-follower-app/src/main/java/org/cardano/foundation/voting/service/account/DefaultAccountService.java @@ -20,7 +20,6 @@ import static org.cardano.foundation.voting.domain.VotingEventType.BALANCE_BASED; import static org.cardano.foundation.voting.domain.VotingEventType.STAKE_BASED; import static org.zalando.problem.Status.BAD_REQUEST; - @Service @Slf4j @RequiredArgsConstructor diff --git a/backend-services/voting-verification-app/src/main/java/org/cardano/foundation/voting/domain/CoseWrappedVote.java b/backend-services/voting-verification-app/src/main/java/org/cardano/foundation/voting/domain/CoseWrappedVote.java index 28cbca2bd..1394b38ba 100644 --- a/backend-services/voting-verification-app/src/main/java/org/cardano/foundation/voting/domain/CoseWrappedVote.java +++ b/backend-services/voting-verification-app/src/main/java/org/cardano/foundation/voting/domain/CoseWrappedVote.java @@ -3,30 +3,17 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import org.cardanofoundation.cip30.CIP30Verifier; import java.util.Optional; -import java.util.function.Function; @Getter @Builder @AllArgsConstructor public class CoseWrappedVote { - public static final Function VOTE_SERIALISER = createSerialiserFunction(); - private String coseSignature; @Builder.Default private Optional cosePublicKey = Optional.empty(); - private static Function createSerialiserFunction() { - return vote -> { - var cip30Verifier = new CIP30Verifier(vote.getCoseSignature(), vote.getCosePublicKey()); - var verificationResult = cip30Verifier.verify(); - - return Optional.ofNullable(verificationResult.getMessage()).orElse(new byte[0]); - }; - } - } diff --git a/backend-services/voting-verification-app/src/main/java/org/cardano/foundation/voting/service/verify/VoteVerificationService.java b/backend-services/voting-verification-app/src/main/java/org/cardano/foundation/voting/service/verify/VoteVerificationService.java index 4507e442d..f8f042d7e 100644 --- a/backend-services/voting-verification-app/src/main/java/org/cardano/foundation/voting/service/verify/VoteVerificationService.java +++ b/backend-services/voting-verification-app/src/main/java/org/cardano/foundation/voting/service/verify/VoteVerificationService.java @@ -21,7 +21,7 @@ import static com.bloxbean.cardano.client.util.HexUtil.decodeHexString; import static com.bloxbean.cardano.client.util.JsonUtil.parseJson; -import static org.cardano.foundation.voting.domain.CoseWrappedVote.VOTE_SERIALISER; +import static org.cardano.foundation.voting.utils.VoteSerialisations.VOTE_SERIALISER; import static org.cardanofoundation.cip30.MessageFormat.TEXT; import static org.zalando.problem.Status.BAD_REQUEST; diff --git a/backend-services/voting-verification-app/src/main/java/org/cardano/foundation/voting/utils/VoteSerialisations.java b/backend-services/voting-verification-app/src/main/java/org/cardano/foundation/voting/utils/VoteSerialisations.java new file mode 100644 index 000000000..a841188f1 --- /dev/null +++ b/backend-services/voting-verification-app/src/main/java/org/cardano/foundation/voting/utils/VoteSerialisations.java @@ -0,0 +1,26 @@ +package org.cardano.foundation.voting.utils; + +import org.cardano.foundation.voting.domain.CoseWrappedVote; +import org.cardanofoundation.cip30.CIP30Verifier; + +import java.util.Optional; +import java.util.function.Function; + +import static com.bloxbean.cardano.client.crypto.Blake2bUtil.blake2bHash256; + +public final class VoteSerialisations { + + public static final Function VOTE_SERIALISER = createSerialiserFunction(); + + private static Function createSerialiserFunction() { + return vote -> { + var cip30Verifier = new CIP30Verifier(vote.getCoseSignature(), vote.getCosePublicKey()); + var verificationResult = cip30Verifier.verify(); + + var bytes = Optional.ofNullable(verificationResult.getMessage()).orElse(new byte[0]); + + return blake2bHash256(bytes); + }; + } + +} diff --git a/integration-tests/cip_1694_mass_vote.sc b/integration-tests/cip_1694_mass_vote.sc index a29ec3448..a6e38f7ea 100644 --- a/integration-tests/cip_1694_mass_vote.sc +++ b/integration-tests/cip_1694_mass_vote.sc @@ -140,7 +140,7 @@ def topUpAccount(newAcc: Account, amount: Int): Boolean = { } println("sleeping for 1 sec") - Thread.sleep(1000L) + Thread.sleep(1000) res } @@ -195,17 +195,14 @@ def castVote(acc: Account, amountAda: Int): Boolean = { acc.stakeHdKeyPair().getPublicKey().getKeyData() ); - val cip30JsonNode = mapper.createObjectNode() - cip30JsonNode.put("coseSignature", castCoteCIP30Result.signature()) - cip30JsonNode.put("cosePublicKey", castCoteCIP30Result.key()) - - val requestString = mapper.writerWithDefaultPrettyPrinter - .writeValueAsString(cip30JsonNode) - val r = requests.post( "http://localhost:9091/api/vote/cast", - headers = Map("Content-Type" -> "application/json"), - data = requestString + headers = Map( + "Content-Type" -> "application/json", + "X-CIP93-Signature" -> castCoteCIP30Result.signature(), + "X-CIP93-Public-Key" -> castCoteCIP30Result.key() + ), + data = "{ }" ) if (r.statusCode == 200) { @@ -436,16 +433,17 @@ def main(isAlreadyRegistered: Boolean = false, organiserAlreadyToppedUp: Boolean println("woke up...") - for (i <- 1 to 10000) { + for (i <- 1 to 25001) { try { val account = Account(Networks.testnet()) - val isPreloaded = topUpAccount(account, amountAda) + //val isPreloaded = topUpAccount(account, amountAda) + val isPreloaded = true if (isPreloaded) { castVote(account, amountAda) } } catch { - case e: Exception => println(s"Couldn't cast vote.") + case e: Exception => println("vote cast error, error:" + e.getMessage()) } } diff --git a/ui/cip-1694/public/static/Cardano_Ballot_black.png b/ui/cip-1694/public/static/Cardano_Ballot_black.png deleted file mode 100644 index 68e82a0d3..000000000 Binary files a/ui/cip-1694/public/static/Cardano_Ballot_black.png and /dev/null differ diff --git a/ui/cip-1694/public/static/brazil-cip-1694.jpeg b/ui/cip-1694/public/static/brazil-cip-1694.jpeg deleted file mode 100644 index 591325f9e..000000000 Binary files a/ui/cip-1694/public/static/brazil-cip-1694.jpeg and /dev/null differ diff --git a/ui/cip-1694/public/static/cardano-summit-hero.jpeg b/ui/cip-1694/public/static/cardano-summit-hero.jpeg deleted file mode 100644 index 7ae0e504a..000000000 Binary files a/ui/cip-1694/public/static/cardano-summit-hero.jpeg and /dev/null differ diff --git a/ui/cip-1694/public/static/ci-1694-tokyo.jpeg b/ui/cip-1694/public/static/ci-1694-tokyo.jpeg deleted file mode 100644 index 65da39572..000000000 Binary files a/ui/cip-1694/public/static/ci-1694-tokyo.jpeg and /dev/null differ diff --git a/ui/cip-1694/public/static/cip-1694-community-workshop.jpeg b/ui/cip-1694/public/static/cip-1694-community-workshop.jpeg deleted file mode 100644 index 5d89e05e2..000000000 Binary files a/ui/cip-1694/public/static/cip-1694-community-workshop.jpeg and /dev/null differ diff --git a/ui/cip-1694/public/static/cip-1694.jpg b/ui/cip-1694/public/static/cip-1694.jpg new file mode 100644 index 000000000..3129012c0 Binary files /dev/null and b/ui/cip-1694/public/static/cip-1694.jpg differ diff --git a/ui/cip-1694/src/common/api/leaderboardService.ts b/ui/cip-1694/src/common/api/leaderboardService.ts index 23c5df266..d0ed12c2d 100644 --- a/ui/cip-1694/src/common/api/leaderboardService.ts +++ b/ui/cip-1694/src/common/api/leaderboardService.ts @@ -1,10 +1,14 @@ -import { ByCategory } from 'types/voting-app-types'; +import { ByProposalsInCategoryStats } from 'types/voting-app-types'; import { DEFAULT_CONTENT_TYPE_HEADERS, doRequest, HttpMethods } from '../handlers/httpHandler'; import { env } from '../../env'; export const LEADERBOARD_URL = `${env.VOTING_APP_SERVER_URL}/api/leaderboard`; export const getStats = async () => - await doRequest(HttpMethods.GET, `${LEADERBOARD_URL}/${env.EVENT_ID}/${env.CATEGORY_ID}`, { - ...DEFAULT_CONTENT_TYPE_HEADERS, - }); + await doRequest( + HttpMethods.GET, + `${LEADERBOARD_URL}/${env.EVENT_ID}/${env.CATEGORY_ID}`, + { + ...DEFAULT_CONTENT_TYPE_HEADERS, + } + ); diff --git a/ui/cip-1694/src/common/api/loginService.ts b/ui/cip-1694/src/common/api/loginService.ts new file mode 100644 index 000000000..abe621196 --- /dev/null +++ b/ui/cip-1694/src/common/api/loginService.ts @@ -0,0 +1,36 @@ +import { canonicalize } from 'json-canonicalize'; +import { SignedWeb3Request } from 'types/voting-app-types'; +import { DEFAULT_CONTENT_TYPE_HEADERS, doRequest, HttpMethods } from '../handlers/httpHandler'; +import { env } from '../../env'; + +export const LOGIN_URL = `${env.VOTING_APP_SERVER_URL}/api/auth/login`; + +type LoginInput = { + stakeAddress: string; + slotNumber: string; +}; + +export const buildCanonicalLoginJson = ({ stakeAddress, slotNumber }: LoginInput): ReturnType => { + return canonicalize({ + action: 'LOGIN', + actionText: 'Login', + slot: slotNumber, + data: { + address: stakeAddress, + event: env.EVENT_ID, + network: env.TARGET_NETWORK, + role: 'VOTER', + }, + }); +}; + +export const submitLogin = async (jsonRequest: SignedWeb3Request) => { + return await doRequest<{ accessToken: string; expiresAt: string }>( + HttpMethods.GET, + LOGIN_URL, + DEFAULT_CONTENT_TYPE_HEADERS, + JSON.stringify(jsonRequest), + undefined, + true + ); +}; diff --git a/ui/cip-1694/src/common/api/voteService.ts b/ui/cip-1694/src/common/api/voteService.ts index 8cebf3274..ffd53bc54 100644 --- a/ui/cip-1694/src/common/api/voteService.ts +++ b/ui/cip-1694/src/common/api/voteService.ts @@ -13,21 +13,25 @@ export const castAVoteWithDigitalSignature = async (jsonRequest: SignedWeb3Reque HttpMethods.POST, CAST_VOTE_URL, DEFAULT_CONTENT_TYPE_HEADERS, - JSON.stringify(jsonRequest) + JSON.stringify(jsonRequest), + undefined, + true ); -export const getSlotNumber = async () => { +export const getChainTip = async () => { return await doRequest(HttpMethods.GET, BLOCKCHAIN_TIP_URL, DEFAULT_CONTENT_TYPE_HEADERS); }; -export const getVoteReceipt = async (jsonRequest: SignedWeb3Request) => { - return await doRequest( - HttpMethods.POST, - VOTE_RECEIPT_URL, - DEFAULT_CONTENT_TYPE_HEADERS, - JSON.stringify(jsonRequest) +export const getVoteReceipt = async (categoryId: string, token: string) => + await doRequest( + HttpMethods.GET, + `${VOTE_RECEIPT_URL}/${env.EVENT_ID}/${categoryId}`, + { + ...DEFAULT_CONTENT_TYPE_HEADERS, + }, + null, + token ); -}; export const getVotingPower = async (eventId: EventPresentation['id'], stakeAddress: string) => { return await doRequest( diff --git a/ui/cip-1694/src/common/handlers/httpHandler.ts b/ui/cip-1694/src/common/handlers/httpHandler.ts index 4235b8782..74df294f2 100644 --- a/ui/cip-1694/src/common/handlers/httpHandler.ts +++ b/ui/cip-1694/src/common/handlers/httpHandler.ts @@ -20,9 +20,11 @@ export enum Headers { CARDANO_BALLOT_TRACE_ID = 'X-Cardano-Ballot-Trace-ID', AUTHORIZATION = 'Authorization', ACCEPT = 'Accept', + X_CIP93_Signature = 'X-CIP93-Signature', + X_CIP93_Public_Key = 'X-CIP93-Public-Key', } -type contentTypeHeaders = Record; +type contentTypeHeaders = Record; export const DEFAULT_CONTENT_TYPE_HEADERS: Partial = { [Headers.CONTENT_TYPE]: MediaTypes.APPLICATION_JSON_UTF8, @@ -194,9 +196,21 @@ export const doRequest = async ( method: HttpMethods, url: string, headers: Partial, - body?: string + body?: string, + token?: string, + bodyInHeader?: boolean ) => { - const allHeaders = headers || DEFAULT_CONTENT_TYPE_HEADERS; + const allHeaders = { ...headers, ...DEFAULT_CONTENT_TYPE_HEADERS }; + + if (body && bodyInHeader) { + allHeaders['X-CIP93-Signature'] = JSON.parse(body).coseSignature; + allHeaders['X-CIP93-Public-Key'] = JSON.parse(body).cosePublicKey; + body = undefined; + } + + if (token) { + allHeaders['Authorization'] = `Bearer ${token}`; + } if (method === HttpMethods.POST) { return await post(url, allHeaders, body); diff --git a/ui/cip-1694/src/common/store/types.ts b/ui/cip-1694/src/common/store/types.ts index f712220c6..5b8d74c4f 100644 --- a/ui/cip-1694/src/common/store/types.ts +++ b/ui/cip-1694/src/common/store/types.ts @@ -1,14 +1,11 @@ -import { VoteReceipt } from 'types/voting-app-types'; -import { EventPresentation } from 'types/voting-ledger-follower-types'; +import { ChainTip, EventPresentation } from 'types/voting-ledger-follower-types'; export interface UserState { isConnectWalletModalVisible: boolean; isVoteSubmittedModalVisible: boolean; connectedWallet: string; - isReceiptFetched: boolean; - receipt: VoteReceipt | null; - proposal: VoteReceipt['proposal']; event: EventPresentation; + tip: ChainTip; } export interface State { diff --git a/ui/cip-1694/src/common/store/userSlice.ts b/ui/cip-1694/src/common/store/userSlice.ts index d1ea69e77..6a7bb8d46 100644 --- a/ui/cip-1694/src/common/store/userSlice.ts +++ b/ui/cip-1694/src/common/store/userSlice.ts @@ -1,17 +1,14 @@ import { createSlice } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit'; -import { VoteReceipt } from 'types/voting-app-types'; -import { EventPresentation } from 'types/voting-ledger-follower-types'; +import { ChainTip, EventPresentation } from 'types/voting-ledger-follower-types'; import { UserState } from './types'; const initialState: UserState = { isConnectWalletModalVisible: false, isVoteSubmittedModalVisible: false, connectedWallet: '', - isReceiptFetched: false, - receipt: null, - proposal: null, event: null, + tip: null, }; export const userSlice = createSlice({ @@ -27,18 +24,12 @@ export const userSlice = createSlice({ setConnectedWallet: (state, action: PayloadAction<{ wallet: string }>) => { state.connectedWallet = action.payload.wallet; }, - setVoteReceipt: (state, action: PayloadAction<{ receipt: VoteReceipt }>) => { - state.receipt = action.payload.receipt; - }, - setIsReceiptFetched: (state, action: PayloadAction<{ isFetched: boolean }>) => { - state.isReceiptFetched = action.payload.isFetched; - }, - setSelectedProposal: (state, action: PayloadAction<{ proposal: VoteReceipt['proposal'] }>) => { - state.proposal = action.payload.proposal; - }, setEventData: (state, action: PayloadAction<{ event: EventPresentation }>) => { state.event = action.payload.event; }, + setChainTipData: (state, action: PayloadAction<{ tip: ChainTip }>) => { + state.tip = action.payload.tip; + }, }, }); @@ -46,9 +37,7 @@ export const { setIsConnectWalletModalVisible, setIsVoteSubmittedModalVisible, setConnectedWallet, - setVoteReceipt, - setIsReceiptFetched, - setSelectedProposal, setEventData, + setChainTipData, } = userSlice.actions; export default userSlice.reducer; diff --git a/ui/cip-1694/src/common/utils/__tests__/dateUtils.test.ts b/ui/cip-1694/src/common/utils/__tests__/dateUtils.test.ts index 46fbaa4d2..1b6d1c0fd 100644 --- a/ui/cip-1694/src/common/utils/__tests__/dateUtils.test.ts +++ b/ui/cip-1694/src/common/utils/__tests__/dateUtils.test.ts @@ -3,9 +3,9 @@ import { formatUTCDate, getDateAndMonth } from '../dateUtils'; describe('dateUtils: ', () => { test('formatUTCDate', () => { - expect(formatUTCDate(eventMock_active.eventStartDate.toString())).toEqual('2023-07-06 00:00 UTC'); + expect(formatUTCDate(eventMock_active.eventStartDate.toString())).toEqual('2023-09-14 00:00 UTC'); }); test('getDateAndMonth', () => { - expect(getDateAndMonth(eventMock_active.eventStartDate.toString())).toEqual('6 July'); + expect(getDateAndMonth(eventMock_active.eventStartDate.toString())).toEqual('14 September'); }); }); diff --git a/ui/cip-1694/src/common/utils/__tests__/voteUtils.test.ts b/ui/cip-1694/src/common/utils/__tests__/voteUtils.test.ts index 931885936..ded8e261d 100644 --- a/ui/cip-1694/src/common/utils/__tests__/voteUtils.test.ts +++ b/ui/cip-1694/src/common/utils/__tests__/voteUtils.test.ts @@ -1,7 +1,7 @@ /* eslint-disable no-var */ var mockv4 = jest.fn(); import { accountDataMock, chainTipMock } from 'test/mocks'; -import { buildCanonicalVoteInputJson, buildCanonicalVoteReceiptInputJson } from '../voteUtils'; +import { buildCanonicalVoteInputJson } from '../voteUtils'; jest.mock('uuid', () => ({ v4: mockv4, @@ -13,8 +13,8 @@ jest.mock('../../../env', () => { ...original, env: { ...original.env, - CATEGORY_ID: 'CIP-1694_Pre_Ratification_4619', - EVENT_ID: 'CIP-1694_Pre_Ratification_4619', + CATEGORY_ID: 'CHANGE_GOV_STRUCTURE', + EVENT_ID: 'CIP-1694_Pre_Ratification_3316', TARGET_NETWORK: 'PREVIEW', }, }; @@ -31,19 +31,10 @@ describe('voteUtils: ', () => { voteId: mockv4Value, slotNumber: chainTipMock.absoluteSlot.toString(), votePower: accountDataMock.votingPower, + category: 'CHANGE_GOV_STRUCTURE', }) ).toEqual( - '{"action":"CAST_VOTE","actionText":"Cast Vote","data":{"address":"stake_test1uqwcz0754wwpuhm6xhdpda6u9enyahaj5ynlc9ay5l4mlms4pyqyg","category":"CIP-1694_Pre_Ratification_4619","event":"CIP-1694_Pre_Ratification_4619","id":"mockv4","network":"PREVIEW","proposal":"YES","votedAt":"36004360","votingPower":"9997463457"},"slot":"36004360","uri":"https://evoting.cardano.org/voltaire"}' - ); - }); - test('buildCanonicalVoteReceiptInputJson', () => { - expect( - buildCanonicalVoteReceiptInputJson({ - voter: accountDataMock.stakeAddress, - slotNumber: chainTipMock.absoluteSlot.toString(), - }) - ).toEqual( - '{"action":"VIEW_VOTE_RECEIPT","actionText":"View Vote Receipt","data":{"address":"stake_test1uqwcz0754wwpuhm6xhdpda6u9enyahaj5ynlc9ay5l4mlms4pyqyg","category":"CIP-1694_Pre_Ratification_4619","event":"CIP-1694_Pre_Ratification_4619","network":"PREVIEW"},"slot":"36004360","uri":"https://evoting.cardano.org/voltaire"}' + '{"action":"CAST_VOTE","actionText":"Cast Vote","data":{"address":"stake_test1uqwcz0754wwpuhm6xhdpda6u9enyahaj5ynlc9ay5l4mlms4pyqyg","category":"CHANGE_GOV_STRUCTURE","event":"CIP-1694_Pre_Ratification_3316","id":"mockv4","network":"PREVIEW","proposal":"YES","votedAt":"36004360","votingPower":"9997463457"},"slot":"36004360","uri":"https://evoting.cardano.org/voltaire"}' ); }); diff --git a/ui/cip-1694/src/common/utils/session.ts b/ui/cip-1694/src/common/utils/session.ts new file mode 100644 index 000000000..7958b3ad6 --- /dev/null +++ b/ui/cip-1694/src/common/utils/session.ts @@ -0,0 +1,22 @@ +const USER_SESSION_KEY = 'userInSession'; + +const saveUserInSession = (session: { accessToken: string; expiresAt: string }) => + sessionStorage.setItem(USER_SESSION_KEY, JSON.stringify(session)); + +const getUserInSession = () => { + const json = sessionStorage.getItem(USER_SESSION_KEY); + return JSON.parse(json); +}; + +const clearUserInSessionStorage = () => { + sessionStorage.removeItem(USER_SESSION_KEY); + sessionStorage.clear(); +}; + +const tokenIsExpired = (expiresAt: string) => { + const currentDate = new Date(); + const givenDate = new Date(expiresAt); + return givenDate < currentDate; +}; + +export { saveUserInSession, getUserInSession, clearUserInSessionStorage, tokenIsExpired }; diff --git a/ui/cip-1694/src/common/utils/voteUtils.ts b/ui/cip-1694/src/common/utils/voteUtils.ts index 3fb4838e0..6b61b69e7 100644 --- a/ui/cip-1694/src/common/utils/voteUtils.ts +++ b/ui/cip-1694/src/common/utils/voteUtils.ts @@ -9,6 +9,7 @@ type voteInput = { voter: string; slotNumber: string; votePower: string; + category: string; }; export const buildCanonicalVoteInputJson = ({ @@ -17,6 +18,7 @@ export const buildCanonicalVoteInputJson = ({ voter, slotNumber, votePower, + category, }: voteInput): ReturnType => { const startOfCurrentDay = new Date(); startOfCurrentDay.setUTCMinutes(0, 0, 0); @@ -30,7 +32,7 @@ export const buildCanonicalVoteInputJson = ({ id: voteId, address: voter, event: env.EVENT_ID, - category: env.CATEGORY_ID, + category, proposal: option, network: env.TARGET_NETWORK, votedAt: slotNumber, @@ -39,29 +41,6 @@ export const buildCanonicalVoteInputJson = ({ }); }; -type votereceiptInput = { - voter: string; - slotNumber: string; -}; - -export const buildCanonicalVoteReceiptInputJson = ({ - voter, - slotNumber, -}: votereceiptInput): ReturnType => - canonicalize({ - // TODO: should this one be hardcoded? - uri: 'https://evoting.cardano.org/voltaire', - action: 'VIEW_VOTE_RECEIPT', - actionText: 'View Vote Receipt', - slot: slotNumber, - data: { - address: voter, - event: env.EVENT_ID, - category: env.CATEGORY_ID, - network: env.TARGET_NETWORK, - }, - }); - export const getSignedMessagePromise = (signMessage: ReturnType['signMessage']) => { return async (message: string): Promise => new Promise((resolve, reject) => { diff --git a/ui/cip-1694/src/components/ConnectWalletModal/__tests__/ConnectWalletModal.test.tsx b/ui/cip-1694/src/components/ConnectWalletModal/__tests__/ConnectWalletModal.test.tsx index b6a54d5fd..e89dd9093 100644 --- a/ui/cip-1694/src/components/ConnectWalletModal/__tests__/ConnectWalletModal.test.tsx +++ b/ui/cip-1694/src/components/ConnectWalletModal/__tests__/ConnectWalletModal.test.tsx @@ -14,8 +14,8 @@ jest.mock('../../../env', () => { ...original, env: { ...original.env, - CATEGORY_ID: 'CIP-1694_Pre_Ratification_4619', - EVENT_ID: 'CIP-1694_Pre_Ratification_4619', + CATEGORY_ID: 'CHANGE_GOV_STRUCTURE', + EVENT_ID: 'CIP-1694_Pre_Ratification_3316', SUPPORTED_WALLETS: mockSupportedWallets, }, }; diff --git a/ui/cip-1694/src/components/common/Content/__tests__/Content.test.tsx b/ui/cip-1694/src/components/common/Content/__tests__/Content.test.tsx index 0ae19d8c5..862453c38 100644 --- a/ui/cip-1694/src/components/common/Content/__tests__/Content.test.tsx +++ b/ui/cip-1694/src/components/common/Content/__tests__/Content.test.tsx @@ -36,8 +36,8 @@ jest.mock('../../../../env', () => { ...original, env: { ...original.env, - CATEGORY_ID: 'CIP-1694_Pre_Ratification_4619', - EVENT_ID: 'CIP-1694_Pre_Ratification_4619', + CATEGORY_ID: 'CHANGE_GOV_STRUCTURE', + EVENT_ID: 'CIP-1694_Pre_Ratification_3316', SUPPORTED_WALLETS: mockSupportedWallets, }, }; diff --git a/ui/cip-1694/src/components/common/Header/Header.tsx b/ui/cip-1694/src/components/common/Header/Header.tsx index b4ecaa8ee..ce8ba3170 100644 --- a/ui/cip-1694/src/components/common/Header/Header.tsx +++ b/ui/cip-1694/src/components/common/Header/Header.tsx @@ -49,7 +49,7 @@ export const Header = () => { onClick={handleLogoClick} className={styles.logo} > - CIP-1694 Ratification + Referendum on Governance diff --git a/ui/cip-1694/src/components/common/Header/__tests__/Header.test.tsx b/ui/cip-1694/src/components/common/Header/__tests__/Header.test.tsx index ad66183c7..327782673 100644 --- a/ui/cip-1694/src/components/common/Header/__tests__/Header.test.tsx +++ b/ui/cip-1694/src/components/common/Header/__tests__/Header.test.tsx @@ -1,15 +1,20 @@ /* eslint-disable no-var */ /* eslint-disable @typescript-eslint/no-explicit-any */ var mockUseCardano = jest.fn(); +var mockToast = jest.fn(); +var mockGetChainTip = jest.fn(); +var mockSupportedWallets = ['Wallet1', 'Wallet2']; import '@testing-library/jest-dom'; import React from 'react'; import { expect } from '@jest/globals'; +import BlockIcon from '@mui/icons-material/Block'; import { screen, within, waitFor, fireEvent, cleanup } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import { UserState } from 'common/store/types'; import { ROUTES } from 'common/routes'; import { renderWithProviders } from 'test/mockProviders'; import { + chainTipMock, eventMock_active, eventMock_finished, eventMock_notStarted, @@ -19,6 +24,7 @@ import { import { CustomRouter } from 'test/CustomRouter'; import { formatUTCDate } from 'pages/Leaderboard/utils'; import { getDateAndMonth } from 'common/utils/dateUtils'; +import { Toast } from 'components/common/Toast/Toast'; import { Header } from '../Header'; jest.mock('@cardano-foundation/cardano-connect-with-wallet', () => ({ @@ -35,9 +41,32 @@ jest.mock('@cardano-foundation/cardano-connect-with-wallet', () => ({ jest.mock('swiper/react', () => ({})); jest.mock('swiper', () => ({})); +jest.mock('common/api/voteService', () => ({ + ...jest.requireActual('common/api/voteService'), + getChainTip: mockGetChainTip, +})); + +jest.mock('react-hot-toast', () => ({ + __esModule: true, + ...jest.requireActual('react-hot-toast'), + default: mockToast, +})); + +jest.mock('../../../../env', () => { + const original = jest.requireActual('../../../../env'); + return { + ...original, + env: { + ...original.env, + SUPPORTED_WALLETS: mockSupportedWallets, + }, + }; +}); + describe('For ongoing event:', () => { beforeEach(() => { mockUseCardano.mockReturnValue(useCardanoMock); + mockGetChainTip.mockReturnValue(chainTipMock); }); afterEach(() => { jest.clearAllMocks(); @@ -50,7 +79,7 @@ describe('For ongoing event:', () => {
, - { preloadedState: { user: { event: eventMock_active } as UserState } } + { preloadedState: { user: { event: eventMock_active, tip: chainTipMock } as UserState } } ); await waitFor(async () => { @@ -59,7 +88,7 @@ describe('For ongoing event:', () => { const headerLogo = await within(header).queryByTestId('header-logo'); expect(headerLogo).not.toBeNull(); - expect(headerLogo.textContent).toEqual('CIP-1694 Ratification'); + expect(headerLogo.textContent).toEqual('Referendum on Governance'); const leaderboardLink = await within(header).queryByTestId('leaderboard-link'); expect(leaderboardLink).not.toBeNull(); @@ -68,6 +97,8 @@ describe('For ongoing event:', () => { const voteLink = await within(header).queryByTestId('vote-link'); expect(voteLink).not.toBeNull(); expect(voteLink.textContent).toEqual('Your vote'); + + expect(mockGetChainTip).toHaveBeenCalledTimes(1); }); }); @@ -79,13 +110,13 @@ describe('For ongoing event:', () => {
, - { preloadedState: { user: { event: eventMock_active } as UserState } } + { preloadedState: { user: { event: eventMock_active, tip: chainTipMock } as UserState } } ); await waitFor(async () => { const header = await screen.queryByTestId('header'); const headerLogo = await within(header).queryByTestId('header-logo'); - expect(headerLogo.textContent).toEqual('CIP-1694 Ratification'); + expect(headerLogo.textContent).toEqual('Referendum on Governance'); const voteLink = await within(header).queryByTestId('vote-link'); expect(voteLink.textContent).toEqual('Your vote'); @@ -107,7 +138,7 @@ describe('For ongoing event:', () => {
, - { preloadedState: { user: { event: eventMock_active } as UserState } } + { preloadedState: { user: { event: eventMock_active, tip: chainTipMock } as UserState } } ); await waitFor(async () => { @@ -130,7 +161,7 @@ describe('For ongoing event:', () => {
, - { preloadedState: { user: { event: eventMock_active } as UserState } } + { preloadedState: { user: { event: eventMock_active, tip: chainTipMock } as UserState } } ); await waitFor(async () => { @@ -153,7 +184,7 @@ describe('For ongoing event:', () => {
, - { preloadedState: { user: { event: eventMock_active } as UserState } } + { preloadedState: { user: { event: eventMock_active, tip: chainTipMock } as UserState } } ); const header = await screen.findByTestId('header'); @@ -185,7 +216,7 @@ describe('For ongoing event:', () => {
, - { preloadedState: { user: { event: eventMock_active } as UserState } } + { preloadedState: { user: { event: eventMock_active, tip: chainTipMock } as UserState } } ); const header = await screen.findByTestId('header'); @@ -203,21 +234,102 @@ describe('For ongoing event:', () => { 'View leaderboard anyway' ); - fireEvent.click(within(confirmModal).queryByTestId('result-comming-soon-modal-close-cta')); - expect(within(confirmModal).queryByTestId('result-comming-soon-modal-description')).toHaveTextContent( - `The results will be displayed after the voting has closed on ${getDateAndMonth( - eventMock_active?.eventEndDate?.toString() - )} ${formatUTCDate(eventMock_active?.eventEndDate?.toString())}` + `The results will be available from ${getDateAndMonth( + eventMock_active?.proposalsRevealDate?.toString() + )} ${formatUTCDate(eventMock_active?.proposalsRevealDate?.toString())}` ); expect(within(confirmModal).queryByTestId('result-comming-soon-modal-title')).toHaveTextContent('Coming soon'); + fireEvent.click(within(confirmModal).queryByTestId('result-comming-soon-modal-close-cta')); + await waitFor(() => { expect(screen.queryByTestId('result-comming-soon-modal')).toBeNull(); }); expect(historyPushSpy.mock.lastCall).toBeUndefined(); }); + + test('should show confirmation modal and discard redirection to leadeboard page on close icon click', async () => { + mockUseCardano.mockReset(); + mockUseCardano.mockReturnValue(useCardanoMock); + + const history = createMemoryHistory({ initialEntries: [ROUTES.INTRO] }); + + const historyPushSpy = jest.spyOn(history, 'push'); + renderWithProviders( + +
+ , + { preloadedState: { user: { event: eventMock_active, tip: chainTipMock } as UserState } } + ); + + const header = await screen.findByTestId('header'); + + const leaderboardLink = within(header).queryByTestId('leaderboard-link'); + expect(screen.queryByTestId('result-comming-soon-modal')).toBeNull(); + + fireEvent.click(leaderboardLink); + + const confirmModal = screen.queryByTestId('result-comming-soon-modal'); + expect(confirmModal).not.toBeNull(); + + fireEvent.click(within(confirmModal).queryByTestId('result-comming-soon-modal-close-icon')); + + await waitFor(() => { + expect(screen.queryByTestId('result-comming-soon-modal')).toBeNull(); + }); + + expect(historyPushSpy.mock.lastCall).toBeUndefined(); + }); + + test('should have leaderboard link disabled if there is no tip fetched', async () => { + mockUseCardano.mockReset(); + mockUseCardano.mockReturnValue(useCardanoMock_notConnected); + + const history = createMemoryHistory({ initialEntries: [ROUTES.INTRO] }); + + renderWithProviders( + +
+ , + { preloadedState: { user: { event: eventMock_active } as UserState } } + ); + + const header = await screen.findByTestId('header'); + + const leaderboardLink = within(header).queryByTestId('leaderboard-link'); + expect(leaderboardLink.closest('button')).toHaveAttribute('disabled'); + expect(screen.queryByTestId('result-comming-soon-modal')).toBeNull(); + + fireEvent.click(leaderboardLink); + + const confirmModal = screen.queryByTestId('result-comming-soon-modal'); + expect(confirmModal).toBeNull(); + expect(mockGetChainTip).not.toHaveBeenCalled(); + }); + + test('should display toast if fetch chain tip request failed', async () => { + mockGetChainTip.mockImplementation(async () => await Promise.reject('error')); + + const history = createMemoryHistory({ initialEntries: [ROUTES.INTRO] }); + renderWithProviders( + +
+ , + { preloadedState: { user: { event: eventMock_active } as UserState } } + ); + + await waitFor(async () => { + expect(mockToast).toBeCalledWith( + } + /> + ); + }); + }); }); describe("For the event that hasn't started yet", () => { @@ -305,7 +417,14 @@ describe('For the event that has already finished', () => {
, - { preloadedState: { user: { event: eventMock_finished } as UserState } } + { + preloadedState: { + user: { + event: eventMock_finished, + tip: { ...chainTipMock, epochNo: eventMock_finished.proposalsRevealEpoch }, + } as UserState, + }, + } ); await waitFor(async () => { diff --git a/ui/cip-1694/src/components/common/Header/components/ConnectWalletButton.tsx b/ui/cip-1694/src/components/common/Header/components/ConnectWalletButton.tsx index dbb3f1399..ba68df125 100644 --- a/ui/cip-1694/src/components/common/Header/components/ConnectWalletButton.tsx +++ b/ui/cip-1694/src/components/common/Header/components/ConnectWalletButton.tsx @@ -12,12 +12,7 @@ import { ConnectWalletButton as CFConnectWalletButton, getWalletIcon, } from '@cardano-foundation/cardano-connect-with-wallet'; -import { - setConnectedWallet, - setIsConnectWalletModalVisible, - setIsReceiptFetched, - setVoteReceipt, -} from 'common/store/userSlice'; +import { setConnectedWallet, setIsConnectWalletModalVisible } from 'common/store/userSlice'; import { Toast } from 'components/common/Toast/Toast'; import { RootState } from 'common/store'; import styles from './ConnectWalletButton.module.scss'; @@ -60,8 +55,6 @@ export const ConnectWalletButton = ({ isMobileMenu = false }) => { const onDisconnectWallet = () => { disconnect(); dispatch(setConnectedWallet({ wallet: '' })); - dispatch(setVoteReceipt({ receipt: null })); - dispatch(setIsReceiptFetched({ isFetched: false })); }; return !connectedWallet ? ( diff --git a/ui/cip-1694/src/components/common/Header/components/HeaderActions.tsx b/ui/cip-1694/src/components/common/Header/components/HeaderActions.tsx index 12e2a42df..d8f8d5fe4 100644 --- a/ui/cip-1694/src/components/common/Header/components/HeaderActions.tsx +++ b/ui/cip-1694/src/components/common/Header/components/HeaderActions.tsx @@ -1,15 +1,21 @@ -import React, { useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { Link, matchPath, useLocation, useNavigate } from 'react-router-dom'; import cn from 'classnames'; +import { useDispatch, useSelector } from 'react-redux'; +import toast from 'react-hot-toast'; import { Grid, Typography, Button } from '@mui/material'; import CheckBoxOutlinedIcon from '@mui/icons-material/CheckBoxOutlined'; +import BlockIcon from '@mui/icons-material/Block'; import LeaderboardIcon from '@mui/icons-material/Leaderboard'; import { ROUTES } from 'common/routes'; -import { ResultsCommingSoonModal } from 'pages/Leaderboard/components/ResultsCommingSoonModal/ResultsCommingSoonModal'; -import { useSelector } from 'react-redux'; +import { useCardano } from '@cardano-foundation/cardano-connect-with-wallet'; import { RootState } from 'common/store'; -import { formatUTCDate } from 'pages/Leaderboard/utils'; import { getDateAndMonth } from 'common/utils/dateUtils'; +import * as voteService from 'common/api/voteService'; +import { setChainTipData } from 'common/store/userSlice'; +import { Toast } from 'components/common/Toast/Toast'; +import { ResultsCommingSoonModal } from 'pages/Leaderboard/components/ResultsCommingSoonModal/ResultsCommingSoonModal'; +import { formatUTCDate } from 'pages/Leaderboard/utils'; import { ConnectWalletButton } from './ConnectWalletButton'; import styles from './HeaderActions.module.scss'; @@ -20,12 +26,37 @@ type HeaderActionsProps = { }; export const HeaderActions = ({ isMobileMenu = false, onClick, showNavigationItems }: HeaderActionsProps) => { + const { isConnected } = useCardano(); const location = useLocation(); const navigate = useNavigate(); + const dispatch = useDispatch(); const event = useSelector((state: RootState) => state.user.event); + const tip = useSelector((state: RootState) => state.user.tip); const [isCommingSoonModalVisible, setIsCommingSoonModalVisible] = useState(false); + const init = useCallback(async () => { + try { + dispatch(setChainTipData({ tip: await voteService.getChainTip() })); + } catch (error) { + const message = `Failed to fecth chain tip: ${error?.message}`; + console.log(message); + toast( + } + /> + ); + } + }, [dispatch]); + + useEffect(() => { + if (isConnected) { + init(); + } + }, [init, isConnected]); + const goToLeaderboard = () => { navigate(ROUTES.LEADERBOARD); setIsCommingSoonModalVisible(false); @@ -33,7 +64,7 @@ export const HeaderActions = ({ isMobileMenu = false, onClick, showNavigationIte }; const onGoToLeaderboard = () => { - if (event?.finished === false) { + if (event?.proposalsRevealEpoch > tip?.epochNo) { setIsCommingSoonModalVisible(true); } else { navigate(ROUTES.LEADERBOARD); @@ -81,7 +112,7 @@ export const HeaderActions = ({ isMobileMenu = false, onClick, showNavigationIte [styles.activeRoute]: !!matchPath(location?.pathname, ROUTES.LEADERBOARD), })} startIcon={} - disabled={!event} + disabled={!event || !tip?.epochNo} > Leaderboard @@ -102,7 +133,11 @@ export const HeaderActions = ({ isMobileMenu = false, onClick, showNavigationIte goToLeaderboard()} + onCloseFn={() => { + setIsCommingSoonModalVisible(false); + onClick?.(); + }} + onConfirmFn={() => goToLeaderboard()} onGoBackFn={() => { setIsCommingSoonModalVisible(false); onClick?.(); @@ -112,10 +147,10 @@ export const HeaderActions = ({ isMobileMenu = false, onClick, showNavigationIte title="Coming soon" description={ <> - The results will be displayed after the voting has closed on{' '} + The results will be available from{' '} - {event?.eventStartDate && getDateAndMonth(event?.eventEndDate?.toString())}{' '} - {formatUTCDate(event?.eventEndDate?.toString())} + {event?.proposalsRevealDate && getDateAndMonth(event?.proposalsRevealDate?.toString())}{' '} + {formatUTCDate(event?.proposalsRevealDate?.toString())} } diff --git a/ui/cip-1694/src/pages/Introduction/Introduction.tsx b/ui/cip-1694/src/pages/Introduction/Introduction.tsx index 849fe358e..ffeb78ef9 100644 --- a/ui/cip-1694/src/pages/Introduction/Introduction.tsx +++ b/ui/cip-1694/src/pages/Introduction/Introduction.tsx @@ -5,10 +5,10 @@ import './Introduction.scss'; export const introItems: SlideItem[] = [ { - image: '/static/cip-1694-community-workshop.jpeg', - title: 'What is CIP-1694 voting?', + image: '/static/cip-1694.jpg', + title: 'A Vote on Minimum-Viable Governance', description: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magnaaliqua. Sit amet justo donec enim diam vulputate.', + 'Cardano has reached an incredible milestone. After six years of initial development and feature cultivation, the Cardano blockchain has reached the age of Voltaire. Guided by a principles-first approach and led by the community, this new age of Cardano advances inclusive accountability for all participants in the ecosystem. The time has come for a vote by the community on the way forward.', }, ]; diff --git a/ui/cip-1694/src/pages/Leaderboard/Leaderboard.tsx b/ui/cip-1694/src/pages/Leaderboard/Leaderboard.tsx index d11211efe..427ff883a 100644 --- a/ui/cip-1694/src/pages/Leaderboard/Leaderboard.tsx +++ b/ui/cip-1694/src/pages/Leaderboard/Leaderboard.tsx @@ -7,7 +7,7 @@ import { PieChart } from 'react-minimal-pie-chart'; import { Grid, Typography } from '@mui/material'; import BlockIcon from '@mui/icons-material/Block'; import { useCardano } from '@cardano-foundation/cardano-connect-with-wallet'; -import { ByCategory } from 'types/voting-app-types'; +import { ByProposalsInCategoryStats } from 'types/voting-app-types'; import { ProposalPresentation } from 'types/voting-ledger-follower-types'; import { RootState } from 'common/store'; import * as leaderboardService from 'common/api/leaderboardService'; @@ -21,7 +21,7 @@ import { StatItem } from './types'; export const Leaderboard = () => { const { isConnected } = useCardano(); const event = useSelector((state: RootState) => state.user.event); - const [stats, setStats] = useState(); + const [stats, setStats] = useState(); const init = useCallback(async () => { try { diff --git a/ui/cip-1694/src/pages/Leaderboard/__tests__/Leaderboard.test.tsx b/ui/cip-1694/src/pages/Leaderboard/__tests__/Leaderboard.test.tsx index f2d444a6b..8d7dfcc5f 100644 --- a/ui/cip-1694/src/pages/Leaderboard/__tests__/Leaderboard.test.tsx +++ b/ui/cip-1694/src/pages/Leaderboard/__tests__/Leaderboard.test.tsx @@ -16,7 +16,7 @@ import { UserState } from 'common/store/types'; import { renderWithProviders } from 'test/mockProviders'; import { useCardanoMock, eventMock_finished, voteStats, eventMock_active } from 'test/mocks'; import { CustomRouter } from 'test/CustomRouter'; -import { ByCategory } from 'types/voting-app-types'; +import { ByProposalsInCategoryStats } from 'types/voting-app-types'; import { Leaderboard } from '../Leaderboard'; import { proposalColorsMap, getPercentage } from '../utils'; @@ -56,8 +56,8 @@ jest.mock('../../../env', () => { ...original, env: { ...original.env, - CATEGORY_ID: 'CIP-1694_Pre_Ratification_4619', - EVENT_ID: 'CIP-1694_Pre_Ratification_4619', + CATEGORY_ID: 'CHANGE_GOV_STRUCTURE', + EVENT_ID: 'CIP-1694_Pre_Ratification_3316', }, }; }); @@ -92,7 +92,7 @@ describe('For the event that has already finished', () => { const statsSum = Object.values(voteStats.proposals)?.reduce((acc, { votes }) => (acc += votes), 0); const statsItems = eventMock_finished?.categories - ?.find(({ id }) => id === 'CIP-1694_Pre_Ratification_4619') + ?.find(({ id }) => id === 'CHANGE_GOV_STRUCTURE') ?.proposals?.map(({ name }) => ({ name, label: capitalize(name.toLowerCase()), @@ -118,9 +118,11 @@ describe('For the event that has already finished', () => { expect(pollStatsTileSummary.textContent).toEqual(`${statsSum}`); const pollStatsItems = await within(pollStatsTile).queryAllByTestId('poll-stats-item'); - expect(pollStatsItems[0].textContent).toEqual(`${capitalize(stats[0][0].toLowerCase())}${stats[0][1].votes}`); - expect(pollStatsItems[1].textContent).toEqual(`${capitalize(stats[1][0].toLowerCase())}${stats[1][1].votes}`); - expect(pollStatsItems[2].textContent).toEqual(`${capitalize(stats[2][0].toLowerCase())}${stats[2][1].votes}`); + for (const item in pollStatsItems) { + expect(pollStatsItems[item].textContent).toEqual( + `${capitalize(stats[item][0].toLowerCase())}${stats[item][1].votes}` + ); + } const currentlyVotingTile = await within(leaderboardPage).queryByTestId('currently-voting-tile'); expect(currentlyVotingTile).not.toBeNull(); @@ -134,24 +136,14 @@ describe('For the event that has already finished', () => { expect(currentlyVotingTileSummary.textContent).toEqual(`${statsSum}`); const currentlyVotingItems = await within(currentlyVotingTile).queryAllByTestId('currently-voting-item'); - expect(currentlyVotingItems[0].textContent).toEqual( - `${capitalize(stats[0][0].toLowerCase())} - ${getPercentage( - voteStats.proposals[stats[0][0]]?.votes, - statsSum - ).toFixed(2)}%` - ); - expect(currentlyVotingItems[1].textContent).toEqual( - `${capitalize(stats[1][0].toLowerCase())} - ${getPercentage( - voteStats.proposals[stats[1][0]]?.votes, - statsSum - ).toFixed(2)}%` - ); - expect(currentlyVotingItems[2].textContent).toEqual( - `${capitalize(stats[2][0].toLowerCase())} - ${getPercentage( - voteStats.proposals[stats[2][0]]?.votes, - statsSum - ).toFixed(2)}%` - ); + for (const item in pollStatsItems) { + expect(currentlyVotingItems[item].textContent).toEqual( + `${capitalize(stats[item][0].toLowerCase())} - ${getPercentage( + voteStats.proposals[stats[item][0]]?.votes, + statsSum + ).toFixed(2)}%` + ); + } const currentlyVotingChart = await within(currentlyVotingTile).queryByTestId('pie-chart'); expect(currentlyVotingChart).toBeInTheDocument(); @@ -160,7 +152,7 @@ describe('For the event that has already finished', () => { lineWidth: 32, data: statsItems.map(({ label, name }) => ({ title: label, - value: (voteStats.proposals?.[name as any] as unknown as ByCategory['proposals'])?.votes, + value: (voteStats.proposals?.[name as any] as unknown as ByProposalsInCategoryStats['proposals'])?.votes, color: proposalColorsMap[name], })), }); @@ -213,9 +205,9 @@ describe("For the event that hasn't finished yet", () => { expect(pollStatsTileSummary.textContent).toEqual(`${statsSum}`); const pollStatsItems = await within(pollStatsTile).queryAllByTestId('poll-stats-item'); - expect(pollStatsItems[0].textContent).toEqual(`${capitalize(stats[0][0].toLowerCase())}${placeholder}`); - expect(pollStatsItems[1].textContent).toEqual(`${capitalize(stats[1][0].toLowerCase())}${placeholder}`); - expect(pollStatsItems[2].textContent).toEqual(`${capitalize(stats[2][0].toLowerCase())}${placeholder}`); + for (const item in pollStatsItems) { + expect(pollStatsItems[item].textContent).toEqual(`${capitalize(stats[item][0].toLowerCase())}${placeholder}`); + } const currentlyVotingTile = await within(leaderboardPage).queryByTestId('currently-voting-tile'); expect(currentlyVotingTile).not.toBeNull(); @@ -229,9 +221,9 @@ describe("For the event that hasn't finished yet", () => { expect(currentlyVotingTileSummary.textContent).toEqual(`${statsSum}`); const currentlyVotingItems = await within(currentlyVotingTile).queryAllByTestId('currently-voting-item'); - expect(currentlyVotingItems[0].textContent).toEqual(`${capitalize(stats[0][0].toLowerCase())}`); - expect(currentlyVotingItems[1].textContent).toEqual(`${capitalize(stats[1][0].toLowerCase())}`); - expect(currentlyVotingItems[2].textContent).toEqual(`${capitalize(stats[2][0].toLowerCase())}`); + for (const item in pollStatsItems) { + expect(currentlyVotingItems[item].textContent).toEqual(`${capitalize(stats[item][0].toLowerCase())}`); + } const currentlyVotingChart = await within(currentlyVotingTile).queryByTestId('pie-chart'); expect(currentlyVotingChart).toBeInTheDocument(); diff --git a/ui/cip-1694/src/pages/Leaderboard/components/ResultsCommingSoonModal/ResultsCommingSoonModal.tsx b/ui/cip-1694/src/pages/Leaderboard/components/ResultsCommingSoonModal/ResultsCommingSoonModal.tsx index af4afa95c..b66b1f156 100644 --- a/ui/cip-1694/src/pages/Leaderboard/components/ResultsCommingSoonModal/ResultsCommingSoonModal.tsx +++ b/ui/cip-1694/src/pages/Leaderboard/components/ResultsCommingSoonModal/ResultsCommingSoonModal.tsx @@ -18,10 +18,11 @@ type ResultsCommingSoonModalProps = { description: string | React.ReactNode; onCloseFn: () => void; onGoBackFn: () => void; + onConfirmFn: () => void; }; export const ResultsCommingSoonModal = (props: ResultsCommingSoonModalProps) => { - const { name, id, openStatus, title, description, onCloseFn, onGoBackFn } = props; + const { name, id, openStatus, title, description, onCloseFn, onGoBackFn, onConfirmFn } = props; return ( aria-label="close" onClick={onCloseFn} className={styles.closeBtn} + data-testid="result-comming-soon-modal-close-icon" > @@ -79,7 +81,7 @@ export const ResultsCommingSoonModal = (props: ResultsCommingSoonModalProps) => className={cn(styles.button, styles.secondary)} size="large" variant="contained" - onClick={() => onCloseFn()} + onClick={() => onConfirmFn()} sx={{}} data-testid="result-comming-soon-modal-cta" > diff --git a/ui/cip-1694/src/pages/Vote/Vote.tsx b/ui/cip-1694/src/pages/Vote/Vote.tsx index 89ce35949..74155feb4 100644 --- a/ui/cip-1694/src/pages/Vote/Vote.tsx +++ b/ui/cip-1694/src/pages/Vote/Vote.tsx @@ -2,6 +2,8 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Link } from 'react-router-dom'; import { v4 as uuidv4 } from 'uuid'; +import capitalize from 'lodash/capitalize'; +import findIndex from 'lodash/findIndex'; import toast from 'react-hot-toast'; import cn from 'classnames'; import { Grid, Typography, Button, CircularProgress } from '@mui/material'; @@ -13,14 +15,9 @@ import BlockIcon from '@mui/icons-material/Block'; import { useCardano } from '@cardano-foundation/cardano-connect-with-wallet'; import { ROUTES } from 'common/routes'; import { EventTime } from 'components/EventTime/EventTime'; -import { - setIsConnectWalletModalVisible, - setIsReceiptFetched, - setIsVoteSubmittedModalVisible, - setSelectedProposal, - setVoteReceipt, -} from 'common/store/userSlice'; +import { setIsConnectWalletModalVisible, setIsVoteSubmittedModalVisible } from 'common/store/userSlice'; import { ProposalPresentation, Account } from 'types/voting-ledger-follower-types'; +import { VoteReceipt as VoteReceiptType } from 'types/voting-app-types'; import { RootState } from 'common/store'; import { VoteReceipt } from 'pages/Vote/components/VoteReceipt/VoteReceipt'; import { Toast } from 'components/common/Toast/Toast'; @@ -28,19 +25,27 @@ import { VoteSubmittedModal } from 'components/VoteSubmittedModal/VoteSubmittedM import { OptionCard } from 'components/OptionCard/OptionCard'; import { OptionItem } from 'components/OptionCard/OptionCard.types'; import SidePage from 'components/common/SidePage/SidePage'; -import { - buildCanonicalVoteInputJson, - buildCanonicalVoteReceiptInputJson, - getSignedMessagePromise, -} from 'common/utils/voteUtils'; +import { buildCanonicalVoteInputJson, getSignedMessagePromise } from 'common/utils/voteUtils'; import * as voteService from 'common/api/voteService'; +import * as loginService from 'common/api/loginService'; import { useToggle } from 'common/hooks/useToggle'; import { HttpError } from 'common/handlers/httpHandler'; import { getDateAndMonth } from 'common/utils/dateUtils'; -import { capitalize } from 'lodash'; +import { getUserInSession, saveUserInSession, tokenIsExpired } from 'common/utils/session'; +import { ConfirmWithWalletSignatureModal } from './components/ConfirmWithWalletSignatureModal/ConfirmWithWalletSignatureModal'; import { env } from '../../env'; import styles from './Vote.module.scss'; -import { ConfirmWithWalletSignatureModal } from './components/ConfirmWithWalletSignatureModal/ConfirmWithWalletSignatureModal'; + +const copies = [ + { + title: 'The Governance of Cardano', + body: 'Should Cardano change its governance structure?', + }, + { + title: 'The Governance of Cardano', + body: 'Should Cardano implement the minimum-viable governance proposed in CIP-1694?', + }, +]; const errorsMap = { INVALID_VOTING_POWER: 'To cast a vote, Voting Power should be more than 0', @@ -56,30 +61,39 @@ const iconsMap: Record export const VotePage = () => { const { stakeAddress, isConnected, signMessage } = useCardano(); - const receipt = useSelector((state: RootState) => state.user.receipt); + const [receipt, setReceipt] = useState(null); const event = useSelector((state: RootState) => state.user.event); - const isReceiptFetched = useSelector((state: RootState) => state.user.isReceiptFetched); + const tip = useSelector((state: RootState) => state.user.tip); + const [isReceiptFetched, setIsReceiptFetched] = useState(false); const isVoteSubmittedModalVisible = useSelector((state: RootState) => state.user.isVoteSubmittedModalVisible); - const [absoluteSlot, setAbsoluteSlot] = useState(); - const savedProposal = useSelector((state: RootState) => state.user.proposal); const [isReceiptDrawerInitializing, setIsReceiptDrawerInitializing] = useState(false); const [isCastingAVote, setIsCastingAVote] = useState(false); - const [optionId, setOptionId] = useState(savedProposal || ''); - const [isConfirmWithWalletSignatureModalVisible, setIsConfirmWithWalletSignatureModalVisible] = useState( - absoluteSlot && stakeAddress && !savedProposal && event?.notStarted === false - ); + const [optionId, setOptionId] = useState(''); + const [isConfirmWithWalletSignatureModalVisible, setIsConfirmWithWalletSignatureModalVisible] = useState(false); const [voteSubmitted, setVoteSubmitted] = useState(false); + const [category, setCategory] = useState(event?.categories?.[0].id); const [isToggledReceipt, toggleReceipt] = useToggle(false); const dispatch = useDispatch(); useEffect(() => { - if (absoluteSlot && stakeAddress && !savedProposal && event?.notStarted === false) { + setCategory(event?.categories?.[0].id); + }, [event]); + + useEffect(() => { + const session = getUserInSession(); + + if ( + tip?.absoluteSlot && + stakeAddress && + event?.notStarted === false && + ((session && tokenIsExpired(session.expiresAt)) || !session) + ) { setIsConfirmWithWalletSignatureModalVisible(true); } - }, [event?.notStarted, absoluteSlot, savedProposal, stakeAddress]); + }, [event?.notStarted, tip?.absoluteSlot, stakeAddress]); const items: OptionItem[] = event?.categories - ?.find(({ id }) => id === env.CATEGORY_ID) + ?.find(({ id }) => id === category) ?.proposals?.map(({ name }) => ({ name, label: capitalize(name.toLowerCase()), @@ -88,22 +102,49 @@ export const VotePage = () => { const signMessagePromisified = useMemo(() => getSignedMessagePromise(signMessage), [signMessage]); + const login = useCallback(async () => { + const canonicalVoteInput = loginService.buildCanonicalLoginJson({ + stakeAddress, + slotNumber: tip.absoluteSlot.toString(), + }); + try { + const requestVoteObject = await signMessagePromisified(canonicalVoteInput); + const response = await loginService.submitLogin(requestVoteObject); + const session = { + accessToken: response.accessToken, + expiresAt: response.expiresAt, + }; + saveUserInSession(session); + return session?.accessToken; + } catch (error) { + const message = `${error?.info || error?.message || error?.toString()}`; + toast( + } + /> + ); + console.log(message); + } + }, [signMessagePromisified, stakeAddress, tip?.absoluteSlot]); + const fetchReceipt = useCallback( async ({ cb, refetch = false }: { cb?: () => void; refetch?: boolean }) => { const errorPrefix = refetch ? 'Unable to refresh your vote receipt. Please try again' : 'Unable to fetch your vote receipt. Please try again'; try { - const voteObjectPayload = await signMessagePromisified( - buildCanonicalVoteReceiptInputJson({ - voter: stakeAddress, - slotNumber: absoluteSlot.toString(), - }) - ); - const receiptResponse = await voteService.getVoteReceipt(voteObjectPayload); + const session = getUserInSession(); + let token = session?.accessToken; + if (!session || tokenIsExpired(session?.expiresAt)) { + token = await login(); + } + if (!token) return; + const receiptResponse = await voteService.getVoteReceipt(category, token); + if ('id' in receiptResponse) { - dispatch(setVoteReceipt({ receipt: receiptResponse })); - dispatch(setSelectedProposal({ proposal: receiptResponse.proposal })); + setReceipt(receiptResponse); } else { const message = `${errorPrefix}', ${receiptResponse?.title}, ${receiptResponse?.detail}`; console.log(message); @@ -115,12 +156,12 @@ export const VotePage = () => { /> ); } - dispatch(setIsReceiptFetched({ isFetched: true })); + setIsReceiptFetched(true); cb?.(); } catch (error) { if (error?.message === 'VOTE_NOT_FOUND') { - dispatch(setVoteReceipt({ receipt: null })); - dispatch(setIsReceiptFetched({ isFetched: true })); + setReceipt(null); + setIsReceiptFetched(true); setIsReceiptDrawerInitializing(false); setIsConfirmWithWalletSignatureModalVisible(false); return; @@ -138,9 +179,16 @@ export const VotePage = () => { setIsReceiptDrawerInitializing(false); setIsConfirmWithWalletSignatureModalVisible(false); }, - [absoluteSlot, dispatch, signMessagePromisified, stakeAddress] + [login, category] ); + useEffect(() => { + const session = getUserInSession(); + if (isConnected && tip?.absoluteSlot && stakeAddress && session && !tokenIsExpired(session.expiresAt) && category) { + fetchReceipt({}); + } + }, [fetchReceipt, isConnected, stakeAddress, tip?.absoluteSlot, category]); + const openReceiptDrawer = async () => { setIsReceiptDrawerInitializing(true); await fetchReceipt({ @@ -151,28 +199,6 @@ export const VotePage = () => { }); }; - const init = useCallback(async () => { - try { - setAbsoluteSlot((await voteService.getSlotNumber())?.absoluteSlot); - } catch (error) { - const message = `Failed to fecth slot number: ${error?.message}`; - console.log(message); - toast( - } - /> - ); - } - }, []); - - useEffect(() => { - if (isConnected) { - init(); - } - }, [init, isConnected]); - const onChangeOption = (option: string | null) => { setOptionId(option); if (!isConnected) dispatch(setIsConnectWalletModalVisible({ isVisible: true })); @@ -184,6 +210,7 @@ export const VotePage = () => { let votingPower: Account['votingPower']; try { ({ votingPower } = await voteService.getVotingPower(env.EVENT_ID, stakeAddress)); + fetchReceipt({}); } catch (error) { const message = `Failed to fetch votingPower ${ error instanceof Error || error instanceof HttpError ? error?.message : error @@ -204,8 +231,9 @@ export const VotePage = () => { option: optionId?.toUpperCase(), voter: stakeAddress, voteId: uuidv4(), - slotNumber: absoluteSlot.toString(), + slotNumber: tip.absoluteSlot.toString(), votePower: votingPower, + category, }); try { @@ -239,11 +267,18 @@ export const VotePage = () => { setIsCastingAVote(true); }; + const onChangeCategory = () => { + setIsReceiptFetched(false); + const currentCategoryIndex = findIndex(event?.categories, ['id', category]); + setCategory(event?.categories[(currentCategoryIndex + 1) % event?.categories?.length]?.id); + }; + const cantSelectOptions = !!receipt || voteSubmitted || (isConnected && !isReceiptFetched) || event?.notStarted || event?.finished; const showViewReceiptButton = receipt?.id || voteSubmitted || (isReceiptFetched && event?.finished); const showConnectButton = !isConnected && !event?.notStarted; const showSubmitButton = isConnected && !event?.notStarted && !event?.finished && !showViewReceiptButton; + const showPagination = isConnected && receipt && category === receipt?.category && event?.categories?.length > 1; return ( <> @@ -273,7 +308,7 @@ export const VotePage = () => { md: '65px', }} > - CIP-1694 Vote + {copies[findIndex(event?.categories, ['id', category])]?.title} @@ -294,12 +329,12 @@ export const VotePage = () => { fontSize={{ xs: '16px', md: '28px' }} data-testid="event-description" > - (..) + {copies[findIndex(event?.categories, ['id', category])]?.body} { aria-label="Receipt" startIcon={} data-testid="show-receipt-button" - disabled={isReceiptDrawerInitializing || !absoluteSlot} + disabled={isReceiptDrawerInitializing || !tip?.absoluteSlot} > Vote receipt {isReceiptDrawerInitializing && ( @@ -370,7 +405,7 @@ export const VotePage = () => { })} size="large" variant="contained" - disabled={!optionId || !isReceiptFetched || isCastingAVote || !absoluteSlot} + disabled={!optionId || !isReceiptFetched || isCastingAVote || !tip?.absoluteSlot} onClick={() => handleSubmit()} data-testid="proposal-submit-button" > @@ -406,6 +441,17 @@ export const VotePage = () => { View the results )} + {showPagination && ( + + )} { @@ -442,8 +489,8 @@ export const VotePage = () => { description={ <>
Thank you, your vote has been submitted.
- Make sure to check back on {event?.eventStartDate && getDateAndMonth(event?.eventEndDate?.toString())} to see - the results! + Make sure to check back on{' '} + {event?.eventStartDate && getDateAndMonth(event?.eventEndDate?.toString())} to see the results! } /> diff --git a/ui/cip-1694/src/pages/Vote/__tests__/Vote.test.tsx b/ui/cip-1694/src/pages/Vote/__tests__/Vote.test.tsx index b5fe0ee48..d498ce31a 100644 --- a/ui/cip-1694/src/pages/Vote/__tests__/Vote.test.tsx +++ b/ui/cip-1694/src/pages/Vote/__tests__/Vote.test.tsx @@ -6,9 +6,14 @@ var mockCastAVoteWithDigitalSignature = jest.fn(); var mockGetVotingPower = jest.fn(); var mockBuildCanonicalVoteInputJson = jest.fn(); var mockGetSignedMessagePromise = jest.fn(); -var mockGetSlotNumber = jest.fn(); +var mockGetChainTip = jest.fn(); var mockGetVoteReceipt = jest.fn(); var mockToast = jest.fn(); +var mockGetUserInSession = jest.fn(); +var mockSaveUserInSession = jest.fn(); +var mockTokenIsExpired = jest.fn(); +var submitLoginMock = jest.fn(); +var buildCanonicalLoginJsonMock = jest.fn(); /* eslint-disable import/imports-first */ import 'whatwg-fetch'; import '@testing-library/jest-dom'; @@ -38,6 +43,7 @@ import { eventMock_notStarted, eventMock_finished, VoteReceiptMock_Full_MediumAssurance, + userInSessionMock, // VoteReceiptMock_Full_MediumAssurance, } from 'test/mocks'; import { CustomRouter } from 'test/CustomRouter'; @@ -78,8 +84,8 @@ jest.mock('../../../env', () => { ...original, env: { ...original.env, - CATEGORY_ID: 'CIP-1694_Pre_Ratification_4619', - EVENT_ID: 'CIP-1694_Pre_Ratification_4619', + CATEGORY_ID: 'CHANGE_GOV_STRUCTURE', + EVENT_ID: 'CIP-1694_Pre_Ratification_3316', }, }; }); @@ -88,16 +94,29 @@ jest.mock('common/api/voteService', () => ({ ...jest.requireActual('common/api/voteService'), castAVoteWithDigitalSignature: mockCastAVoteWithDigitalSignature, getVotingPower: mockGetVotingPower, - getSlotNumber: mockGetSlotNumber, + getChainTip: mockGetChainTip, getVoteReceipt: mockGetVoteReceipt, })); +jest.mock('common/api/loginService', () => ({ + ...jest.requireActual('common/api/loginService'), + submitLogin: submitLoginMock, + buildCanonicalLoginJson: buildCanonicalLoginJsonMock, +})); + jest.mock('common/utils/voteUtils', () => ({ ...jest.requireActual('common/utils/voteUtils'), buildCanonicalVoteInputJson: mockBuildCanonicalVoteInputJson, getSignedMessagePromise: mockGetSignedMessagePromise, })); +jest.mock('common/utils/session', () => ({ + ...jest.requireActual('common/utils/session'), + getUserInSession: mockGetUserInSession, + saveUserInSession: mockSaveUserInSession, + tokenIsExpired: mockTokenIsExpired, +})); + export const handlers = [ rest.get(`${EVENT_BY_ID_REFERENCE_URL}/${env.EVENT_ID}`, (req, res, ctx) => { return res(ctx.json(eventMock_active), ctx.delay(150)); @@ -116,7 +135,9 @@ afterAll(() => server.close()); describe('For ongoing event:', () => { beforeEach(() => { mockUseCardano.mockReturnValue(useCardanoMock); - mockGetSlotNumber.mockReturnValue(chainTipMock); + mockGetChainTip.mockReturnValue(chainTipMock); + mockGetUserInSession.mockReturnValue({ accessToken: true }); + mockTokenIsExpired.mockReturnValue(false); mockGetVoteReceipt.mockReturnValue({}); }); afterEach(() => { @@ -160,27 +181,25 @@ describe('For ongoing event:', () => { const eventTitle = await within(votePage).queryByTestId('event-title'); expect(eventTitle).not.toBeNull(); - expect(eventTitle.textContent).toEqual('CIP-1694 Vote'); + expect(eventTitle.textContent).toEqual('The Governance of Cardano'); const eventTime = await within(votePage).queryByTestId('event-time'); expect(eventTime).not.toBeNull(); - expect(eventTime.textContent).toEqual(`Voting closes: ${formatUTCDate(eventMock_active.eventEndDate.toString())}`); + expect(eventTime.textContent).toEqual( + `Voting closes: ${formatUTCDate(eventMock_active.eventEndDate.toString())}` + ); const eventDescription = await within(votePage).queryByTestId('event-description'); expect(eventDescription).not.toBeNull(); - expect(eventDescription.textContent).toEqual('(..)'); + expect(eventDescription.textContent).toEqual('Should Cardano change its governance structure?'); const options = await within(votePage).queryAllByTestId('option-card'); expect(options.length).toEqual(eventMock_active.categories[0].proposals.length); - expect(options[0].textContent).toEqual( - capitalize(eventMock_active.categories[0].proposals[0].name.toLowerCase()) - ); - expect(options[1].textContent).toEqual( - capitalize(eventMock_active.categories[0].proposals[1].name.toLowerCase()) - ); - expect(options[2].textContent).toEqual( - capitalize(eventMock_active.categories[0].proposals[2].name.toLowerCase()) - ); + for (const option in options) { + expect(options[option].textContent).toEqual( + capitalize(eventMock_active.categories[0].proposals[option].name.toLowerCase()) + ); + } const cta = await within(votePage).queryByTestId('proposal-connect-button'); expect(cta).not.toBeNull(); @@ -251,27 +270,25 @@ describe('For ongoing event:', () => { const eventTitle = await within(votePage).queryByTestId('event-title'); expect(eventTitle).not.toBeNull(); - expect(eventTitle.textContent).toEqual('CIP-1694 Vote'); + expect(eventTitle.textContent).toEqual('The Governance of Cardano'); const eventTime = await within(votePage).queryByTestId('event-time'); expect(eventTime).not.toBeNull(); - expect(eventTime.textContent).toEqual(`Voting closes: ${formatUTCDate(eventMock_active.eventEndDate.toString())}`); + expect(eventTime.textContent).toEqual( + `Voting closes: ${formatUTCDate(eventMock_active.eventEndDate.toString())}` + ); const eventDescription = await within(votePage).queryByTestId('event-description'); expect(eventDescription).not.toBeNull(); - expect(eventDescription.textContent).toEqual('(..)'); + expect(eventDescription.textContent).toEqual('Should Cardano change its governance structure?'); const options = await within(votePage).queryAllByTestId('option-card'); expect(options.length).toEqual(eventMock_active.categories[0].proposals.length); - expect(options[0].textContent).toEqual( - capitalize(eventMock_active.categories[0].proposals[0].name.toLowerCase()) - ); - expect(options[1].textContent).toEqual( - capitalize(eventMock_active.categories[0].proposals[1].name.toLowerCase()) - ); - expect(options[2].textContent).toEqual( - capitalize(eventMock_active.categories[0].proposals[2].name.toLowerCase()) - ); + for (const option in options) { + expect(options[option].textContent).toEqual( + capitalize(eventMock_active.categories[0].proposals[option].name.toLowerCase()) + ); + } const cta = await within(votePage).queryByTestId('proposal-submit-button'); expect(cta).not.toBeNull(); @@ -316,33 +333,145 @@ describe('For ongoing event:', () => { mockGetVotingPower.mockResolvedValue(accountDataMock); mockBuildCanonicalVoteInputJson.mockReset(); mockBuildCanonicalVoteInputJson.mockReturnValue(canonicalVoteInputJsonMock); + const history = createMemoryHistory({ initialEntries: [ROUTES.VOTE] }); - const { store } = renderWithProviders( + let store: ReturnType['store']; + await act(async () => { + ({ store } = renderWithProviders( + + + , + { preloadedState: { user: { event: eventMock_active, tip: chainTipMock } as UserState } } + )); + }); + + await waitFor(async () => { + const votePage = screen.queryByTestId('vote-page'); + + const options = within(votePage).queryAllByTestId('option-card'); + + await act(async () => { + fireEvent.click(options[0]); + }); + + const cta = within(votePage).queryByTestId('proposal-submit-button'); + expect(cta).not.toBeNull(); + expect(cta.closest('button')).not.toBeDisabled(); + + expect(store.getState().user.isVoteSubmittedModalVisible).toBeFalsy(); + await act(async () => { + fireEvent.click(cta); + }); + + expect(mockCastAVoteWithDigitalSignature).toHaveBeenCalledWith(canonicalVoteInputJsonMock); + expect(store.getState().user.isVoteSubmittedModalVisible).toBeTruthy; + }); + }); + + test('should ask to fetch receipt and display proper state if present and user session is active', async () => { + const mockSignMessage = jest.fn().mockImplementation(async (message) => await message); + mockGetVoteReceipt.mockReset(); + mockGetVoteReceipt.mockReturnValue(VoteReceiptMock_Basic); + mockUseCardano.mockReset(); + mockUseCardano.mockReturnValue({ + ...useCardanoMock, + signMessage: mockSignMessage, + }); + mockGetSignedMessagePromise.mockReset(); + mockGetSignedMessagePromise.mockImplementation( + (signMessage: (message: string) => string) => async (message: string) => await signMessage(message) + ); + mockGetVotingPower.mockReset(); + mockGetVotingPower.mockResolvedValue(accountDataMock); + + mockGetUserInSession.mockReset(); + mockGetUserInSession.mockReturnValue({ accessToken: true }); + mockTokenIsExpired.mockReset(); + mockTokenIsExpired.mockReturnValue(false); + + const history = createMemoryHistory({ initialEntries: [ROUTES.VOTE] }); + renderWithProviders( + + + , + { preloadedState: { user: { event: eventMock_active, tip: chainTipMock } as UserState } } + ); + + await waitFor(async () => { + const confirmationModal = screen.queryByTestId('confirm-with-signature-modal'); + expect(confirmationModal).toBeNull(); + }); + }); + + test('should ask to fetch receipt and display proper state if present and there is no user session', async () => { + const canonicalVoteInput = 'canonicalVoteInput'; + buildCanonicalLoginJsonMock.mockReset(); + buildCanonicalLoginJsonMock.mockReturnValue(canonicalVoteInput); + const mockSignMessage = jest.fn().mockImplementation(async (message) => await message); + mockGetVoteReceipt.mockReset(); + mockGetVoteReceipt.mockReturnValue(VoteReceiptMock_Basic); + mockUseCardano.mockReset(); + mockUseCardano.mockReturnValue({ + ...useCardanoMock, + signMessage: mockSignMessage, + }); + mockGetSignedMessagePromise.mockReset(); + mockGetSignedMessagePromise.mockImplementation( + (signMessage: (message: string) => string) => async (message: string) => await signMessage(message) + ); + mockGetVotingPower.mockReset(); + mockGetVotingPower.mockResolvedValue(accountDataMock); + + mockGetUserInSession.mockReset(); + mockGetUserInSession.mockReturnValue(null); + + submitLoginMock.mockReset(); + submitLoginMock.mockReturnValue(userInSessionMock); + + const history = createMemoryHistory({ initialEntries: [ROUTES.VOTE] }); + renderWithProviders( , - { preloadedState: { user: { event: eventMock_active, isReceiptFetched: true } as UserState } } + { preloadedState: { user: { event: eventMock_active, tip: chainTipMock } as UserState } } ); const votePage = await screen.findByTestId('vote-page'); - const options = await within(votePage).queryAllByTestId('option-card'); - fireEvent.click(options[0]); + const confirmationModal = await screen.findByTestId('confirm-with-signature-modal'); + expect(confirmationModal).not.toBeNull(); + expect(await within(confirmationModal).findByTestId('confirm-with-signature-title')).toHaveTextContent( + 'Wallet signature' + ); + expect(await within(confirmationModal).findByTestId('confirm-with-signature-description')).toHaveTextContent( + 'We need to check if you’ve already voted. Please confirm with your wallet signature.' + ); + const confirmCta = await within(confirmationModal).findByTestId('confirm-with-signature-cta'); + expect(confirmCta).toHaveTextContent('Confirm'); - const cta = await within(votePage).queryByTestId('proposal-submit-button'); - expect(cta).not.toBeNull(); + fireEvent.click(confirmCta); - expect(store.getState().user.isVoteSubmittedModalVisible).toBeFalsy(); - fireEvent.click(cta); + await waitFor(async () => { + expect(screen.queryAllByRole('button', { pressed: true })[0].textContent).toEqual( + capitalize( + eventMock_active.categories[0].proposals + .find(({ name }) => name === VoteReceiptMock_Basic.proposal) + .name.toLowerCase() + ) + ); - await waitFor(() => { - expect(mockCastAVoteWithDigitalSignature).toHaveBeenCalledWith(canonicalVoteInputJsonMock); - expect(store.getState().user.isVoteSubmittedModalVisible).toBeTruthy; + const cta = await within(votePage).queryByTestId('show-receipt-button'); + expect(cta).not.toBeNull(); + expect(cta.textContent).toEqual('Vote receipt'); + + expect(await screen.queryByTestId('confirm-with-signature-modal')).toBeNull(); + expect(submitLoginMock).toBeCalledWith(canonicalVoteInput); + expect(mockSaveUserInSession).toBeCalledWith(userInSessionMock); }); }); - test('should ask to fetch receipt and display proper state if present', async () => { + test('should ask to fetch receipt and display proper state if present and user token has expired', async () => { const mockSignMessage = jest.fn().mockImplementation(async (message) => await message); mockGetVoteReceipt.mockReset(); mockGetVoteReceipt.mockReturnValue(VoteReceiptMock_Basic); @@ -358,12 +487,21 @@ describe('For ongoing event:', () => { mockGetVotingPower.mockReset(); mockGetVotingPower.mockResolvedValue(accountDataMock); + mockGetUserInSession.mockReset(); + mockGetUserInSession.mockReturnValue({ accessToken: true }); + + mockTokenIsExpired.mockReset(); + mockTokenIsExpired.mockReturnValue(true); + + submitLoginMock.mockReset(); + submitLoginMock.mockReturnValue(userInSessionMock); + const history = createMemoryHistory({ initialEntries: [ROUTES.VOTE] }); - const { store } = renderWithProviders( + renderWithProviders( , - { preloadedState: { user: { event: eventMock_active } as UserState } } + { preloadedState: { user: { event: eventMock_active, tip: chainTipMock } as UserState } } ); const votePage = await screen.findByTestId('vote-page'); @@ -382,9 +520,6 @@ describe('For ongoing event:', () => { fireEvent.click(confirmCta); await waitFor(async () => { - expect(store.getState().user.proposal).toEqual(VoteReceiptMock_Basic.proposal); - expect(store.getState().user.receipt).toEqual(VoteReceiptMock_Basic); - expect(screen.queryAllByRole('button', { pressed: true })[0].textContent).toEqual( capitalize( eventMock_active.categories[0].proposals @@ -427,8 +562,123 @@ describe('For ongoing event:', () => { preloadedState: { user: { event: eventMock_active, - proposal: VoteReceiptMock_Basic.proposal, - receipt: VoteReceiptMock_Basic, + tip: chainTipMock, + } as UserState, + }, + } + ); + }); + + const votePage = await screen.findByTestId('vote-page'); + expect(votePage).toBeInTheDocument(); + expect(screen.queryByTestId('confirm-with-signature-modal')).not.toBeInTheDocument(); + + const cta = within(votePage).queryByTestId('show-receipt-button'); + expect(cta.closest('button')).not.toBeDisabled(); + expect(screen.queryByTestId('vote-receipt')).not.toBeInTheDocument(); + + await act(async () => { + fireEvent.click(cta); + }); + expect(screen.queryByTestId('vote-receipt')).toBeInTheDocument(); + }); + + test('should switch between categories', async () => { + const mockSignMessage = jest.fn().mockImplementation(async (message) => await message); + mockGetVoteReceipt.mockReset(); + mockGetVoteReceipt.mockReturnValue(VoteReceiptMock_Basic); + mockUseCardano.mockReset(); + mockUseCardano.mockReturnValue({ + ...useCardanoMock, + signMessage: mockSignMessage, + }); + mockGetSignedMessagePromise.mockReset(); + mockGetSignedMessagePromise.mockImplementation( + (signMessage: (message: string) => string) => async (message: string) => await signMessage(message) + ); + mockGetVotingPower.mockReset(); + mockGetVotingPower.mockResolvedValue(accountDataMock); + + const history = createMemoryHistory({ initialEntries: [ROUTES.VOTE] }); + await act(async () => { + renderWithProviders( + + + , + { + preloadedState: { + user: { + event: eventMock_active, + tip: chainTipMock, + } as UserState, + }, + } + ); + }); + + const votePage = await screen.findByTestId('vote-page'); + expect(votePage).toBeInTheDocument(); + + const cta = within(votePage).queryByTestId('next-question-button'); + expect(cta.closest('button')).not.toBeDisabled(); + expect(cta).toHaveTextContent('Next question'); + + await act(async () => { + fireEvent.click(cta); + }); + + const eventTitle = await within(votePage).queryByTestId('event-title'); + expect(eventTitle).not.toBeNull(); + expect(eventTitle.textContent).toEqual('The Governance of Cardano'); + + const eventTime = await within(votePage).queryByTestId('event-time'); + expect(eventTime).not.toBeNull(); + expect(eventTime.textContent).toEqual(`Voting closes: ${formatUTCDate(eventMock_active.eventEndDate.toString())}`); + + const eventDescription = await within(votePage).queryByTestId('event-description'); + expect(eventDescription).not.toBeNull(); + expect(eventDescription.textContent).toEqual( + 'Should Cardano implement the minimum-viable governance proposed in CIP-1694?' + ); + + const options = await within(votePage).queryAllByTestId('option-card'); + expect(options.length).toEqual(eventMock_active.categories[1].proposals.length); + for (const option in options) { + expect(options[option].textContent).toEqual( + capitalize(eventMock_active.categories[1].proposals[option].name.toLowerCase()) + ); + } + expect(screen.queryByTestId('vote-receipt')).not.toBeInTheDocument(); + expect(mockGetVoteReceipt).toHaveBeenLastCalledWith('MIN_VIABLE_GOV_STRUCTURE', true); + }); + + test('should handle show vote receipt for inactive user session', async () => { + const mockSignMessage = jest.fn().mockImplementation(async (message) => await message); + mockGetVoteReceipt.mockReset(); + mockGetVoteReceipt.mockReturnValue(VoteReceiptMock_Basic); + mockUseCardano.mockReset(); + mockUseCardano.mockReturnValue({ + ...useCardanoMock, + signMessage: mockSignMessage, + }); + mockGetSignedMessagePromise.mockReset(); + mockGetSignedMessagePromise.mockImplementation( + (signMessage: (message: string) => string) => async (message: string) => await signMessage(message) + ); + mockGetVotingPower.mockReset(); + mockGetVotingPower.mockResolvedValue(accountDataMock); + + const history = createMemoryHistory({ initialEntries: [ROUTES.VOTE] }); + await act(async () => { + renderWithProviders( + + + , + { + preloadedState: { + user: { + event: eventMock_active, + tip: chainTipMock, } as UserState, }, } @@ -465,6 +715,11 @@ describe('For ongoing event:', () => { mockGetVotingPower.mockReset(); mockGetVotingPower.mockResolvedValue(accountDataMock); + mockGetUserInSession.mockReset(); + mockGetUserInSession.mockReturnValue({ accessToken: true }); + mockTokenIsExpired.mockReset(); + mockTokenIsExpired.mockReturnValue(false); + const history = createMemoryHistory({ initialEntries: [ROUTES.VOTE] }); await act(async () => { renderWithProviders( @@ -475,8 +730,7 @@ describe('For ongoing event:', () => { preloadedState: { user: { event: eventMock_active, - proposal: VoteReceiptMock_Basic.proposal, - receipt: VoteReceiptMock_Basic, + tip: chainTipMock, } as UserState, }, } @@ -508,8 +762,10 @@ describe('For ongoing event:', () => { describe("For the event that hasn't started yet", () => { beforeEach(() => { mockUseCardano.mockReturnValue(useCardanoMock); - mockGetSlotNumber.mockReturnValue(chainTipMock); + mockGetChainTip.mockReturnValue(chainTipMock); mockGetVoteReceipt.mockReturnValue(null); + mockGetUserInSession.mockReturnValue({ accessToken: true }); + mockTokenIsExpired.mockReturnValue(false); }); afterEach(() => { jest.clearAllMocks(); @@ -526,7 +782,7 @@ describe("For the event that hasn't started yet", () => { , - { preloadedState: { user: { event: eventMock_notStarted } as UserState } } + { preloadedState: { user: { event: eventMock_notStarted, tip: chainTipMock } as UserState } } ); await waitFor(async () => { @@ -535,7 +791,7 @@ describe("For the event that hasn't started yet", () => { const eventTitle = await within(votePage).queryByTestId('event-title'); expect(eventTitle).not.toBeNull(); - expect(eventTitle.textContent).toEqual('CIP-1694 Vote'); + expect(eventTitle.textContent).toEqual('The Governance of Cardano'); const eventTime = await within(votePage).queryByTestId('event-time'); expect(eventTime).not.toBeNull(); @@ -547,22 +803,16 @@ describe("For the event that hasn't started yet", () => { const eventDescription = await within(votePage).queryByTestId('event-description'); expect(eventDescription).not.toBeNull(); - expect(eventDescription.textContent).toEqual('(..)'); + expect(eventDescription.textContent).toEqual('Should Cardano change its governance structure?'); const options = await within(votePage).queryAllByTestId('option-card'); expect(options.length).toEqual(eventMock_notStarted.categories[0].proposals.length); - expect(options[0].textContent).toEqual( - capitalize(eventMock_notStarted.categories[0].proposals[0].name.toLowerCase()) - ); - expect(options[0].closest('button')).toHaveAttribute('disabled'); - expect(options[1].textContent).toEqual( - capitalize(eventMock_notStarted.categories[0].proposals[1].name.toLowerCase()) - ); - expect(options[1].closest('button')).toHaveAttribute('disabled'); - expect(options[2].textContent).toEqual( - capitalize(eventMock_notStarted.categories[0].proposals[2].name.toLowerCase()) - ); - expect(options[2].closest('button')).toHaveAttribute('disabled'); + for (const option in options) { + expect(options[option].textContent).toEqual( + capitalize(eventMock_notStarted.categories[0].proposals[option].name.toLowerCase()) + ); + expect(options[option].closest('button')).toHaveAttribute('disabled'); + } const cta = await within(votePage).queryByTestId('event-hasnt-started-submit-button'); expect(cta).not.toBeNull(); @@ -576,8 +826,10 @@ describe("For the event that hasn't started yet", () => { describe('For the event that has already finished', () => { beforeEach(() => { mockUseCardano.mockReturnValue(useCardanoMock); - mockGetSlotNumber.mockReturnValue(chainTipMock); + mockGetChainTip.mockReturnValue(chainTipMock); mockGetVoteReceipt.mockReturnValue({}); + mockGetUserInSession.mockReturnValue({ accessToken: true }); + mockTokenIsExpired.mockReturnValue(false); }); afterEach(() => { jest.clearAllMocks(); @@ -603,7 +855,7 @@ describe('For the event that has already finished', () => { const eventTitle = await within(votePage).queryByTestId('event-title'); expect(eventTitle).not.toBeNull(); - expect(eventTitle.textContent).toEqual('CIP-1694 Vote'); + expect(eventTitle.textContent).toEqual('The Governance of Cardano'); const eventTime = await within(votePage).queryByTestId('event-time'); expect(eventTime).not.toBeNull(); @@ -613,19 +865,15 @@ describe('For the event that has already finished', () => { const eventDescription = await within(votePage).queryByTestId('event-description'); expect(eventDescription).not.toBeNull(); - expect(eventDescription.textContent).toEqual('(..)'); + expect(eventDescription.textContent).toEqual('Should Cardano change its governance structure?'); const options = await within(votePage).queryAllByTestId('option-card'); expect(options.length).toEqual(eventMock_finished.categories[0].proposals.length); - expect(options[0].textContent).toEqual( - capitalize(eventMock_finished.categories[0].proposals[0].name.toLowerCase()) - ); - expect(options[1].textContent).toEqual( - capitalize(eventMock_finished.categories[0].proposals[1].name.toLowerCase()) - ); - expect(options[2].textContent).toEqual( - capitalize(eventMock_finished.categories[0].proposals[2].name.toLowerCase()) - ); + for (const option in options) { + expect(options[option].textContent).toEqual( + capitalize(eventMock_finished.categories[0].proposals[option].name.toLowerCase()) + ); + } const cta = await within(votePage).queryByTestId('proposal-connect-button'); expect(cta).not.toBeNull(); @@ -662,7 +910,7 @@ describe('For the event that has already finished', () => { , - { preloadedState: { user: { event: eventMock_finished, isReceiptFetched: true } as UserState } } + { preloadedState: { user: { event: eventMock_finished } as UserState } } ); const votePage = await screen.findByTestId('vote-page'); @@ -704,8 +952,7 @@ describe('For the event that has already finished', () => { preloadedState: { user: { event: eventMock_finished, - proposal: VoteReceiptMock_Basic.proposal, - receipt: VoteReceiptMock_Basic, + tip: chainTipMock, } as UserState, }, } diff --git a/ui/cip-1694/src/pages/Vote/components/VoteReceipt/VoteReceipt.tsx b/ui/cip-1694/src/pages/Vote/components/VoteReceipt/VoteReceipt.tsx index 5f1a85721..0e00293ae 100644 --- a/ui/cip-1694/src/pages/Vote/components/VoteReceipt/VoteReceipt.tsx +++ b/ui/cip-1694/src/pages/Vote/components/VoteReceipt/VoteReceipt.tsx @@ -1,5 +1,4 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; import toast from 'react-hot-toast'; import pick from 'lodash/pick'; import Grid from '@mui/material/Grid'; @@ -9,7 +8,7 @@ import BlockIcon from '@mui/icons-material/Block'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { VoteVerificationRequest } from 'types/voting-verification-app-types'; import * as verificationService from 'common/api/verificationService'; -import { RootState } from 'common/store'; +import { VoteReceipt as VoteReceiptType } from 'types/voting-app-types'; import { Toast } from 'components/common/Toast/Toast'; import { AdvancedFullFieldsToDisplayArrayKeys, @@ -25,10 +24,10 @@ import styles from './VoteReceipt.module.scss'; type VoteReceiptProps = { setOpen: () => void; fetchReceipt: (props: { cb?: () => void; refetch?: boolean }) => void; + receipt: VoteReceiptType; }; -export const VoteReceipt = ({ setOpen, fetchReceipt }: VoteReceiptProps) => { - const receipt = useSelector((state: RootState) => state.user.receipt); +export const VoteReceipt = ({ setOpen, fetchReceipt, receipt }: VoteReceiptProps) => { const [isVerified, setIsVerified] = useState(false); const verifyVote = useCallback(async () => { diff --git a/ui/cip-1694/src/pages/Vote/components/VoteReceipt/__tests__/VoteReceipt.test.tsx b/ui/cip-1694/src/pages/Vote/components/VoteReceipt/__tests__/VoteReceipt.test.tsx index 508833bb0..fdeb94fb6 100644 --- a/ui/cip-1694/src/pages/Vote/components/VoteReceipt/__tests__/VoteReceipt.test.tsx +++ b/ui/cip-1694/src/pages/Vote/components/VoteReceipt/__tests__/VoteReceipt.test.tsx @@ -8,7 +8,6 @@ import { expect } from '@jest/globals'; import { cleanup, act, waitFor, screen, within, fireEvent } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import BlockIcon from '@mui/icons-material/Block'; -import { UserState } from 'common/store/types'; import { ROUTES } from 'common/routes'; import { Toast } from 'components/common/Toast/Toast'; import { renderWithProviders } from 'test/mockProviders'; @@ -75,9 +74,9 @@ describe('Vote receipt:', () => { - , - { preloadedState: { user: { receipt: VoteReceiptMock_Full_MediumAssurance } as UserState } } + ) ); @@ -103,9 +102,9 @@ describe('Vote receipt:', () => { - , - { preloadedState: { user: { receipt: VoteReceiptMock_Basic } as UserState } } + ) ); @@ -180,9 +179,9 @@ describe('Vote receipt:', () => { - , - { preloadedState: { user: { receipt: VoteReceiptMock_Partial } as UserState } } + ) ); @@ -255,9 +254,9 @@ describe('Vote receipt:', () => { - , - { preloadedState: { user: { receipt: VoteReceiptMock_Rollback } as UserState } } + ) ); @@ -333,9 +332,9 @@ describe('Vote receipt:', () => { - , - { preloadedState: { user: { receipt: VoteReceiptMock_Full_LowAssurance } as UserState } } + ) ); @@ -414,9 +413,9 @@ describe('Vote receipt:', () => { - , - { preloadedState: { user: { receipt: VoteReceiptMock_Full_LowAssurance } as UserState } } + ) ); @@ -439,9 +438,9 @@ describe('Vote receipt:', () => { - , - { preloadedState: { user: { receipt: VoteReceiptMock_Full_MediumAssurance } as UserState } } + ) ); @@ -464,9 +463,9 @@ describe('Vote receipt:', () => { - , - { preloadedState: { user: { receipt: VoteReceiptMock_Full_HighAssurance } as UserState } } + ) ); @@ -490,9 +489,9 @@ describe('Vote receipt:', () => { - , - { preloadedState: { user: { receipt: VoteReceiptMock_Full_HighAssurance } as UserState } } + ) ); diff --git a/ui/cip-1694/src/setupProxy.js b/ui/cip-1694/src/setupProxy.js index ef8d4f5e3..91416fb15 100644 --- a/ui/cip-1694/src/setupProxy.js +++ b/ui/cip-1694/src/setupProxy.js @@ -23,6 +23,13 @@ module.exports = function (app) { changeOrigin: true, }) ); + app.use( + '/api/auth/**', + createProxyMiddleware({ + target: 'http://localhost:9091', + changeOrigin: true, + }) + ); app.use( '/api', createProxyMiddleware({ diff --git a/ui/cip-1694/src/test/mocks.ts b/ui/cip-1694/src/test/mocks.ts index 66ecc46dc..ea3f53fd3 100644 --- a/ui/cip-1694/src/test/mocks.ts +++ b/ui/cip-1694/src/test/mocks.ts @@ -1,11 +1,11 @@ -import { VoteReceipt, ByCategory } from 'types/voting-app-types'; +import { VoteReceipt, ByProposalsInCategoryStats } from 'types/voting-app-types'; import { EventPresentation, ChainTip, Account } from 'types/voting-ledger-follower-types'; import { useCardano } from '@cardano-foundation/cardano-connect-with-wallet'; import { canonicalize } from 'json-canonicalize'; -export const voteStats: ByCategory = { - category: '1694_Pre_Ratification_4619', +export const voteStats: ByProposalsInCategoryStats = { + category: 'CHANGE_GOV_STRUCTURE', proposals: { YES: { votes: 2134, votingPower: '123' }, NO: { votes: 700, votingPower: '123' }, @@ -18,8 +18,8 @@ export const canonicalVoteInputJsonMock = canonicalize({ actionText: 'Cast Vote', data: { address: 'stake_test1uqwcz0754wwpuhm6xhdpda6u9enyahaj5ynlc9ay5l4mlms4pyqyg', - category: 'CIP-1694_Pre_Ratification_4619', - event: 'CIP-1694_Pre_Ratification_4619', + category: 'CHANGE_GOV_STRUCTURE', + event: 'CIP-1694_Pre_Ratification_3316', id: 'ebff2758-7122-4007-899f-90eea0e236c0', network: 'PREPROD', proposal: 'YES', @@ -81,43 +81,62 @@ export const useCardanoMock_notConnected: ReturnType = { }; export const eventMock_active: EventPresentation = { - id: 'CIP-1694_Pre_Ratification_4619', - team: 'CF & IOG', + id: 'CIP-1694_Pre_Ratification_3316', + organisers: 'CF and IOG', votingEventType: 'STAKE_BASED', startSlot: null, endSlot: null, - startEpoch: 80, - eventStartDate: '2023-07-06T00:00:00Z' as unknown as Date, - eventEndDate: '2023-09-23T23:59:59Z' as unknown as Date, - snapshotTime: '2023-07-05T23:59:59Z' as unknown as Date, - endEpoch: 95, - snapshotEpoch: 79, + proposalsRevealSlot: null, + startEpoch: 94, + eventStartDate: '2023-09-14T00:00:00Z' as unknown as Date, + eventEndDate: '2023-10-18T23:59:59Z' as unknown as Date, + proposalsRevealDate: '2023-11-08T00:00:00Z' as unknown as Date, + snapshotTime: '2023-09-13T23:59:59Z' as unknown as Date, + endEpoch: 100, + snapshotEpoch: 93, + proposalsRevealEpoch: 105, categories: [ { - id: 'CIP-1694_Pre_Ratification_4619', + id: 'CHANGE_GOV_STRUCTURE', gdprProtection: false, proposals: [ { - id: '00048bb6-028d-4f13-b3e5-d19deb22d2c2', + id: '1f082124-ee46-4deb-9140-84a4529f98be', name: 'YES', }, { - id: 'e858953c-37f2-4d1b-b844-c2e4b125fe23', + id: 'ed9f03e8-8ee9-4de5-93a3-30779216f150', name: 'NO', }, + ], + }, + { + id: 'MIN_VIABLE_GOV_STRUCTURE', + gdprProtection: false, + proposals: [ + { + id: '291f91b3-3e3c-402e-aebf-854f141b372b', + name: 'CIP-1694', + }, { - id: '6f05012e-081e-4746-ba53-1833ff995fe3', + id: '842cf5fc-2eda-44a0-b067-87e6a7035aa1', + name: 'OTHER', + }, + { + id: 'adcec241-67de-4860-a881-aaa91a5283a2', name: 'ABSTAIN', }, ], }, ], active: true, + finished: false, notStarted: false, + proposalsReveal: false, allowVoteChanging: false, - finished: false, + highLevelEventResultsWhileVoting: false, + highLevelCategoryResultsWhileVoting: false, categoryResultsWhileVoting: false, - highLevelResultsWhileVoting: false, }; export const eventMock_notStarted: EventPresentation = { @@ -153,7 +172,7 @@ export const VoteReceiptMock_Basic: VoteReceipt = { coseSignature: '8458200201276761646472657373581de01d813fdab9cle5f7a35da16f75c2664edfb2a127c17a4a7ebbfeel6668617368656445901907622616374696f6e223a22434153545f596179356c346d6c6d73347079717967222c226361746567672792230224349502d3136393455072655526174696669636174696f6e534363139222c226576656e74223a224349502f73616c223a22594553222c227667465644174223a33323936333037392c227667469667506776572223a2239393937227d2c22736c674223a33323936333037392c2275726922375d8Fb996a6cb01', cosePublicKey: '04010103272006215820c4821499cef96eda9c00cdd', - category: '', + category: 'CHANGE_GOV_STRUCTURE', }; export const VoteReceiptMock_Partial: VoteReceipt = { @@ -183,3 +202,9 @@ export const VoteReceiptMock_Full_HighAssurance: VoteReceipt = { status: 'FULL', finalityScore: 'VERY_HIGH', }; + +export const userInSessionMock = { + accessToken: + 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJzdGFrZV90ZXN0MXVxd2N6MDc1NHd3cHVobTZ4aGRwZGE2dTllbnlhaGFqNXlubGM5YXk1bDRtbG1zNHB5cXlnIiwiZXZlbnRJZCI6IkNJUC0xNjk0X1ByZV9SYXRpZmljYXRpb25fMzMxNiIsInJvbGUiOiJWT1RFUiIsImlzcyI6Imh0dHBzOi8vY2FyZGFub2ZvdW5kYXRpb24ub3JnIiwic3Rha2VBZGRyZXNzIjoic3Rha2VfdGVzdDF1cXdjejA3NTR3d3B1aG02eGhkcGRhNnU5ZW55YWhhajV5bmxjOWF5NWw0bWxtczRweXF5ZyIsImV4cCI6MTY5NDc4NDk4NCwiaWF0IjoxNjk0Njk4NTg0LCJqdGkiOiIzNmIxZjc1NS1mZDc2LTQyMzAtYTVmMy0zYjhkMDJhN2I2ZGYiLCJjYXJkYW5vTmV0d29yayI6IlBSRVBST0QifQ.MHXhEiXhak-5HOVxBRN9y5kx5LGO2zIpU3c4L09GNlg8cJDqtfSgFwgDl0eY0kZQQKkWJhT5kpz5V7Bqu7fxDQ', + expiresAt: '2023-09-15T16:36:24.903634', +}; diff --git a/ui/cip-1694/src/types/voting-app-types.ts b/ui/cip-1694/src/types/voting-app-types.ts index e58088e54..c3e983ee9 100644 --- a/ui/cip-1694/src/types/voting-app-types.ts +++ b/ui/cip-1694/src/types/voting-app-types.ts @@ -1,6 +1,6 @@ /* tslint:disable */ /* eslint-disable */ -// Generated using typescript-generator version 3.2.1263 on 2023-08-23 10:07:20. +// Generated using typescript-generator version 3.2.1263 on 2023-09-14 10:18:45. export interface Either extends Value, Serializable { left: L; @@ -32,21 +32,31 @@ export interface L1SubmissionData { export interface Leaderboard { } -export interface ByCategory { - category: string; - proposals: { [index: string]: Votes }; +export interface ByCategoryStats { + id: string; + votes: number; + votingPower: string; } -export interface ByCategoryBuilder { +export interface ByCategoryStatsBuilder { } -export interface ByEvent { +export interface ByEventStats { event: string; totalVotesCount: number; totalVotingPower: string; + categories: ByCategoryStats[]; } -export interface ByEventBuilder { +export interface ByEventStatsBuilder { +} + +export interface ByProposalsInCategoryStats { + category: string; + proposals: { [index: string]: Votes }; +} + +export interface ByProposalsInCategoryStatsBuilder { } export interface LeaderboardBuilder { @@ -57,6 +67,14 @@ export interface Votes { votingPower: string; } +export interface VotesBuilder { +} + +export interface LoginResult { + accessToken: string; + expiresAt: Date; +} + export interface TxBody { txDataHex: string; } @@ -98,7 +116,14 @@ export interface MerkleProofItemBuilder { export interface VoteReceiptBuilder { } +export interface WellKnownPointWithProtocolMagic { + wellKnownPointForNetwork?: Point; + protocolMagic: number; +} + export interface AbstractTimestampEntity { + createdAt: Date; + updatedAt: Date; } export interface Vote extends AbstractTimestampEntity { @@ -135,11 +160,20 @@ export interface CIP93Envelope { actionText: string; slot: string; data: T; + slotAsLong: number; + actionAsEnum?: Web3Action; } export interface CIP93EnvelopeBuilder { } +export interface JwtLoginEnvelope { + event: string; + address: string; + network: string; + role: string; +} + export interface SignedWeb3Request { coseSignature: string; cosePublicKey?: string; @@ -165,18 +199,77 @@ export interface VoteEnvelope { network: string; votedAt: string; votingPower?: string; + votedAtSlot: number; } export interface VoteEnvelopeBuilder { } -export interface StakeAddressVerificationService { +export interface DefaultLoginService extends LoginService { +} + +export interface DefaultLoginService__BeanDefinitions { } -export interface StakeAddressVerificationService__Autowiring { +export interface Headers { } -export interface StakeAddressVerificationService__BeanDefinitions { +export interface LoginService { +} + +export interface LoginSystemDetector { +} + +export interface LoginSystemDetector__BeanDefinitions { +} + +export interface JwtAuthenticationToken extends AbstractAuthenticationToken { +} + +export interface JwtFilter extends OncePerRequestFilter { + beanName: string; + servletContext: ServletContext; +} + +export interface JwtFilter__BeanDefinitions { +} + +export interface JwtPrincipal extends Principal, AuthenticatedPrincipal { + signedJWT: SignedJWT; +} + +export interface JwtService { +} + +export interface JwtService__Autowiring { +} + +export interface JwtService__BeanDefinitions { +} + +export interface Web3AuthenticationToken extends AbstractAuthenticationToken { + details: Web3Details; +} + +export interface Web3Details { + stakeAddress: string; + event: EventDetailsResponse; + action: Web3Action; + network: CardanoNetwork; + cip30VerificationResult: Cip30VerificationResult; + envelope: CIP93Envelope<{ [index: string]: any }>; + signedWeb3Request: SignedWeb3Request; +} + +export interface Web3DetailsBuilder { +} + +export interface Web3Filter extends OncePerRequestFilter { + beanName: string; + servletContext: ServletContext; +} + +export interface Web3Filter__BeanDefinitions { } export interface BackendServiceBlockchainTransactionSubmissionService extends BlockchainTransactionSubmissionService { @@ -212,9 +305,6 @@ export interface JsonService__BeanDefinitions { export interface DefaultLeaderBoardService extends LeaderBoardService { } -export interface DefaultLeaderBoardService__Autowiring { -} - export interface DefaultLeaderBoardService__BeanDefinitions { } @@ -287,22 +377,28 @@ export interface TransactionSubmissionService { export interface DefaultVoteService extends VoteService { } -export interface DefaultVoteService__Autowiring { -} - export interface DefaultVoteService__BeanDefinitions { } export interface VoteService { } +export interface RollbackHandler { +} + +export interface RollbackHandler__Autowiring { +} + +export interface RollbackHandler__BeanDefinitions { +} + export interface Problem { instance: URI; type: URI; parameters: { [index: string]: any }; status: StatusType; - title: string; detail: string; + title: string; } export interface Serializable { @@ -312,6 +408,100 @@ export interface MerkleElement { empty: boolean; } +export interface Point { + slot: number; + hash: string; +} + +export interface GrantedAuthority extends Serializable { + authority: string; +} + +export interface AbstractAuthenticationToken extends Authentication, CredentialsContainer { +} + +export interface Environment extends PropertyResolver { + activeProfiles: string[]; + defaultProfiles: string[]; +} + +export interface FilterConfig { + initParameterNames: Enumeration; + servletContext: ServletContext; + filterName: string; +} + +export interface ServletContext { + sessionTimeout: number; + classLoader: ClassLoader; + majorVersion: number; + minorVersion: number; + defaultSessionTrackingModes: SessionTrackingMode[]; + effectiveSessionTrackingModes: SessionTrackingMode[]; + requestCharacterEncoding: string; + responseCharacterEncoding: string; + effectiveMajorVersion: number; + effectiveMinorVersion: number; + /** + * @deprecated + */ + servlets: Enumeration; + /** + * @deprecated + */ + servletNames: Enumeration; + serverInfo: string; + initParameterNames: Enumeration; + servletContextName: string; + servletRegistrations: { [index: string]: ServletRegistration }; + filterRegistrations: { [index: string]: FilterRegistration }; + sessionCookieConfig: SessionCookieConfig; + jspConfigDescriptor: JspConfigDescriptor; + virtualServerName: string; + contextPath: string; + attributeNames: Enumeration; +} + +export interface OncePerRequestFilter extends GenericFilterBean { +} + +export interface SignedJWT extends JWSObject, JWT { + header: JWSHeader; +} + +export interface Principal { + name: string; +} + +export interface AuthenticatedPrincipal { + name: string; +} + +export interface EventDetailsResponse { + id: string; + finished: boolean; + notStarted: boolean; + active: boolean; + proposalsReveal: boolean; + allowVoteChanging: boolean; + highLevelEventResultsWhileVoting: boolean; + highLevelCategoryResultsWhileVoting: boolean; + categoryResultsWhileVoting: boolean; + votingEventType: VotingEventType; + categories: CategoryDetailsResponse[]; + eventInactive: boolean; +} + +export interface Cip30VerificationResult { + validationError?: ValidationError; + address?: any; + ed25519PublicKey: any; + ed25519Signature: any; + message: any; + cosePayload: any; + valid: boolean; +} + export interface URI extends Comparable, Serializable { } @@ -324,8 +514,226 @@ export interface Value extends Iterable { empty: boolean; singleValued: boolean; orNull: T; - lazy: boolean; async: boolean; + lazy: boolean; +} + +export interface Authentication extends Principal, Serializable { + authorities: GrantedAuthority[]; + authenticated: boolean; + principal: any; + details: any; + credentials: any; +} + +export interface CredentialsContainer { +} + +export interface PropertyResolver { +} + +export interface Enumeration { +} + +export interface ClassLoader { +} + +export interface Servlet { + servletConfig: ServletConfig; + servletInfo: string; +} + +export interface ServletRegistration extends Registration { + mappings: string[]; + runAsRole: string; +} + +export interface FilterRegistration extends Registration { + servletNameMappings: string[]; + urlPatternMappings: string[]; +} + +export interface SessionCookieConfig { + domain: string; + name: string; + path: string; + comment: string; + httpOnly: boolean; + maxAge: number; + secure: boolean; +} + +export interface JspConfigDescriptor { + jspPropertyGroups: JspPropertyGroupDescriptor[]; + taglibs: TaglibDescriptor[]; +} + +export interface GenericFilterBean extends Filter, BeanNameAware, EnvironmentAware, EnvironmentCapable, ServletContextAware, InitializingBean, DisposableBean { + filterConfig: FilterConfig; +} + +export interface Payload extends Serializable { + origin: Origin; +} + +export interface Base64URL extends Base64 { +} + +export interface JWSHeader extends CommonSEHeader { + algorithm: JWSAlgorithm; + base64URLEncodePayload: boolean; +} + +export interface JWTClaimsSet extends Serializable { + claims: { [index: string]: any }; + issuer: string; + expirationTime: Date; + audience: string[]; + notBeforeTime: Date; + issueTime: Date; + subject: string; + jwtid: string; +} + +export interface JWSObject extends JOSEObject { + header: JWSHeader; + signature: Base64URL; + state: State; + signingInput: any; +} + +export interface JWT extends Serializable { + header: Header; + parsedParts: Base64URL[]; + parsedString: string; + jwtclaimsSet: JWTClaimsSet; +} + +export interface CategoryDetailsResponse { + id: string; + gdprProtection: boolean; + proposals: ProposalDetailsResponse[]; +} + +export interface ServletConfig { + servletName: string; + initParameterNames: Enumeration; + servletContext: ServletContext; +} + +export interface Registration { + name: string; + className: string; + initParameters: { [index: string]: string }; +} + +export interface JspPropertyGroupDescriptor { + buffer: string; + errorOnUndeclaredNamespace: string; + trimDirectiveWhitespaces: string; + deferredSyntaxAllowedAsLiteral: string; + urlPatterns: string[]; + elIgnored: string; + pageEncoding: string; + scriptingInvalid: string; + includePreludes: string[]; + includeCodas: string[]; + defaultContentType: string; + isXml: string; +} + +export interface TaglibDescriptor { + taglibURI: string; + taglibLocation: string; +} + +export interface Filter { +} + +export interface BeanNameAware extends Aware { +} + +export interface EnvironmentAware extends Aware { +} + +export interface EnvironmentCapable { + environment: Environment; +} + +export interface ServletContextAware extends Aware { +} + +export interface InitializingBean { +} + +export interface DisposableBean { +} + +export interface Base64 extends Serializable { +} + +export interface JWK extends Serializable { + keyStore: KeyStore; + expirationTime: Date; + algorithm: Algorithm; + private: boolean; + x509CertSHA256Thumbprint: Base64URL; + requiredParams: { [index: string]: any }; + keyOperations: KeyOperation[]; + x509CertURL: URI; + /** + * @deprecated + */ + x509CertThumbprint: Base64URL; + x509CertChain: Base64[]; + notBeforeTime: Date; + issueTime: Date; + parsedX509CertChain: X509Certificate[]; + keyType: KeyType; + keyUse: KeyUse; + keyID: string; +} + +export interface JWSAlgorithm extends Algorithm { +} + +export interface JOSEObjectType extends Serializable { + type: string; +} + +export interface CommonSEHeader extends Header { + jwk: JWK; + x509CertSHA256Thumbprint: Base64URL; + x509CertURL: URI; + /** + * @deprecated + */ + x509CertThumbprint: Base64URL; + x509CertChain: Base64[]; + keyID: string; + jwkurl: URI; +} + +export interface JOSEObject extends Serializable { + payload: Payload; + parsedParts: Base64URL[]; + header: Header; + parsedString: string; +} + +export interface Header extends Serializable { + customParams: { [index: string]: any }; + parsedBase64URL: Base64URL; + algorithm: Algorithm; + contentType: string; + type: JOSEObjectType; + criticalParams: string[]; + includedParams: string[]; +} + +export interface ProposalDetailsResponse { + id: string; + name: string; } export interface Comparable { @@ -334,10 +742,87 @@ export interface Comparable { export interface Iterable { } +export interface Aware { +} + +export interface KeyStore { + type: string; + provider: { [index: string]: any }; +} + +export interface Algorithm extends Serializable { + name: string; + requirement: Requirement; +} + +export interface X509Certificate extends Certificate, X509Extension { + subjectX500Principal: X500Principal; + issuerX500Principal: X500Principal; + sigAlgName: string; + serialNumber: number; + /** + * @deprecated since 16 + */ + subjectDN: Principal; + /** + * @deprecated since 16 + */ + issuerDN: Principal; + notBefore: Date; + notAfter: Date; + sigAlgParams: any; + extendedKeyUsage: string[]; + tbscertificate: any; + sigAlgOID: string; + issuerUniqueID: boolean[]; + subjectUniqueID: boolean[]; + issuerAlternativeNames: any[][]; + keyUsage: boolean[]; + signature: any; + basicConstraints: number; + version: number; + subjectAlternativeNames: any[][]; +} + +export interface KeyType extends Serializable { + value: string; + requirement: Requirement; +} + +export interface KeyUse extends Serializable { + value: string; +} + +export interface X500Principal extends Principal, Serializable { + encoded: any; +} + +export interface PublicKey extends Key { +} + +export interface Certificate extends Serializable { + type: string; + encoded: any; + publicKey: PublicKey; +} + +export interface X509Extension { + nonCriticalExtensionOIDs: string[]; + criticalExtensionOIDs: string[]; +} + +export interface Key extends Serializable { + algorithm: string; + encoded: any; + format: string; +} + export type CardanoNetwork = "MAIN" | "PREPROD" | "PREVIEW" | "DEV"; export type OnChainEventType = "COMMITMENTS" | "EVENT_REGISTRATION" | "CATEGORY_REGISTRATION"; +export type Role = "VOTER"; + export type SchemaVersion = "V1"; export type MerkleProofType = "Left" | "Right"; @@ -348,6 +833,20 @@ export type VotingEventType = "USER_BASED" | "STAKE_BASED" | "BALANCE_BASED"; export type VotingPowerAsset = "ADA"; -export type Web3Action = "CAST_VOTE" | "VIEW_VOTE_RECEIPT" | "FULL_METADATA_SCAN"; +export type Web3Action = "CAST_VOTE" | "VIEW_VOTE_RECEIPT" | "LOGIN"; + +export type LoginSystem = "JWT" | "CIP93"; export type FinalityScore = "LOW" | "MEDIUM" | "HIGH" | "VERY_HIGH" | "FINAL"; + +export type SessionTrackingMode = "COOKIE" | "URL" | "SSL"; + +export type State = "UNSIGNED" | "SIGNED" | "VERIFIED"; + +export type ValidationError = "UNKNOWN" | "CIP8_FORMAT_ERROR" | "NO_PUBLIC_KEY"; + +export type Origin = "JSON" | "STRING" | "BYTE_ARRAY" | "BASE64URL" | "JWS_OBJECT" | "SIGNED_JWT"; + +export type KeyOperation = "SIGN" | "VERIFY" | "ENCRYPT" | "DECRYPT" | "WRAP_KEY" | "UNWRAP_KEY" | "DERIVE_KEY" | "DERIVE_BITS"; + +export type Requirement = "REQUIRED" | "RECOMMENDED" | "OPTIONAL"; diff --git a/ui/cip-1694/src/types/voting-ledger-follower-types.ts b/ui/cip-1694/src/types/voting-ledger-follower-types.ts index 59b2a2433..1fa514683 100644 --- a/ui/cip-1694/src/types/voting-ledger-follower-types.ts +++ b/ui/cip-1694/src/types/voting-ledger-follower-types.ts @@ -1,6 +1,6 @@ /* tslint:disable */ /* eslint-disable */ -// Generated using typescript-generator version 3.2.1263 on 2023-09-01 18:09:47. +// Generated using typescript-generator version 3.2.1263 on 2023-09-14 10:20:22. export interface Either extends Value, Serializable { left: L; @@ -43,6 +43,19 @@ export interface EraData { nextEra?: Era; } +export interface EventAdditionalInfo { + id: string; + notStarted: boolean; + finished: boolean; + active: boolean; + proposalsReveal: boolean; +} + +export interface IsMerkleRootPresentResult { + isPresent: boolean; + network: CardanoNetwork; +} + export interface TransactionDetails { transactionHash: string; absoluteSlot: number; @@ -55,16 +68,9 @@ export interface TransactionDetails { export interface TransactionDetailsBuilder { } -export interface TransactionMetadataLabelCbor { - tx_hash: string; - slot: number; - cbor_metadata: string; -} - -export interface TransactionMetadataLabelCborBuilder { -} - export interface AbstractTimestampEntity { + createdAt: Date; + updatedAt: Date; } export interface Category extends AbstractTimestampEntity { @@ -82,17 +88,20 @@ export interface CategoryBuilder { export interface Event extends AbstractTimestampEntity { id: string; - team: string; + organisers: string; votingEventType: VotingEventType; votingPowerAsset?: VotingPowerAsset; allowVoteChanging: boolean; - categoryResultsWhileVoting: boolean; - highLevelResultsWhileVoting: boolean; + highLevelEpochResultsWhileVoting?: boolean; + highLevelCategoryResultsWhileVoting?: boolean; + categoryResultsWhileVoting?: boolean; startEpoch?: number; endEpoch?: number; + snapshotEpoch?: number; + proposalsRevealEpoch?: number; startSlot?: number; endSlot?: number; - snapshotEpoch?: number; + proposalsRevealSlot?: number; version: SchemaVersion; categories: Category[]; absoluteSlot: number; @@ -132,23 +141,28 @@ export interface CategoryPresentationBuilder { export interface EventPresentation { id: string; - team: string; + organisers: string; votingEventType: VotingEventType; startSlot?: number; endSlot?: number; + proposalsRevealSlot?: number; startEpoch?: number; eventStartDate?: Date; eventEndDate?: Date; + proposalsRevealDate?: Date; snapshotTime?: Date; endEpoch?: number; snapshotEpoch?: number; + proposalsRevealEpoch?: number; categories: CategoryPresentation[]; active: boolean; + highLevelCategoryResultsWhileVoting: boolean; + categoryResultsWhileVoting: boolean; + highLevelEventResultsWhileVoting: boolean; allowVoteChanging: boolean; - notStarted: boolean; + proposalsReveal: boolean; finished: boolean; - categoryResultsWhileVoting: boolean; - highLevelResultsWhileVoting: boolean; + notStarted: boolean; } export interface EventPresentationBuilder { @@ -162,20 +176,9 @@ export interface ProposalPresentation { export interface ProposalPresentationBuilder { } -export interface CIP93Envelope { - uri: string; - action: string; - actionText: string; - slot: string; - data: T; -} - -export interface CIP93EnvelopeBuilder { -} - export interface CategoryRegistrationEnvelope { type: OnChainEventType; - name: string; + id: string; event: string; schemaVersion: string; creationSlot: number; @@ -201,29 +204,27 @@ export interface CommitmentsEnvelopeBuilder { export interface EventRegistrationEnvelope { type: OnChainEventType; name: string; - team: string; + organisers: string; schemaVersion: string; creationSlot: number; allowVoteChanging: boolean; + highLevelEventResultsWhileVoting: boolean; + highLevelCategoryResultsWhileVoting: boolean; categoryResultsWhileVoting: boolean; - highLevelResultsWhileVoting: boolean; votingEventType: VotingEventType; votingPowerAsset?: VotingPowerAsset; - startEpoch?: number; - endEpoch?: number; startSlot?: number; endSlot?: number; + proposalsRevealSlot?: number; + startEpoch?: number; + endEpoch?: number; snapshotEpoch?: number; + proposalsRevealEpoch?: number; } export interface EventRegistrationEnvelopeBuilder { } -export interface FullMetadataScanEnvelope { - address: string; - network: string; -} - export interface ProposalEnvelope { id: string; name: string; @@ -232,14 +233,6 @@ export interface ProposalEnvelope { export interface ProposalEnvelopeBuilder { } -export interface SignedWeb3Request { - coseSignature: string; - cosePublicKey?: string; -} - -export interface SignedWeb3RequestBuilder { -} - export interface AccountService { } @@ -249,15 +242,6 @@ export interface DefaultAccountService extends AccountService { export interface DefaultAccountService__BeanDefinitions { } -export interface StakeAddressVerificationService { -} - -export interface StakeAddressVerificationService__Autowiring { -} - -export interface StakeAddressVerificationService__BeanDefinitions { -} - export interface BlockchainDataChainTipService { chainTip: Either; } @@ -305,9 +289,6 @@ export interface ChainSyncService__BeanDefinitions { export interface CustomEpochService { } -export interface CustomEpochService__Autowiring { -} - export interface CustomEpochService__BeanDefinitions { } @@ -320,13 +301,10 @@ export interface CustomEraService__Autowiring { export interface CustomEraService__BeanDefinitions { } -export interface ExpirationService { -} - -export interface ExpirationService__Autowiring { +export interface EventAdditionalInfoService { } -export interface ExpirationService__BeanDefinitions { +export interface EventAdditionalInfoService__BeanDefinitions { } export interface YaciStoreTipHealthIndicator extends HealthIndicator { @@ -335,15 +313,6 @@ export interface YaciStoreTipHealthIndicator extends HealthIndicator { export interface YaciStoreTipHealthIndicator__BeanDefinitions { } -export interface JsonService { -} - -export interface JsonService__Autowiring { -} - -export interface JsonService__BeanDefinitions { -} - export interface CustomMetadataProcessor { } @@ -353,21 +322,9 @@ export interface CustomMetadataProcessor__Autowiring { export interface CustomMetadataProcessor__BeanDefinitions { } -export interface CustomMetadataService { -} - -export interface CustomMetadataService__Autowiring { -} - -export interface CustomMetadataService__BeanDefinitions { -} - export interface ReferenceDataService { } -export interface ReferenceDataService__Autowiring { -} - export interface ReferenceDataService__BeanDefinitions { } @@ -395,10 +352,13 @@ export interface MerkleRootHashService__BeanDefinitions { export interface VotingPowerService { } -export interface VotingPowerService__Autowiring { +export interface VotingPowerService__BeanDefinitions { } -export interface VotingPowerService__BeanDefinitions { +export interface YaciCustomMetadataStorage extends TxMetadataStorage { +} + +export interface YaciCustomMetadataStorage__BeanDefinitions { } export interface Problem { @@ -406,8 +366,8 @@ export interface Problem { type: URI; parameters: { [index: string]: any }; status: StatusType; - detail: string; title: string; + detail: string; } export interface Serializable { @@ -419,6 +379,9 @@ export interface Exception extends Throwable { export interface HealthIndicator extends HealthContributor { } +export interface TxMetadataStorage { +} + export interface URI extends Comparable, Serializable { } @@ -428,11 +391,11 @@ export interface StatusType { } export interface Value extends Iterable { - singleValued: boolean; empty: boolean; + singleValued: boolean; orNull: T; - lazy: boolean; async: boolean; + lazy: boolean; } export interface Throwable extends Serializable { @@ -477,6 +440,4 @@ export type VotingEventType = "USER_BASED" | "STAKE_BASED" | "BALANCE_BASED"; export type VotingPowerAsset = "ADA"; -export type Web3Action = "CAST_VOTE" | "VIEW_VOTE_RECEIPT" | "FULL_METADATA_SCAN"; - export type Era = "Byron" | "Shelley" | "Allegra" | "Mary" | "Alonzo" | "Babbage"; diff --git a/version.txt b/version.txt index e7ccda1a3..aa0499977 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.2.35 +0.2.37