void init_handlers()

in src/node/rpc/member_frontend.h [502:1396]


    void init_handlers() override
    {
      CommonEndpointRegistry::init_handlers();

      const AuthnPolicies member_sig_only = {member_signature_auth_policy};

      const AuthnPolicies member_cert_or_sig = {
        member_cert_auth_policy, member_signature_auth_policy};

      //! A member acknowledges state
      auto ack = [this](auto& ctx, nlohmann::json&& params) {
        const auto& caller_identity =
          ctx.template get_caller<ccf::MemberSignatureAuthnIdentity>();
        const auto& signed_request = caller_identity.signed_request;

        auto mas = ctx.tx.rw(this->network.member_acks);
        const auto ma = mas->get(caller_identity.member_id);
        if (!ma)
        {
          return make_error(
            HTTP_STATUS_FORBIDDEN,
            ccf::errors::AuthorizationFailed,
            fmt::format(
              "No ACK record exists for caller {}.",
              caller_identity.member_id));
        }

        const auto digest = params.get<StateDigest>();
        if (ma->state_digest != digest.state_digest)
        {
          return make_error(
            HTTP_STATUS_BAD_REQUEST,
            ccf::errors::StateDigestMismatch,
            "Submitted state digest is not valid.");
        }

        auto sig = ctx.tx.rw(this->network.signatures);
        const auto s = sig->get();
        if (!s)
        {
          mas->put(caller_identity.member_id, MemberAck({}, signed_request));
        }
        else
        {
          mas->put(
            caller_identity.member_id, MemberAck(s->root, signed_request));
        }

        // update member status to ACTIVE
        GenesisGenerator g(this->network, ctx.tx);
        try
        {
          g.activate_member(caller_identity.member_id);
        }
        catch (const std::logic_error& e)
        {
          return make_error(
            HTTP_STATUS_FORBIDDEN,
            ccf::errors::AuthorizationFailed,
            fmt::format("Error activating new member: {}", e.what()));
        }

        auto service_status = g.get_service_status();
        if (!service_status.has_value())
        {
          return make_error(
            HTTP_STATUS_INTERNAL_SERVER_ERROR,
            ccf::errors::InternalError,
            "No service currently available.");
        }

        auto members = ctx.tx.rw(this->network.member_info);
        auto member_info = members->get(caller_identity.member_id);
        if (
          service_status.value() == ServiceStatus::OPEN &&
          g.is_recovery_member(caller_identity.member_id))
        {
          // When the service is OPEN and the new active member is a recovery
          // member, all recovery members are allocated new recovery shares
          try
          {
            share_manager.shuffle_recovery_shares(ctx.tx);
          }
          catch (const std::logic_error& e)
          {
            return make_error(
              HTTP_STATUS_INTERNAL_SERVER_ERROR,
              ccf::errors::InternalError,
              fmt::format("Error issuing new recovery shares: {}", e.what()));
          }
        }
        return make_success();
      };
      make_endpoint("/ack", HTTP_POST, json_adapter(ack), member_sig_only)
        .set_auto_schema<StateDigest, void>()
        .install();

      //! A member asks for a fresher state digest
      auto update_state_digest = [this](auto& ctx, nlohmann::json&&) {
        const auto member_id = get_caller_member_id(ctx);
        if (!member_id.has_value())
        {
          return make_error(
            HTTP_STATUS_FORBIDDEN,
            ccf::errors::AuthorizationFailed,
            "Caller is a not a valid member id");
        }

        auto mas = ctx.tx.rw(this->network.member_acks);
        auto sig = ctx.tx.rw(this->network.signatures);
        auto ma = mas->get(member_id.value());
        if (!ma)
        {
          return make_error(
            HTTP_STATUS_FORBIDDEN,
            ccf::errors::AuthorizationFailed,
            fmt::format(
              "No ACK record exists for caller {}.", member_id.value()));
        }

        auto s = sig->get();
        if (s)
        {
          ma->state_digest = s->root.hex_str();
          mas->put(member_id.value(), ma.value());
        }
        nlohmann::json j;
        j["state_digest"] = ma->state_digest;

        return make_success(j);
      };
      make_endpoint(
        "/ack/update_state_digest",
        HTTP_POST,
        json_adapter(update_state_digest),
        member_cert_or_sig)
        .set_auto_schema<void, StateDigest>()
        .install();

      auto get_encrypted_recovery_share = [this](auto& ctx, nlohmann::json&&) {
        const auto member_id = get_caller_member_id(ctx);
        if (!member_id.has_value())
        {
          return make_error(
            HTTP_STATUS_FORBIDDEN,
            ccf::errors::AuthorizationFailed,
            "Member is unknown.");
        }
        if (!check_member_active(ctx.tx, member_id.value()))
        {
          return make_error(
            HTTP_STATUS_FORBIDDEN,
            ccf::errors::AuthorizationFailed,
            "Only active members are given recovery shares.");
        }

        auto encrypted_share =
          share_manager.get_encrypted_share(ctx.tx, member_id.value());

        if (!encrypted_share.has_value())
        {
          return make_error(
            HTTP_STATUS_NOT_FOUND,
            ccf::errors::ResourceNotFound,
            fmt::format(
              "Recovery share not found for member {}.", member_id->value()));
        }

        return make_success(
          GetRecoveryShare::Out{crypto::b64_from_raw(encrypted_share.value())});
      };
      make_endpoint(
        "/recovery_share",
        HTTP_GET,
        json_adapter(get_encrypted_recovery_share),
        member_cert_or_sig)
        .set_auto_schema<GetRecoveryShare>()
        .install();

      auto submit_recovery_share = [this](auto& ctx, nlohmann::json&& params) {
        // Only active members can submit their shares for recovery
        const auto member_id = get_caller_member_id(ctx);
        if (!member_id.has_value())
        {
          return make_error(
            HTTP_STATUS_FORBIDDEN,
            ccf::errors::AuthorizationFailed,
            "Member is unknown.");
        }
        if (!check_member_active(ctx.tx, member_id.value()))
        {
          return make_error(
            HTTP_STATUS_FORBIDDEN,
            errors::AuthorizationFailed,
            "Member is not active");
        }

        GenesisGenerator g(this->network, ctx.tx);
        if (
          g.get_service_status() != ServiceStatus::WAITING_FOR_RECOVERY_SHARES)
        {
          return make_error(
            HTTP_STATUS_FORBIDDEN,
            errors::ServiceNotWaitingForRecoveryShares,
            "Service is not waiting for recovery shares");
        }

        if (context.get_node_state().is_reading_private_ledger())
        {
          return make_error(
            HTTP_STATUS_FORBIDDEN,
            errors::NodeAlreadyRecovering,
            "Node is already recovering private ledger");
        }

        const auto in = params.get<SubmitRecoveryShare::In>();
        auto raw_recovery_share = crypto::raw_from_b64(in.share);
        OPENSSL_cleanse(const_cast<char*>(in.share.data()), in.share.size());

        size_t submitted_shares_count = 0;
        try
        {
          submitted_shares_count = share_manager.submit_recovery_share(
            ctx.tx, member_id.value(), raw_recovery_share);
        }
        catch (const std::exception& e)
        {
          constexpr auto error_msg = "Error submitting recovery shares";
          LOG_FAIL_FMT(error_msg);
          LOG_DEBUG_FMT("Error: {}", e.what());
          return make_error(
            HTTP_STATUS_INTERNAL_SERVER_ERROR,
            errors::InternalError,
            error_msg);
        }
        OPENSSL_cleanse(raw_recovery_share.data(), raw_recovery_share.size());

        if (submitted_shares_count < g.get_recovery_threshold())
        {
          // The number of shares required to re-assemble the secret has not yet
          // been reached
          return make_success(SubmitRecoveryShare::Out{fmt::format(
            "{}/{} recovery shares successfully submitted.",
            submitted_shares_count,
            g.get_recovery_threshold())});
        }

        LOG_DEBUG_FMT(
          "Reached recovery threshold {}", g.get_recovery_threshold());

        try
        {
          context.get_node_state().initiate_private_recovery(ctx.tx);
        }
        catch (const std::exception& e)
        {
          // Clear the submitted shares if combination fails so that members can
          // start over.
          constexpr auto error_msg = "Failed to initiate private recovery";
          LOG_FAIL_FMT(error_msg);
          LOG_DEBUG_FMT("Error: {}", e.what());
          share_manager.clear_submitted_recovery_shares(ctx.tx);
          ctx.rpc_ctx->set_apply_writes(true);
          return make_error(
            HTTP_STATUS_INTERNAL_SERVER_ERROR,
            errors::InternalError,
            error_msg);
        }

        share_manager.clear_submitted_recovery_shares(ctx.tx);

        return make_success(SubmitRecoveryShare::Out{fmt::format(
          "{}/{} recovery shares successfully submitted. End of recovery "
          "procedure initiated.",
          submitted_shares_count,
          g.get_recovery_threshold())});
      };
      make_endpoint(
        "/recovery_share",
        HTTP_POST,
        json_adapter(submit_recovery_share),
        member_cert_or_sig)
        .set_auto_schema<SubmitRecoveryShare>()
        .install();

      using JWTKeyMap = std::map<JwtKeyId, KeyIdInfo>;

      auto get_jwt_keys = [this](auto& ctx, nlohmann::json&& body) {
        auto keys = ctx.tx.ro(network.jwt_public_signing_keys);
        auto keys_to_issuer = ctx.tx.ro(network.jwt_public_signing_key_issuer);

        JWTKeyMap kmap;
        keys->foreach(
          [&kmap, &keys_to_issuer](const auto& kid, const auto& kpem) {
            auto issuer = keys_to_issuer->get(kid);
            if (!issuer.has_value())
            {
              throw std::logic_error(fmt::format("kid {} has no issuer", kid));
            }
            kmap.emplace(
              kid, KeyIdInfo{issuer.value(), crypto::cert_der_to_pem(kpem)});
            return true;
          });

        return make_success(kmap);
      };
      make_endpoint(
        "/jwt_keys/all", HTTP_GET, json_adapter(get_jwt_keys), no_auth_required)
        .set_auto_schema<void, JWTKeyMap>()
        .install();

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wc99-extensions"

      auto post_proposals_js = [this](ccf::endpoints::EndpointContext& ctx) {
        const auto& caller_identity =
          ctx.get_caller<ccf::MemberSignatureAuthnIdentity>();
        if (!check_member_active(ctx.tx, caller_identity.member_id))
        {
          ctx.rpc_ctx->set_error(
            HTTP_STATUS_FORBIDDEN,
            ccf::errors::AuthorizationFailed,
            "Member is not active.");
          return;
        }

        if (!consensus)
        {
          ctx.rpc_ctx->set_error(
            HTTP_STATUS_INTERNAL_SERVER_ERROR,
            ccf::errors::InternalError,
            "No consensus available.");
          return;
        }

        ProposalId proposal_id;
        if (consensus->type() == ConsensusType::CFT)
        {
          auto root_at_read = ctx.tx.get_root_at_read_version();
          if (!root_at_read.has_value())
          {
            ctx.rpc_ctx->set_error(
              HTTP_STATUS_INTERNAL_SERVER_ERROR,
              ccf::errors::InternalError,
              "Proposal failed to bind to state.");
            return;
          }

          // caller_identity.request_digest is set when getting the
          // MemberSignatureAuthnIdentity identity. The proposal id is a
          // digest of the root of the state tree at the read version and the
          // request digest.
          std::vector<uint8_t> acc(
            root_at_read.value().h.begin(), root_at_read.value().h.end());
          acc.insert(
            acc.end(),
            caller_identity.request_digest.begin(),
            caller_identity.request_digest.end());
          const crypto::Sha256Hash proposal_digest(acc);
          proposal_id = proposal_digest.hex_str();
        }
        else
        {
          proposal_id = fmt::format(
            "{:02x}", fmt::join(caller_identity.request_digest, ""));
        }

        auto constitution = ctx.tx.ro(network.constitution)->get();
        if (!constitution.has_value())
        {
          ctx.rpc_ctx->set_error(
            HTTP_STATUS_INTERNAL_SERVER_ERROR,
            ccf::errors::InternalError,
            "No constitution is set - proposals cannot be evaluated");
          return;
        }

        auto validate_script = constitution.value();

        js::Runtime rt;
        js::Context context(rt);
        rt.add_ccf_classdefs();
        js::populate_global(
          nullptr,
          nullptr,
          nullptr,
          std::nullopt,
          nullptr,
          nullptr,
          nullptr,
          nullptr,
          nullptr,
          nullptr,
          context);

        auto validate_func = context.function(
          validate_script, "validate", "public:ccf.gov.constitution[0]");

        auto body =
          reinterpret_cast<const char*>(ctx.rpc_ctx->get_request_body().data());
        auto body_len = ctx.rpc_ctx->get_request_body().size();

        auto proposal = context.new_string_len(body, body_len);
        auto val = context.call(validate_func, {proposal});

        if (JS_IsException(val))
        {
          js::js_dump_error(context);
          ctx.rpc_ctx->set_error(
            HTTP_STATUS_INTERNAL_SERVER_ERROR,
            ccf::errors::InternalError,
            "Failed to execute validation");
          return;
        }

        if (!JS_IsObject(val))
        {
          ctx.rpc_ctx->set_error(
            HTTP_STATUS_INTERNAL_SERVER_ERROR,
            ccf::errors::InternalError,
            "Validation failed to return an object");
          return;
        }

        std::string description;
        auto desc = context(JS_GetPropertyStr(context, val, "description"));
        if (JS_IsString(desc))
        {
          description = context.to_str(desc).value_or("");
        }

        auto valid = context(JS_GetPropertyStr(context, val, "valid"));
        if (!JS_ToBool(context, valid))
        {
          ctx.rpc_ctx->set_error(
            HTTP_STATUS_BAD_REQUEST,
            ccf::errors::ProposalFailedToValidate,
            fmt::format("Proposal failed to validate: {}", description));
          return;
        }

        auto pm = ctx.tx.rw<ccf::jsgov::ProposalMap>(Tables::PROPOSALS);
        // Introduce a read dependency, so that if identical proposal
        // creations are in-flight and reading at the same version, all except
        // the first conflict and are re-executed. If we ever produce a
        // proposal ID which already exists, we must have a hash collision.
        if (pm->has(proposal_id))
        {
          ctx.rpc_ctx->set_error(
            HTTP_STATUS_INTERNAL_SERVER_ERROR,
            ccf::errors::InternalError,
            "Proposal ID collision.");
          return;
        }
        pm->put(proposal_id, ctx.rpc_ctx->get_request_body());

        auto pi =
          ctx.tx.rw<ccf::jsgov::ProposalInfoMap>(Tables::PROPOSALS_INFO);
        pi->put(
          proposal_id,
          {caller_identity.member_id, ccf::ProposalState::OPEN, {}});

        record_voting_history(
          ctx.tx, caller_identity.member_id, caller_identity.signed_request);

        auto rv = resolve_proposal(
          ctx.tx,
          proposal_id,
          ctx.rpc_ctx->get_request_body(),
          constitution.value());

        if (rv.state == ProposalState::FAILED)
        {
          // If the proposal failed to apply, we want to discard the tx and not
          // apply its side-effects to the KV state.
          ctx.rpc_ctx->set_error(
            HTTP_STATUS_INTERNAL_SERVER_ERROR,
            ccf::errors::InternalError,
            fmt::format("{}", rv.failure));
          return;
        }
        else
        {
          pi->put(
            proposal_id,
            {caller_identity.member_id,
             rv.state,
             {},
             {},
             std::nullopt,
             rv.failure});
          ctx.rpc_ctx->set_response_header(
            http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON);
          ctx.rpc_ctx->set_response_body(nlohmann::json(rv).dump());
          ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
          return;
        }
      };

      make_endpoint("/proposals", HTTP_POST, post_proposals_js, member_sig_only)
        .set_auto_schema<jsgov::Proposal, jsgov::ProposalInfoSummary>()
        .install();

      auto get_proposal_js =
        [this](endpoints::ReadOnlyEndpointContext& ctx, nlohmann::json&&) {
          const auto member_id = get_caller_member_id(ctx);
          if (!member_id.has_value())
          {
            return make_error(
              HTTP_STATUS_FORBIDDEN,
              ccf::errors::AuthorizationFailed,
              "Member is unknown.");
          }
          if (!check_member_active(ctx.tx, member_id.value()))
          {
            return make_error(
              HTTP_STATUS_FORBIDDEN,
              ccf::errors::AuthorizationFailed,
              "Member is not active.");
          }

          // Take expand=ballots, return eg. "ballots": 3 if not set
          // or "ballots": list of ballots in full if passed

          ProposalId proposal_id;
          std::string error;
          if (!get_proposal_id_from_path(
                ctx.rpc_ctx->get_request_path_params(), proposal_id, error))
          {
            return make_error(
              HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName, error);
          }

          auto pm = ctx.tx.ro<ccf::jsgov::ProposalMap>(Tables::PROPOSALS);
          auto p = pm->get(proposal_id);

          if (!p)
          {
            return make_error(
              HTTP_STATUS_NOT_FOUND,
              ccf::errors::ProposalNotFound,
              fmt::format("Proposal {} does not exist.", proposal_id));
          }

          auto pi =
            ctx.tx.ro<ccf::jsgov::ProposalInfoMap>(Tables::PROPOSALS_INFO);
          auto pi_ = pi->get(proposal_id);

          if (!pi_)
          {
            return make_error(
              HTTP_STATUS_INTERNAL_SERVER_ERROR,
              ccf::errors::InternalError,
              fmt::format(
                "No proposal info associated with {} exists.", proposal_id));
          }

          return make_success(pi_.value());
        };

      make_read_only_endpoint(
        "/proposals/{proposal_id}",
        HTTP_GET,
        json_read_only_adapter(get_proposal_js),
        member_cert_or_sig)
        .set_auto_schema<void, jsgov::ProposalInfo>()
        .install();

      auto withdraw_js = [this](
                           endpoints::EndpointContext& ctx, nlohmann::json&&) {
        const auto& caller_identity =
          ctx.template get_caller<ccf::MemberSignatureAuthnIdentity>();
        if (!check_member_active(ctx.tx, caller_identity.member_id))
        {
          return make_error(
            HTTP_STATUS_FORBIDDEN,
            ccf::errors::AuthorizationFailed,
            "Member is not active.");
        }

        ProposalId proposal_id;
        std::string error;
        if (!get_proposal_id_from_path(
              ctx.rpc_ctx->get_request_path_params(), proposal_id, error))
        {
          return make_error(
            HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName, error);
        }

        auto pi =
          ctx.tx.rw<ccf::jsgov::ProposalInfoMap>(Tables::PROPOSALS_INFO);
        auto pi_ = pi->get(proposal_id);

        if (!pi_)
        {
          return make_error(
            HTTP_STATUS_BAD_REQUEST,
            ccf::errors::ProposalNotFound,
            fmt::format("Proposal {} does not exist.", proposal_id));
        }

        if (caller_identity.member_id != pi_->proposer_id)
        {
          return make_error(
            HTTP_STATUS_FORBIDDEN,
            ccf::errors::AuthorizationFailed,
            fmt::format(
              "Proposal {} can only be withdrawn by proposer {}, not caller "
              "{}.",
              proposal_id,
              pi_->proposer_id,
              caller_identity.member_id));
        }

        if (pi_->state != ProposalState::OPEN)
        {
          return make_error(
            HTTP_STATUS_BAD_REQUEST,
            ccf::errors::ProposalNotOpen,
            fmt::format(
              "Proposal {} is currently in state {} - only {} proposals can be "
              "withdrawn.",
              proposal_id,
              pi_->state,
              ProposalState::OPEN));
        }

        pi_->state = ProposalState::WITHDRAWN;
        pi->put(proposal_id, pi_.value());

        remove_all_other_non_open_proposals(ctx.tx, proposal_id);
        record_voting_history(
          ctx.tx, caller_identity.member_id, caller_identity.signed_request);

        return make_success(pi_.value());
      };

      make_endpoint(
        "/proposals/{proposal_id}/withdraw",
        HTTP_POST,
        json_adapter(withdraw_js),
        member_sig_only)
        .set_auto_schema<void, jsgov::ProposalInfo>()
        .install();

      auto get_proposal_actions_js =
        [this](ccf::endpoints::ReadOnlyEndpointContext& ctx) {
          const auto& caller_identity =
            ctx.get_caller<ccf::MemberSignatureAuthnIdentity>();
          if (!check_member_active(ctx.tx, caller_identity.member_id))
          {
            ctx.rpc_ctx->set_error(
              HTTP_STATUS_FORBIDDEN,
              ccf::errors::AuthorizationFailed,
              "Member is not active.");
            return;
          }

          ProposalId proposal_id;
          std::string error;
          if (!get_proposal_id_from_path(
                ctx.rpc_ctx->get_request_path_params(), proposal_id, error))
          {
            ctx.rpc_ctx->set_error(
              HTTP_STATUS_BAD_REQUEST,
              ccf::errors::InvalidResourceName,
              std::move(error));
            return;
          }

          auto pm = ctx.tx.ro<ccf::jsgov::ProposalMap>(Tables::PROPOSALS);
          auto p = pm->get(proposal_id);

          if (!p)
          {
            ctx.rpc_ctx->set_error(
              HTTP_STATUS_NOT_FOUND,
              ccf::errors::ProposalNotFound,
              fmt::format("Proposal {} does not exist.", proposal_id));
            return;
          }

          ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
          ctx.rpc_ctx->set_response_header(
            http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON);
          ctx.rpc_ctx->set_response_body(std::move(p.value()));
        };

      make_read_only_endpoint(
        "/proposals/{proposal_id}/actions",
        HTTP_GET,
        get_proposal_actions_js,
        member_cert_or_sig)
        .set_auto_schema<void, jsgov::Proposal>()
        .install();

      auto vote_js = [this](ccf::endpoints::EndpointContext& ctx) {
        const auto& caller_identity =
          ctx.get_caller<ccf::MemberSignatureAuthnIdentity>();
        if (!check_member_active(ctx.tx, caller_identity.member_id))
        {
          ctx.rpc_ctx->set_error(
            HTTP_STATUS_FORBIDDEN,
            ccf::errors::AuthorizationFailed,
            "Member is not active.");
          return;
        }

        ProposalId proposal_id;
        std::string error;
        if (!get_proposal_id_from_path(
              ctx.rpc_ctx->get_request_path_params(), proposal_id, error))
        {
          ctx.rpc_ctx->set_error(
            HTTP_STATUS_BAD_REQUEST,
            ccf::errors::InvalidResourceName,
            std::move(error));
          return;
        }

        auto constitution = ctx.tx.ro(network.constitution)->get();
        if (!constitution.has_value())
        {
          ctx.rpc_ctx->set_error(
            HTTP_STATUS_INTERNAL_SERVER_ERROR,
            ccf::errors::InternalError,
            "No constitution is set - proposals cannot be evaluated");
          return;
        }

        auto pi =
          ctx.tx.rw<ccf::jsgov::ProposalInfoMap>(Tables::PROPOSALS_INFO);
        auto pi_ = pi->get(proposal_id);
        if (!pi_)
        {
          ctx.rpc_ctx->set_error(
            HTTP_STATUS_NOT_FOUND,
            ccf::errors::ProposalNotFound,
            fmt::format("Could not find proposal {}.", proposal_id));
          return;
        }

        if (pi_.value().state != ProposalState::OPEN)
        {
          ctx.rpc_ctx->set_error(
            HTTP_STATUS_BAD_REQUEST,
            ccf::errors::ProposalNotOpen,
            fmt::format(
              "Proposal {} is currently in state {} - only {} proposals can "
              "receive votes.",
              proposal_id,
              pi_.value().state,
              ProposalState::OPEN));
          return;
        }

        auto pm = ctx.tx.ro<ccf::jsgov::ProposalMap>(Tables::PROPOSALS);
        auto p = pm->get(proposal_id);

        if (!p)
        {
          ctx.rpc_ctx->set_error(
            HTTP_STATUS_NOT_FOUND,
            ccf::errors::ProposalNotFound,
            fmt::format("Proposal {} does not exist.", proposal_id));
          return;
        }

        if (pi_->ballots.find(caller_identity.member_id) != pi_->ballots.end())
        {
          ctx.rpc_ctx->set_error(
            HTTP_STATUS_BAD_REQUEST,
            ccf::errors::VoteAlreadyExists,
            "Vote already submitted.");
          return;
        }
        // Validate vote

        auto params = nlohmann::json::parse(ctx.rpc_ctx->get_request_body());

        {
          js::Runtime rt;
          js::Context context(rt);
          auto ballot_func =
            context.function(params["ballot"], "vote", "body[\"ballot\"]");
        }

        pi_->ballots[caller_identity.member_id] = params["ballot"];
        pi->put(proposal_id, pi_.value());

        // Do we still need to do this?
        record_voting_history(
          ctx.tx, caller_identity.member_id, caller_identity.signed_request);

        auto rv = resolve_proposal(
          ctx.tx, proposal_id, p.value(), constitution.value());
        if (rv.state == ProposalState::FAILED)
        {
          // If the proposal failed to apply, we want to discard the tx and not
          // apply its side-effects to the KV state.
          ctx.rpc_ctx->set_error(
            HTTP_STATUS_INTERNAL_SERVER_ERROR,
            ccf::errors::InternalError,
            fmt::format("{}", rv.failure));
          return;
        }
        else
        {
          pi_.value().state = rv.state;
          pi_.value().final_votes = rv.votes;
          pi_.value().vote_failures = rv.vote_failures;
          pi_.value().failure = rv.failure;
          pi->put(proposal_id, pi_.value());
          ctx.rpc_ctx->set_response_header(
            http::headers::CONTENT_TYPE, http::headervalues::contenttype::JSON);
          ctx.rpc_ctx->set_response_body(nlohmann::json(rv).dump());
          ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK);
          return;
        }
      };
      make_endpoint(
        "/proposals/{proposal_id}/ballots", HTTP_POST, vote_js, member_sig_only)
        .set_auto_schema<jsgov::Ballot, jsgov::ProposalInfoSummary>()
        .install();

      auto get_vote_js =
        [this](endpoints::ReadOnlyEndpointContext& ctx, nlohmann::json&&) {
          const auto member_id = get_caller_member_id(ctx);
          if (!member_id.has_value())
          {
            return make_error(
              HTTP_STATUS_FORBIDDEN,
              ccf::errors::AuthorizationFailed,
              "Member is unknown.");
          }
          if (!check_member_active(ctx.tx, member_id.value()))
          {
            return make_error(
              HTTP_STATUS_FORBIDDEN,
              ccf::errors::AuthorizationFailed,
              "Member is not active.");
          }

          std::string error;
          ProposalId proposal_id;
          if (!get_proposal_id_from_path(
                ctx.rpc_ctx->get_request_path_params(), proposal_id, error))
          {
            return make_error(
              HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName, error);
          }

          MemberId vote_member_id;
          if (!get_member_id_from_path(
                ctx.rpc_ctx->get_request_path_params(), vote_member_id, error))
          {
            return make_error(
              HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName, error);
          }

          auto pi =
            ctx.tx.ro<ccf::jsgov::ProposalInfoMap>(Tables::PROPOSALS_INFO);
          auto pi_ = pi->get(proposal_id);
          if (!pi_)
          {
            return make_error(
              HTTP_STATUS_NOT_FOUND,
              ccf::errors::ProposalNotFound,
              fmt::format("Proposal {} does not exist.", proposal_id));
          }

          const auto vote_it = pi_->ballots.find(vote_member_id);
          if (vote_it == pi_->ballots.end())
          {
            return make_error(
              HTTP_STATUS_NOT_FOUND,
              ccf::errors::VoteNotFound,
              fmt::format(
                "Member {} has not voted for proposal {}.",
                vote_member_id,
                proposal_id));
          }

          return make_success(jsgov::Ballot{vote_it->second});
        };
      make_read_only_endpoint(
        "/proposals/{proposal_id}/ballots/{member_id}",
        HTTP_GET,
        json_read_only_adapter(get_vote_js),
        member_cert_or_sig)
        .set_auto_schema<void, jsgov::Ballot>()
        .install();

#pragma clang diagnostic pop
    }