Skip to content

Commit

Permalink
feat: historical proposals (#112)
Browse files Browse the repository at this point in the history
* feat: add proposalsByGroupPolicyAddress query

* fix: modify ProposalFragment

* WIP: graphql-request and react-query

* feat: use react-query and graphql-request

* feat: add useGraphQLClient and useGroupHistoricalProposals

* fix: use provider for graphql client

* chore: add overwrite flag in codegen config

* chore: tiny formatting change for graphql query

* fix: parse historical proposals into UIProposal[]

* feat: add historical proposals on groups-proposals-table

* fix: rename fragment masking helper function

* fix: add eslintignore to lint command and ignore src/gql/

* fix: apply linter on some files

* chore: fix lint problems in hooks/use-query

* chore: remove sort

* fix: do not show execute button for historical proposals

* fix: use better implementation with simpler type for useGroupHistoricalProposals

* feat: render historical proposal on proposal page

* fix: react key warning

* feat: add proposal final tally table

* chore: configure netlify with indexer api urls

* fix: typo in final tally results table title

Co-authored-by: Marie Gauthier <[email protected]>

* fix: remove console.log statement

Co-authored-by: Marie Gauthier <[email protected]>

* refactor: is inline conditionals for rendering the proposal page

* fix: make onVote optional in ProposalSummary

* chore: remove unused AllProposals query

* refactor: follow hyphenated naming convention

* refactor: dedupe conversion of historical proposal into UIProposals

* fix: use an index for proposal messages list

* refactor: add helper function for converting executor result to code

* fix: show final tally result table if voting period is closed

* chore: rename historicalProposal to historical-proposal

---------

Co-authored-by: Marie Gauthier <[email protected]>
Co-authored-by: Ryan Christoffersen <[email protected]>
  • Loading branch information
3 people authored Aug 3, 2023
1 parent dad0e76 commit d04c72f
Show file tree
Hide file tree
Showing 27 changed files with 5,131 additions and 93 deletions.
1 change: 1 addition & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
VITE_LOCAL_HOSTNAME=http://127.0.0.1
VITE_PROXY_URL_REGEN_MAINNET=http://api.registry.regen.network
VITE_PROXY_URL_REGEN_TESTNET=http://api-staging.registry.regen.network
VITE_INDEXER_GRAPHQL_API=http://localhost:5000/indexer/graphql
3 changes: 2 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ node_modules
dist
dist-ssr
*.local
node_modules/*
node_modules/*
src/gql/*
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ node_modules
dist
dist-ssr
*.local
node_modules/*
node_modules/*
src/gql/
15 changes: 15 additions & 0 deletions indexer-codegen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { CodegenConfig } from '@graphql-codegen/cli'

const config: CodegenConfig = {
overwrite: true,
schema: 'http://localhost:5000/indexer/graphql',
documents: ['src/graphql/indexer/**/*.graphql'],
generates: {
'./src/gql/': {
preset: 'client',
presetConfig: { fragmentMasking: { unmaskFunctionName: 'getFragmentData' } },
},
},
}

export default config
6 changes: 6 additions & 0 deletions netlify.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
[context.production.environment]
VITE_INDEXER_GRAPHQL_API="https://api.registry.regen.network/indexer/graphql"

[context.deploy-preview.environment]
VITE_INDEXER_GRAPHQL_API="https://api-staging.registry.regen.network/indexer/graphql"

[[redirects]]
from = "/*"
to = "/index.html"
Expand Down
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"build": "tsc && vite build",
"serve": "vite preview",
"gen:theme-typings": "chakra-cli tokens ./src/theme",
"lint:fix": "eslint ./src --ext .jsx,.js,.ts,.tsx --fix --ignore-path ./.gitignore",
"lint:fix": "eslint ./src --ext .jsx,.js,.ts,.tsx --fix --ignore-path ./.gitignore --ignore-path ./.eslintignore",
"lint:format": "prettier --loglevel warn --write \"./**/*.{js,jsx,ts,tsx,css,md,json}\" ",
"lint": "yarn lint:format && yarn lint:fix ",
"test": "yarn vitest",
Expand Down Expand Up @@ -38,6 +38,8 @@
"chart.js": "^4.3.0",
"dayjs": "^1.11.8",
"framer-motion": "^10.12.8",
"graphql": "^16.7.1",
"graphql-request": "^5.0.0",
"ipfs-http-client": "^60.0.0",
"jotai": "^2.1.1",
"react": "^18.2.0",
Expand All @@ -55,6 +57,8 @@
"@babel/core": "^7.21.8",
"@chakra-ui/cli": "^2.4.1",
"@chakra-ui/storybook-addon": "^4.0.17",
"@graphql-codegen/cli": "^4.0.1",
"@graphql-codegen/client-preset": "^4.0.1",
"@keplr-wallet/types": "^0.11.59",
"@storybook/addon-actions": "^7.0.18",
"@storybook/addon-essentials": "^7.0.18",
Expand Down Expand Up @@ -87,8 +91,9 @@
"rollup-plugin-visualizer": "^5.9.0",
"storybook": "^7.0.18",
"storybook-addon-react-router-v6": "^1.0.2",
"ts-node": "^10.9.1",
"ts-proto": "^1.148.2",
"typescript": "^5.1.3",
"typescript": "^5.1.6",
"vite": "4.2.3",
"vite-plugin-checker": "^0.6.0",
"vite-plugin-fonts": "^0.7.0",
Expand Down
3 changes: 3 additions & 0 deletions src/components/organisms/group-proposals-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ export const GroupProposalsTable = ({
const baseProps = { key, proposal }
switch (proposal.status) {
case ProposalStatus.PROPOSAL_STATUS_ACCEPTED:
if (proposal.historical) {
return <Row {...baseProps} />
}
return <ExecuteRow {...baseProps} onExecute={onExecute} />
default:
return <Row {...baseProps} />
Expand Down
34 changes: 34 additions & 0 deletions src/components/organisms/proposal-final-tally-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { TallyResult } from '@regen-network/api/types/codegen/cosmos/group/v1/types'

import { Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@/atoms'
import { TableTitlebar } from '@/molecules/table-titlebar'

export const ProposalFinalTallyTable = ({
finalTallyResult,
}: {
finalTallyResult: TallyResult
}) => {
return (
<TableContainer>
<TableTitlebar title="Final Tally Results" />
<Table variant="striped" size="lg">
<Thead>
<Tr>
<Th>Yes</Th>
<Th>No</Th>
<Th>Abstain</Th>
<Th>No with veto</Th>
</Tr>
</Thead>
<Tbody>
<Tr>
<Td>{finalTallyResult.yesCount}</Td>
<Td>{finalTallyResult.noCount}</Td>
<Td>{finalTallyResult.abstainCount}</Td>
<Td>{finalTallyResult.noWithVetoCount}</Td>
</Tr>
</Tbody>
</Table>
</TableContainer>
)
}
19 changes: 12 additions & 7 deletions src/components/organisms/proposal-summary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,16 @@ export const ProposalSummary = ({
proposal,
userVote,
votes,
votingClosed,
}: {
group: UIGroup
onVote: (option: VoteOptionType) => void
onVote?: (option: VoteOptionType) => void
proposal: UIProposal
userVote?: Vote
votes?: Vote[]
votingClosed?: boolean
}) => {
const cardBgDark = useColorModeValue('gray.100', 'gray.700')
const now = new Date()
const votingClosed = new Date(proposal.votingPeriodEnd || now).getTime() < now.getTime()
const proposalFinalized =
proposal.status.toString() === 'PROPOSAL_STATUS_ACCEPTED' ||
proposal.status.toString() === 'PROPOSAL_STATUS_REJECTED'
Expand All @@ -65,8 +65,8 @@ export const ProposalSummary = ({
</Stack>
<Heading>{proposal.metadata.title}</Heading>
<Text>{proposal.metadata.summary}</Text>
{proposal.messages.map((msg) =>
renderMessage(msg, proposal.groupPolicyAddress),
{proposal.messages.map((msg, index) =>
renderMessage(msg, proposal.groupPolicyAddress, index),
)}
</Stack>
</CardBody>
Expand Down Expand Up @@ -108,12 +108,13 @@ export const ProposalSummary = ({

// TODO: https://github.com/regen-network/regen-js/issues/71
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function renderMessage(msg: any, groupPolicyAddress: string) {
function renderMessage(msg: any, groupPolicyAddress: string, index: number) {
if (!msg) return null
switch (msg.typeUrl) {
case '/cosmos.bank.v1beta1.MsgSend':
return (
<SendReview
key={index}
groupPolicyAddress={groupPolicyAddress}
values={
{
Expand All @@ -129,6 +130,7 @@ function renderMessage(msg: any, groupPolicyAddress: string) {
case '/cosmos.staking.v1beta1.MsgDelegate':
return (
<StakeReview
key={index}
groupPolicyAddress={groupPolicyAddress}
values={
{
Expand All @@ -144,6 +146,7 @@ function renderMessage(msg: any, groupPolicyAddress: string) {
case '/cosmos.staking.v1beta1.MsgBeginRedelegate':
return (
<StakeReview
key={index}
groupPolicyAddress={groupPolicyAddress}
values={
{
Expand All @@ -160,6 +163,7 @@ function renderMessage(msg: any, groupPolicyAddress: string) {
case '/cosmos.staking.v1beta1.MsgUndelegate':
return (
<StakeReview
key={index}
groupPolicyAddress={groupPolicyAddress}
values={
{
Expand All @@ -175,6 +179,7 @@ function renderMessage(msg: any, groupPolicyAddress: string) {
case '/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward':
return (
<StakeReview
key={index}
groupPolicyAddress={groupPolicyAddress}
values={
{
Expand All @@ -186,6 +191,6 @@ function renderMessage(msg: any, groupPolicyAddress: string) {
/>
)
default:
return <JSONDisplay data={msg} />
return <JSONDisplay key={index} data={msg} />
}
}
66 changes: 66 additions & 0 deletions src/gql/fragment-masking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core';
import { FragmentDefinitionNode } from 'graphql';
import { Incremental } from './graphql';


export type FragmentType<TDocumentType extends DocumentTypeDecoration<any, any>> = TDocumentType extends DocumentTypeDecoration<
infer TType,
any
>
? [TType] extends [{ ' $fragmentName'?: infer TKey }]
? TKey extends string
? { ' $fragmentRefs'?: { [key in TKey]: TType } }
: never
: never
: never;

// return non-nullable if `fragmentType` is non-nullable
export function getFragmentData<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>
): TType;
// return nullable if `fragmentType` is nullable
export function getFragmentData<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null | undefined
): TType | null | undefined;
// return array of non-nullable if `fragmentType` is array of non-nullable
export function getFragmentData<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
): ReadonlyArray<TType>;
// return array of nullable if `fragmentType` is array of nullable
export function getFragmentData<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): ReadonlyArray<TType> | null | undefined;
export function getFragmentData<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): TType | ReadonlyArray<TType> | null | undefined {
return fragmentType as any;
}


export function makeFragmentData<
F extends DocumentTypeDecoration<any, any>,
FT extends ResultOf<F>
>(data: FT, _fragment: F): FragmentType<F> {
return data as FragmentType<F>;
}
export function isFragmentReady<TQuery, TFrag>(
queryNode: DocumentTypeDecoration<TQuery, any>,
fragmentNode: TypedDocumentNode<TFrag>,
data: FragmentType<TypedDocumentNode<Incremental<TFrag>, any>> | null | undefined
): data is FragmentType<typeof fragmentNode> {
const deferredFields = (queryNode as { __meta__?: { deferredFields: Record<string, (keyof TFrag)[]> } }).__meta__
?.deferredFields;

if (!deferredFields) return true;

const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined;
const fragName = fragDef?.name?.value;

const fields = (fragName && deferredFields[fragName]) || [];
return fields.length > 0 && fields.every(field => data && field in data);
}
52 changes: 52 additions & 0 deletions src/gql/gql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* eslint-disable */
import * as types from './graphql';
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';

/**
* Map of all GraphQL operations in the project.
*
* This map has several performance disadvantages:
* 1. It is not tree-shakeable, so it will include all operations in the project.
* 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
* 3. It does not support dead code elimination, so it will add unused operations.
*
* Therefore it is highly recommended to use the babel or swc plugin for production.
*/
const documents = {
"fragment ProposalItem on Proposal {\n type\n blockHeight\n txIdx\n msgIdx\n chainNum\n timestamp\n txHash\n id: proposalId\n status\n groupPolicyAddress\n groupPolicyVersion\n metadata\n proposers\n submitTime\n groupVersion\n groupPolicyAddress\n finalTallyResult\n votingPeriodEnd\n executorResult\n messages\n}": types.ProposalItemFragmentDoc,
"query ProposalsByGroupPolicyAddress($groupPolicyAddress: String!) {\n allProposals(condition: {groupPolicyAddress: $groupPolicyAddress}) {\n nodes {\n ...ProposalItem\n }\n }\n}": types.ProposalsByGroupPolicyAddressDocument,
"query ProposalsByProposalId($proposalId: BigInt!) {\n allProposals(condition: {proposalId: $proposalId}) {\n nodes {\n ...ProposalItem\n }\n }\n}": types.ProposalsByProposalIdDocument,
};

/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*
*
* @example
* ```ts
* const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`);
* ```
*
* The query argument is unknown!
* Please regenerate the types.
*/
export function graphql(source: string): unknown;

/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "fragment ProposalItem on Proposal {\n type\n blockHeight\n txIdx\n msgIdx\n chainNum\n timestamp\n txHash\n id: proposalId\n status\n groupPolicyAddress\n groupPolicyVersion\n metadata\n proposers\n submitTime\n groupVersion\n groupPolicyAddress\n finalTallyResult\n votingPeriodEnd\n executorResult\n messages\n}"): (typeof documents)["fragment ProposalItem on Proposal {\n type\n blockHeight\n txIdx\n msgIdx\n chainNum\n timestamp\n txHash\n id: proposalId\n status\n groupPolicyAddress\n groupPolicyVersion\n metadata\n proposers\n submitTime\n groupVersion\n groupPolicyAddress\n finalTallyResult\n votingPeriodEnd\n executorResult\n messages\n}"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "query ProposalsByGroupPolicyAddress($groupPolicyAddress: String!) {\n allProposals(condition: {groupPolicyAddress: $groupPolicyAddress}) {\n nodes {\n ...ProposalItem\n }\n }\n}"): (typeof documents)["query ProposalsByGroupPolicyAddress($groupPolicyAddress: String!) {\n allProposals(condition: {groupPolicyAddress: $groupPolicyAddress}) {\n nodes {\n ...ProposalItem\n }\n }\n}"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "query ProposalsByProposalId($proposalId: BigInt!) {\n allProposals(condition: {proposalId: $proposalId}) {\n nodes {\n ...ProposalItem\n }\n }\n}"): (typeof documents)["query ProposalsByProposalId($proposalId: BigInt!) {\n allProposals(condition: {proposalId: $proposalId}) {\n nodes {\n ...ProposalItem\n }\n }\n}"];

export function graphql(source: string) {
return (documents as any)[source] ?? {};
}

export type DocumentType<TDocumentNode extends DocumentNode<any, any>> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never;
Loading

0 comments on commit d04c72f

Please sign in to comment.