Skip to content
Merged
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
167 changes: 108 additions & 59 deletions apps/api-gateway/schema.graphql

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const VOTER = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const FULL_VOTE_ITEM = {
voter: VOTER,
proposalId: "proposal-1",
choice: { "1": 1 },
choice: ["1"],
vp: 100,
reason: "",
created: 1700000000,
Expand Down
7 changes: 4 additions & 3 deletions apps/api/src/mappers/votes/offchainVotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,13 @@ export const OffchainVoteChoiceSchema = z
z
.number()
.int()
.transform((val) => [val]),
z.array(z.number().int()),
.transform((val) => [val.toString()]),
z.array(z.string()),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Accept numeric-array vote choices

This schema now only accepts z.array(z.string()) for array-form choices, but Snapshot approval/ranked-choice votes are commonly stored as numeric arrays (e.g. [1, 2]). Those rows will fail OffchainVotesResponseSchema.parse(...) and make /offchain/votes and /offchain/proposals/{id}/votes return 500 whenever such votes are present. Keep supporting numeric arrays (or transform them) to preserve compatibility with existing offchain vote data.

Useful? React with 👍 / 👎.

z.record(z.string(), z.number()).transform((val) => Object.keys(val)),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Convert object vote choices to integers

When Snapshot stores a vote choice as an object (e.g. approval/weighted formats like { "1": 1 }), this branch returns Object.keys(val), which is always a string[]. That violates this endpoint’s declared choice: [Int]! contract and changes runtime payloads from numeric choices to strings for exactly the inputs this commit is trying to support, so consumers that rely on integer arrays will receive the wrong type.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Filter zero-weight entries from object choices

Mapping object-form choices with Object.keys(val) treats every key as selected, even when its weight is 0; this produces incorrect displayed choices for weighted/quadratic votes when the stored payload includes zero-value options. Because weighted vote payloads can include all choice keys with zero defaults, this branch should filter by positive weight before extracting keys to avoid misreporting voter intent.

Useful? React with 👍 / 👎.

])
.openapi("SnapshotVoteChoice", {
type: "array",
items: { type: "integer", nullable: false },
items: { type: "string", nullable: false },
});

export const OffchainVoteResponseSchema = z
Expand Down
8 changes: 8 additions & 0 deletions apps/dashboard/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ const config: StorybookConfig = {
"@apollo/client": dirname(
require.resolve("@apollo/client/package.json"),
),
"@anticapture/graphql-client$": resolve(
__dirname,
"../../../packages/graphql-client/generated/hooks.ts",
),
"@anticapture/graphql-client/hooks": resolve(
__dirname,
"../../../packages/graphql-client/generated/hooks.ts",
),
};
return config;
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
"use client";

import type { GetOffchainVotesByProposalIdQuery } from "@anticapture/graphql-client";
import { useGetOffchainVotesByProposalIdQuery } from "@anticapture/graphql-client/hooks";
import type { OffchainVote } from "@anticapture/graphql-client";
import {
OrderDirection,
QueryInput_VotesOffchainByProposalId_OrderBy,
useGetOffchainVotesByProposalIdQuery,
} from "@anticapture/graphql-client/hooks";
import type { ColumnDef } from "@tanstack/react-table";
import { CheckCircle2, CircleMinus, Inbox, XCircle } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { Address } from "viem";

import { VotesTable } from "@/features/governance/components/proposal-overview/VotesTable";
import { BlankSlate, SkeletonRow } from "@/shared/components";
import { BlankSlate } from "@/shared/components/design-system/blank-slate/BlankSlate";
import { Button } from "@/shared/components/design-system/buttons/button/Button";
import { SkeletonRow } from "@/shared/components/skeletons/SkeletonRow";
import { EnsAvatar } from "@/shared/components/design-system/avatars/ens-avatar/EnsAvatar";
import { ArrowState, ArrowUpDown } from "@/shared/components/icons";
import type { DaoIdEnum } from "@/shared/types/daos";
import { formatNumberUserReadable } from "@/shared/utils";
import { formatNumberUserReadable } from "@/shared/utils/formatNumberUserReadable";
import { getAuthHeaders } from "@/shared/utils/server-utils";

type OffchainVoteItem = NonNullable<
NonNullable<
GetOffchainVotesByProposalIdQuery["votesOffchainByProposalId"]
>["items"][number]
>;
import { CopyAndPasteButton } from "@/shared/components/buttons/CopyAndPasteButton";

const LOADING_ROW = "__LOADING_ROW__";

Expand Down Expand Up @@ -63,9 +65,42 @@ export const OffchainVotesContent = ({
}: OffchainVotesContentProps) => {
const loadingRowRef = useRef<HTMLTableRowElement>(null);

const [orderBy, setOrderBy] =
useState<QueryInput_VotesOffchainByProposalId_OrderBy>(
QueryInput_VotesOffchainByProposalId_OrderBy.Timestamp,
);
const [orderDirection, setOrderDirection] = useState<OrderDirection>(
OrderDirection.Desc,
);

const handleSort = useCallback(
(field: QueryInput_VotesOffchainByProposalId_OrderBy) => {
if (orderBy === field) {
setOrderDirection((prev) =>
prev === OrderDirection.Asc
? OrderDirection.Desc
: OrderDirection.Asc,
);
} else {
setOrderBy(field);
setOrderDirection(OrderDirection.Desc);
}
},
[orderBy],
);

const { data, loading, error, fetchMore } =
useGetOffchainVotesByProposalIdQuery({
variables: { id: proposalId, limit: 10, skip: 0 },
variables: {
id: proposalId,
limit: 10,
skip: 0,
fromDate: null,
toDate: null,
voterAddresses: null,
orderBy,
orderDirection,
},
context: {
headers: {
"anticapture-dao-id": daoId,
Expand All @@ -78,7 +113,7 @@ export const OffchainVotesContent = ({
const votes = useMemo(
() =>
(data?.votesOffchainByProposalId?.items ?? []).filter(
(vote): vote is OffchainVoteItem => vote !== null,
(vote): vote is OffchainVote => vote !== null,
),
[data],
);
Expand Down Expand Up @@ -116,7 +151,9 @@ export const OffchainVotesContent = ({
}, [hasNextPage, loading, loadMore]);

const tableData = useMemo(() => {
const rows: (OffchainVoteItem & { isSubRow?: boolean })[] = [];
const rows: (Omit<OffchainVote, "proposalId" | "proposalTitle"> & {
isSubRow?: boolean;
})[] = [];

votes.forEach((vote) => {
rows.push(vote);
Expand All @@ -142,10 +179,10 @@ export const OffchainVotesContent = ({
});
}

return rows as OffchainVoteItem[];
return rows as OffchainVote[];
}, [votes, hasNextPage, loading]);

const columns: ColumnDef<OffchainVoteItem>[] = useMemo(
const columns: ColumnDef<OffchainVote>[] = useMemo(
() => [
{
accessorKey: "voter",
Expand Down Expand Up @@ -199,6 +236,11 @@ export const OffchainVotesContent = ({
variant="rounded"
showName={true}
isDashed={true}
/>{" "}
<CopyAndPasteButton
textToCopy={voter}
className="text-secondary hover:text-primary ml-2 inline-flex p-1 align-middle transition-colors"
iconSize="md"
/>
</div>
);
Expand Down Expand Up @@ -238,9 +280,27 @@ export const OffchainVotesContent = ({
accessorKey: "created",
size: 120,
header: () => (
<div className="text-table-header flex h-8 w-full items-center justify-start px-2">
<p>Date</p>
</div>
<Button
variant="ghost"
size="sm"
className="text-secondary w-full justify-start"
onClick={() =>
handleSort(QueryInput_VotesOffchainByProposalId_OrderBy.Timestamp)
}
>
<h4 className="text-table-header whitespace-nowrap">Date</h4>
<ArrowUpDown
props={{ className: "size-4 ml-1" }}
activeState={
orderBy ===
QueryInput_VotesOffchainByProposalId_OrderBy.Timestamp
? orderDirection === OrderDirection.Asc
? ArrowState.UP
: ArrowState.DOWN
: ArrowState.DEFAULT
}
/>
</Button>
),
cell: ({ row }) => {
const voter = row.getValue("voter") as string;
Expand Down Expand Up @@ -284,9 +344,31 @@ export const OffchainVotesContent = ({
accessorKey: "vp",
size: 160,
header: () => (
<div className="text-table-header flex h-8 w-full items-center justify-start px-2">
<p>Voting Power</p>
</div>
<Button
variant="ghost"
size="sm"
className="text-secondary w-full justify-start"
onClick={() =>
handleSort(
QueryInput_VotesOffchainByProposalId_OrderBy.VotingPower,
)
}
>
<h4 className="text-table-header whitespace-nowrap">
Voting Power
</h4>
<ArrowUpDown
props={{ className: "size-4 ml-1" }}
activeState={
orderBy ===
QueryInput_VotesOffchainByProposalId_OrderBy.VotingPower
? orderDirection === OrderDirection.Asc
? ArrowState.UP
: ArrowState.DOWN
: ArrowState.DEFAULT
}
/>
</Button>
),
cell: ({ row }) => {
const voter = row.getValue("voter") as string;
Expand Down Expand Up @@ -316,7 +398,7 @@ export const OffchainVotesContent = ({
},
},
],
[totalVotingPower, choices],
[totalVotingPower, choices, orderBy, orderDirection, handleSort],
);

if (error) return <div>Error: {error.message}</div>;
Expand All @@ -326,7 +408,7 @@ export const OffchainVotesContent = ({
<div className="w-full lg:p-4">
<VotesTable
columns={columns}
data={Array.from({ length: 7 }, () => ({}) as OffchainVoteItem)}
data={Array.from({ length: 7 }, () => ({}) as OffchainVote)}
/>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"test": "jest",
"format": "prettier --write .",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"build-storybook": "pnpm --filter @anticapture/graphql-client codegen && storybook build",
"clean": "rm -rf node_modules .next out build coverage storybook-static *.tsbuildinfo *storybook.log",
"typecheck": "tsc --noEmit"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,3 @@ query GetOffchainProposal($id: String!) {
}
}

query GetOffchainVotesByProposalId(
$id: String!
$skip: Int
$limit: Int = 10
) {
votesOffchainByProposalId(id: $id, skip: $skip, limit: $limit) {
items {
voter
choice
vp
reason
created
}
totalCount
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
query GetOffchainVotesByProposalId(
$id: String!
$fromDate: Int
$limit: Int
$orderBy: queryInput_votesOffchainByProposalId_orderBy = timestamp
$orderDirection: OrderDirection = asc
$skip: Int
$toDate: Int
$voterAddresses: [String]
) {
votesOffchainByProposalId(
fromDate: $fromDate
limit: $limit
orderBy: $orderBy
orderDirection: $orderDirection
toDate: $toDate
voterAddresses: $voterAddresses
id: $id
skip: $skip
) {
totalCount
items {
choice
created
proposalId
proposalTitle
reason
voter
vp
}
}
}
5 changes: 4 additions & 1 deletion turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,12 @@
"outputs": ["dist/**", ".next/**"]
},
"storybook": {
"outputs": ["dist/**"]
"dependsOn": ["@anticapture/graphql-client#codegen"],
"persistent": true,
"cache": false
},
"build-storybook": {
"dependsOn": ["@anticapture/graphql-client#codegen"],
"outputs": ["storybook-static/**"]
},
"start": {
Expand Down
Loading