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
24 changes: 24 additions & 0 deletions apps/namadillo/src/atoms/proposals/atoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
fetchPaginatedProposals,
fetchProposalById,
fetchProposalDataById,
fetchProposalVotes,
fetchVotedProposalsByAccount,
} from "./functions";

Expand Down Expand Up @@ -68,6 +69,29 @@ export const proposalVoteFamily = atomFamily((id: bigint) =>
})
);

// New atom for fetching all votes for a specific proposal
export const proposalVotesFamily = atomFamily((id: bigint) =>
atomWithQuery((get) => {
const api = get(indexerApiAtom);
const enablePolling = get(shouldUpdateProposalAtom);

return {
// TODO: subscribe to indexer events when it's done
refetchInterval: enablePolling ? 2000 : false, // Slightly slower polling for votes
queryKey: ["proposal-votes", id.toString()],
queryFn: () => fetchProposalVotes(api, id),
// Handle errors gracefully
retry: (failureCount, error) => {
// Only retry network errors, not API method not found errors
if (error?.message?.includes("not a function")) {
return false;
}
return failureCount < 3;
},
};
})
);

export const allProposalsAtom = atomWithQuery((get) => {
const api = get(indexerApiAtom);
return {
Expand Down
175 changes: 130 additions & 45 deletions apps/namadillo/src/atoms/proposals/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,17 +211,27 @@ const toProposal = (
votingPower: IndexerVotingPower
): Proposal => {
const ContentSchema = t.record(t.string, t.union([t.string, t.undefined]));
const content = JSON.parse(proposal.content);
const contentDecoded = ContentSchema.decode(content);

if (E.isLeft(contentDecoded)) {
throw new Error("content is not valid");
let content: Record<string, string | undefined> = {};
try {
const parsedContent = JSON.parse(proposal.content);
const contentDecoded = ContentSchema.decode(parsedContent);

if (E.isLeft(contentDecoded)) {
console.warn("Invalid proposal content format, using fallback", contentDecoded.left);
content = { title: "Invalid Content", description: proposal.content };
} else {
content = contentDecoded.right;
}
} catch (error) {
console.warn("Failed to parse proposal content, using fallback", error);
content = { title: "Parse Error", description: proposal.content };
}

return {
id: BigInt(proposal.id),
author: proposal.author,
content: contentDecoded.right,
content,
startEpoch: BigInt(proposal.startEpoch),
endEpoch: BigInt(proposal.endEpoch),
activationEpoch: BigInt(proposal.activationEpoch),
Expand All @@ -233,16 +243,17 @@ const toProposal = (
tallyType: toTally(proposal.tallyType),
status: fromIndexerStatus(proposal.status),
totalVotingPower: BigNumber(votingPower.totalVotingPower),
yay: BigNumber(proposal.yayVotes),
nay: BigNumber(proposal.nayVotes),
abstain: BigNumber(proposal.abstainVotes),
yay: BigNumber(proposal.yayVotes || "0"),
nay: BigNumber(proposal.nayVotes || "0"),
abstain: BigNumber(proposal.abstainVotes || "0"),
};
};

export const fetchProposalById = async (
api: DefaultApi,
id: bigint
): Promise<Proposal> => {
// Updated API endpoint for individual proposal fetching
const proposalPromise = api.apiV1GovProposalIdGet(Number(id));
const totalVotingPowerPromise = api.apiV1PosVotingPowerGet();
const [proposalResponse, votingPowerResponse] = await Promise.all([
Expand All @@ -257,13 +268,14 @@ export const fetchProposalDataById = async (
api: DefaultApi,
id: bigint
): Promise<string> => {
const totalVotingPowerPromise = await api.apiV1GovProposalIdDataGet(
// Updated API endpoint for proposal data fetching
const proposalDataPromise = await api.apiV1GovProposalIdDataGet(
Number(id)
);

// TODO: fix after fixing swagger return type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return totalVotingPowerPromise.data as any as string;
return proposalDataPromise.data as any as string;
};

const fromIndexerStatus = (
Expand Down Expand Up @@ -322,17 +334,28 @@ const toIndexerProposalType = (
export const fetchAllProposals = async (
api: DefaultApi
): Promise<Proposal[]> => {
const proposalsPromise = api.apiV1GovProposalAllGet();
const totalVotingPowerPromise = api.apiV1PosVotingPowerGet();

const [proposalResponse, votingPowerResponse] = await Promise.all([
proposalsPromise,
totalVotingPowerPromise,
]);

return proposalResponse.data.map((proposal) =>
toProposal(proposal, votingPowerResponse.data)
);
try {
// Updated API endpoint for fetching all proposals
const proposalsPromise = api.apiV1GovProposalAllGet();
const totalVotingPowerPromise = api.apiV1PosVotingPowerGet();

const [proposalResponse, votingPowerResponse] = await Promise.all([
proposalsPromise,
totalVotingPowerPromise,
]);

if (!proposalResponse.data || !Array.isArray(proposalResponse.data)) {
console.warn("Invalid proposals response format", proposalResponse);
return [];
}

return proposalResponse.data.map((proposal) =>
toProposal(proposal, votingPowerResponse.data)
);
} catch (error) {
console.error("Failed to fetch all proposals:", error);
throw new Error(`Failed to fetch proposals: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};

export const fetchPaginatedProposals = async (
Expand All @@ -342,40 +365,102 @@ export const fetchPaginatedProposals = async (
proposalType?: ProposalTypeString,
search?: string
): Promise<{ proposals: Proposal[]; pagination: Pagination }> => {
const proposalsPromise = api.apiV1GovProposalGet(
mapUndefined((p) => p + 1, page), // indexer uses 1 as first page, not 0
mapUndefined(toIndexerStatus, status),
mapUndefined(toIndexerProposalType, proposalType),
search,
undefined
);
try {
// Updated API endpoint for paginated proposal fetching
const proposalsPromise = api.apiV1GovProposalGet(
mapUndefined((p) => p + 1, page), // indexer uses 1 as first page, not 0
mapUndefined(toIndexerStatus, status),
mapUndefined(toIndexerProposalType, proposalType),
search,
undefined
);

const totalVotingPowerPromise = api.apiV1PosVotingPowerGet();
const [proposalResponse, votingPowerResponse] = await Promise.all([
proposalsPromise,
totalVotingPowerPromise,
]);
const totalVotingPowerPromise = api.apiV1PosVotingPowerGet();
const [proposalResponse, votingPowerResponse] = await Promise.all([
proposalsPromise,
totalVotingPowerPromise,
]);

const proposals = proposalResponse.data.results.map((proposal) =>
toProposal(proposal, votingPowerResponse.data)
);
if (!proposalResponse.data || !proposalResponse.data.results || !Array.isArray(proposalResponse.data.results)) {
console.warn("Invalid paginated proposals response format", proposalResponse);
return {
proposals: [],
pagination: { page: "1" },
};
}

return {
proposals,
pagination: proposalResponse.data.pagination,
};
const proposals = proposalResponse.data.results.map((proposal) =>
toProposal(proposal, votingPowerResponse.data)
);

return {
proposals,
pagination: proposalResponse.data.pagination || { page: "1" },
};
} catch (error) {
console.error("Failed to fetch paginated proposals:", error);
throw new Error(`Failed to fetch paginated proposals: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};

export const fetchVotedProposalsByAccount = async (
api: DefaultApi,
account: Account
): Promise<{ proposalId: bigint; vote: VoteType | UnknownVoteType }[]> => {
const response = await api.apiV1GovVoterAddressVotesGet(account.address);
try {
// Updated API endpoint for fetching votes by account
const response = await api.apiV1GovVoterAddressVotesGet(account.address);

if (!response.data || !Array.isArray(response.data)) {
console.warn("Invalid voted proposals response format", response);
return [];
}

return response.data.map(({ proposalId, vote }) => ({
proposalId: BigInt(proposalId),
vote,
}));
} catch (error) {
console.error("Failed to fetch voted proposals for account:", account.address, error);
// Return empty array instead of throwing to prevent UI breaks
return [];
}
};

// New function to fetch all votes for a specific proposal
export const fetchProposalVotes = async (
api: DefaultApi,
proposalId: bigint
): Promise<{
proposalId: bigint;
votes: Array<{
address: string;
vote: VoteType | UnknownVoteType;
votingPower?: bigint;
}>;
}> => {
try {
// Try the new endpoint pattern first
const response = await api.apiV1GovProposalIdVotesGet?.(Number(proposalId));
if (response?.data && Array.isArray(response.data)) {
return {
proposalId,
votes: response.data.map((voteData: Record<string, unknown>) => ({
address: (voteData.address as string) || "",
vote: (voteData.vote as VoteType | UnknownVoteType) || "unknown",
votingPower: voteData.votingPower ? BigInt(voteData.votingPower as string | number) : undefined,
})),
};
}
} catch (error) {
console.warn("New proposal votes endpoint not available, falling back to old method:", error);
}

return response.data.map(({ proposalId, vote }) => ({
proposalId: BigInt(proposalId),
vote,
}));
// Fallback to empty votes array if the new endpoint doesn't exist
return {
proposalId,
votes: [],
};
};

export const createVoteProposalTx = async (
Expand Down
Loading