Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions govtool/backend/sql/get-proposal-survey-tally.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
WITH context AS (
SELECT
gov_action_proposal.id AS proposal_db_id,
off_chain_vote_data.json->>'surveyTxId' AS survey_tx_id,
survey_meta.json->'surveyDetails' AS survey_details
FROM gov_action_proposal
JOIN tx AS creator_tx ON creator_tx.id = gov_action_proposal.tx_id
LEFT JOIN voting_anchor ON voting_anchor.id = gov_action_proposal.voting_anchor_id
LEFT JOIN off_chain_vote_data ON off_chain_vote_data.voting_anchor_id = voting_anchor.id
LEFT JOIN tx survey_tx
ON encode(survey_tx.hash, 'hex') = LOWER(off_chain_vote_data.json->>'surveyTxId')
LEFT JOIN tx_metadata survey_meta
ON survey_meta.tx_id = survey_tx.id
AND survey_meta.key = 17
AND survey_meta.json ? 'surveyDetails'
WHERE encode(creator_tx.hash, 'hex') = ?
AND gov_action_proposal.index = ?
LIMIT 1
),
LatestDrepDistr AS (
SELECT
*,
ROW_NUMBER() OVER (PARTITION BY hash_id ORDER BY epoch_no DESC) AS rn
FROM drep_distr
),
responses AS (
SELECT
encode(drep_hash.raw, 'hex') AS response_credential,
tx_metadata.id AS metadata_id,
block.slot_no AS slot_no,
tx.block_index AS tx_index,
tx_metadata.json->'surveyResponse' AS survey_response,
COALESCE(ldd.amount, 0) AS drep_voting_power
FROM context
JOIN tx_metadata
ON tx_metadata.key = 17
AND tx_metadata.json ? 'surveyResponse'
AND LOWER(tx_metadata.json->'surveyResponse'->>'surveyTxId') = LOWER(context.survey_tx_id)
JOIN tx ON tx.id = tx_metadata.tx_id
JOIN block ON block.id = tx.block_id
JOIN voting_procedure
ON voting_procedure.tx_id = tx.id
AND voting_procedure.gov_action_proposal_id = context.proposal_db_id
AND voting_procedure.drep_voter IS NOT NULL
JOIN drep_hash ON drep_hash.id = voting_procedure.drep_voter
LEFT JOIN LatestDrepDistr ldd
ON ldd.hash_id = voting_procedure.drep_voter
AND ldd.rn = 1
),
latest AS (
SELECT DISTINCT ON (response_credential)
*
FROM responses
ORDER BY
response_credential,
slot_no DESC,
tx_index DESC,
metadata_id DESC
),
question_counts AS (
SELECT
answer->>'questionId' AS question_id,
COUNT(*)::bigint AS answers_count,
SUM(
CASE
WHEN ? = 'StakeBased' THEN drep_voting_power
ELSE 1
END
)::bigint AS weighted_count
FROM latest
JOIN LATERAL jsonb_array_elements(
CASE
WHEN jsonb_typeof(latest.survey_response->'answers') = 'array'
THEN latest.survey_response->'answers'
ELSE '[]'::jsonb
END
) AS answer ON true
GROUP BY answer->>'questionId'
),
method_results AS (
SELECT
COALESCE(
jsonb_agg(
jsonb_build_object(
'questionId', q->>'questionId',
'question', q->>'question',
'methodType', q->>'methodType',
'count', COALESCE(question_counts.answers_count, 0),
'weightedCount', COALESCE(question_counts.weighted_count, 0)
)
),
'[]'::jsonb
) AS results
FROM context
LEFT JOIN LATERAL jsonb_array_elements(context.survey_details->'questions') AS q ON true
LEFT JOIN question_counts ON question_counts.question_id = q->>'questionId'
)
SELECT
jsonb_build_object(
'surveyTxId', context.survey_tx_id,
'totals', jsonb_build_object(
'totalSeen', COALESCE((SELECT COUNT(*) FROM responses), 0),
'valid', COALESCE((SELECT COUNT(*) FROM latest), 0),
'invalid', GREATEST(
COALESCE((SELECT COUNT(*) FROM responses), 0)
- COALESCE((SELECT COUNT(*) FROM latest), 0),
0
),
'deduped', GREATEST(
COALESCE((SELECT COUNT(*) FROM responses), 0)
- COALESCE((SELECT COUNT(*) FROM latest), 0),
0
),
'uniqueResponders', COALESCE((SELECT COUNT(*) FROM latest), 0)
),
'roleResults', jsonb_build_array(
jsonb_build_object(
'responderRole', 'DRep',
'weightingMode', ?,
'totals', jsonb_build_object(
'totalSeen', COALESCE((SELECT COUNT(*) FROM responses), 0),
'valid', COALESCE((SELECT COUNT(*) FROM latest), 0),
'invalid', GREATEST(
COALESCE((SELECT COUNT(*) FROM responses), 0)
- COALESCE((SELECT COUNT(*) FROM latest), 0),
0
),
'deduped', GREATEST(
COALESCE((SELECT COUNT(*) FROM responses), 0)
- COALESCE((SELECT COUNT(*) FROM latest), 0),
0
),
'uniqueResponders', COALESCE((SELECT COUNT(*) FROM latest), 0)
),
'methodResults', COALESCE(method_results.results, '[]'::jsonb)
)
),
'errors', '[]'::jsonb
) AS survey_tally
FROM context
CROSS JOIN method_results;
122 changes: 122 additions & 0 deletions govtool/backend/sql/get-proposal-survey.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
WITH proposal_data AS (
SELECT
gov_action_proposal.type::text AS proposal_type,
encode(creator_tx.hash, 'hex') AS proposal_tx_id,
gov_action_proposal.index AS proposal_index,
gov_action_proposal.expiration AS expiration_epoch,
off_chain_vote_data.json AS anchor_json,
off_chain_vote_data.json->>'kind' AS kind,
off_chain_vote_data.json->>'surveyTxId' AS survey_tx_id
FROM gov_action_proposal
JOIN tx AS creator_tx ON creator_tx.id = gov_action_proposal.tx_id
LEFT JOIN voting_anchor ON voting_anchor.id = gov_action_proposal.voting_anchor_id
LEFT JOIN off_chain_vote_data ON off_chain_vote_data.voting_anchor_id = voting_anchor.id
WHERE encode(creator_tx.hash, 'hex') = ?
AND gov_action_proposal.index = ?
LIMIT 1
),
survey_payload AS (
SELECT
proposal_data.*,
survey_meta.json->'surveyDetails' AS survey_details,
CASE
WHEN (survey_meta.json->'surveyDetails'->>'endEpoch') ~ '^[0-9]+$'
THEN (survey_meta.json->'surveyDetails'->>'endEpoch')::bigint
ELSE NULL
END AS survey_end_epoch,
survey_meta.json->'surveyDetails'->'roleWeighting' AS role_weighting,
CASE
WHEN proposal_type IN ('TreasuryWithdrawals', 'NewConstitution')
THEN '["DRep","CC"]'::jsonb
ELSE '["DRep","SPO","CC"]'::jsonb
END AS action_eligibility
FROM proposal_data
LEFT JOIN tx survey_tx
ON encode(survey_tx.hash, 'hex') = LOWER(proposal_data.survey_tx_id)
LEFT JOIN tx_metadata survey_meta
ON survey_meta.tx_id = survey_tx.id
AND survey_meta.key = 17
AND survey_meta.json ? 'surveyDetails'
)
SELECT
jsonb_build_object(
'linked', survey_tx_id IS NOT NULL,
'surveyTxId', survey_tx_id,
'linkValidation', jsonb_build_object(
'valid',
(
kind = 'cardano-governance-survey-link'
AND survey_tx_id IS NOT NULL
AND survey_details IS NOT NULL
)
AND (
role_weighting IS NOT NULL
AND (
SELECT COUNT(*)
FROM jsonb_object_keys(role_weighting) AS role_key
WHERE action_eligibility ? role_key
) > 0
),
'errors',
to_jsonb(array_remove(ARRAY[
CASE WHEN kind IS DISTINCT FROM 'cardano-governance-survey-link'
THEN 'Anchor metadata kind is not cardano-governance-survey-link.' END,
CASE WHEN survey_tx_id IS NULL
THEN 'Missing surveyTxId in anchor metadata.' END,
CASE WHEN survey_tx_id IS NOT NULL AND survey_details IS NULL
THEN 'Referenced surveyTxId has no label 17 surveyDetails payload.' END,
CASE WHEN role_weighting IS NOT NULL
AND (
SELECT COUNT(*)
FROM jsonb_object_keys(role_weighting) AS role_key
WHERE action_eligibility ? role_key
) = 0
THEN 'Linked survey has no eligible roles after governance action filtering.' END
], NULL)),
'actionEligibility', action_eligibility,
'linkedRoleWeighting',
COALESCE(
(
SELECT jsonb_object_agg(role_key, role_weighting->role_key)
FROM jsonb_object_keys(role_weighting) AS role_key
WHERE action_eligibility ? role_key
),
'null'::jsonb
),
'linkedActionId', jsonb_build_object(
'txId', proposal_tx_id,
'govActionIx', proposal_index
)
),
'surveyDetails', survey_details,
'surveyDetailsValidation', jsonb_build_object(
'valid',
(
survey_details IS NOT NULL
AND role_weighting IS NOT NULL
AND jsonb_typeof(role_weighting) = 'object'
AND jsonb_object_length(role_weighting) > 0
AND survey_end_epoch IS NOT NULL
AND survey_end_epoch = expiration_epoch
),
'errors',
to_jsonb(array_remove(ARRAY[
CASE WHEN survey_details IS NULL
THEN 'Missing surveyDetails payload.' END,
CASE WHEN survey_details IS NOT NULL
AND (
role_weighting IS NULL
OR jsonb_typeof(role_weighting) <> 'object'
OR jsonb_object_length(role_weighting) = 0
)
THEN 'surveyDetails.roleWeighting must be a non-empty object.' END,
CASE WHEN survey_details IS NOT NULL
AND survey_end_epoch IS NULL
THEN 'surveyDetails.endEpoch is required.' END,
CASE WHEN survey_end_epoch IS NOT NULL
AND survey_end_epoch <> expiration_epoch
THEN 'surveyDetails.endEpoch must exactly match the governance action expiration epoch.' END
], NULL))
)
) AS survey_payload
FROM survey_payload;
21 changes: 20 additions & 1 deletion govtool/backend/src/VVA/API.hs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import qualified Data.Text as Text
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Encoding as TL
import Data.Time (TimeZone, localTimeToUTC)
import Data.Time.LocalTime (TimeZone, getCurrentTimeZone)
import Data.Time.LocalTime (getCurrentTimeZone)
import qualified Data.Vector as V
import Data.Hashable (hash, hashWithSalt)

Expand All @@ -50,6 +50,7 @@ import qualified VVA.Epoch as Epoch
import qualified VVA.Ipfs as Ipfs
import VVA.Network as Network
import qualified VVA.Proposal as Proposal
import qualified VVA.Survey as Survey
import qualified VVA.Transaction as Transaction
import qualified VVA.Types as Types
import VVA.Types (App, AppEnv (..),
Expand Down Expand Up @@ -89,6 +90,8 @@ type VVAApi =
:> QueryParam "search" Text
:> Get '[JSON] ListProposalsResponse
:<|> "proposal" :> "get" :> Capture "proposalId" GovActionId :> QueryParam "drepId" HexText :> Get '[JSON] GetProposalResponse
:<|> "proposal" :> "survey" :> Capture "proposalId" GovActionId :> Get '[JSON] AnyValue
:<|> "proposal" :> "survey" :> Capture "proposalId" GovActionId :> "tally" :> QueryParam "weighting" Text :> Get '[JSON] AnyValue
:<|> "proposal" :> "enacted-details" :> QueryParam "type" GovernanceActionType :> Get '[JSON] (Maybe EnactedProposalDetailsResponse)
:<|> "epoch" :> "params" :> Get '[JSON] GetCurrentEpochParamsResponse
:<|> "transaction" :> "status" :> Capture "transactionId" HexText :> Get '[JSON] GetTransactionStatusResponse
Expand All @@ -109,6 +112,8 @@ server = upload
:<|> getStakeKeyVotingPower
:<|> listProposals
:<|> getProposal
:<|> getProposalSurvey
:<|> getProposalSurveyTally
:<|> getEnactedProposalDetails
:<|> getCurrentEpochParams
:<|> getTransactionStatus
Expand Down Expand Up @@ -485,6 +490,20 @@ getProposal g@(GovActionId govActionTxHash govActionIndex) mDrepId' = do
, getProposalResponseVote = voteResponse
}

getProposalSurvey :: App m => GovActionId -> m AnyValue
getProposalSurvey (GovActionId govActionTxHash govActionIndex) = do
payload <- Survey.getProposalSurvey (unHexText govActionTxHash) govActionIndex
pure $ AnyValue (Just payload)

getProposalSurveyTally :: App m => GovActionId -> Maybe Text -> m AnyValue
getProposalSurveyTally (GovActionId govActionTxHash govActionIndex) mWeighting = do
let weighting = fromMaybe "CredentialBased" mWeighting
payload <- Survey.getProposalSurveyTally
(unHexText govActionTxHash)
govActionIndex
weighting
pure $ AnyValue (Just payload)

getEnactedProposalDetails :: App m => Maybe GovernanceActionType -> m (Maybe EnactedProposalDetailsResponse)
getEnactedProposalDetails maybeType = do
let proposalType = maybe "HardForkInitiation" governanceActionTypeToText maybeType
Expand Down
Loading