Skip to content

Commit dc6b6c4

Browse files
committed
fix: update governance API paths and enhance proposal details handling
- Modified API path checks in governanceActiveProposals to remove leading slashes for consistency. - Enhanced error handling for metadata retrieval, returning structured error responses. - Improved proposal status resolution logic to handle additional details from the provider. - Refactored proposal details fetching to ensure accurate data retrieval and status determination.
1 parent 109bade commit dc6b6c4

4 files changed

Lines changed: 166 additions & 48 deletions

File tree

src/__tests__/governance.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { describe, expect, it } from "@jest/globals";
2+
import { getProposalStatus } from "../lib/governance";
3+
import type { ProposalDetails } from "../types/governance";
4+
5+
describe("getProposalStatus", () => {
6+
const baseProposal: ProposalDetails = {
7+
id: "proposal",
8+
tx_hash: "tx-hash",
9+
cert_index: 0,
10+
governance_type: "info_action",
11+
deposit: "0",
12+
return_address: "addr_test1...",
13+
expiration: null,
14+
governance_description: { tag: "off_chain" },
15+
ratified_epoch: null,
16+
enacted_epoch: null,
17+
dropped_epoch: null,
18+
expired_epoch: null,
19+
};
20+
21+
it("returns active when all terminal epochs are null", () => {
22+
expect(getProposalStatus({ ...baseProposal, id: "proposal-1" })).toBe("active");
23+
});
24+
25+
it("returns active when terminal epoch fields are undefined", () => {
26+
expect(
27+
getProposalStatus({
28+
...baseProposal,
29+
id: "proposal-2",
30+
ratified_epoch: undefined as unknown as number | null,
31+
enacted_epoch: undefined as unknown as number | null,
32+
dropped_epoch: undefined as unknown as number | null,
33+
expired_epoch: undefined as unknown as number | null,
34+
} as unknown as ProposalDetails),
35+
).toBe("active");
36+
});
37+
});

src/__tests__/governanceActiveProposals.test.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ describe("governanceActiveProposals API", () => {
147147

148148
it("returns only active proposals and tolerates metadata 404", async () => {
149149
providerGetMock.mockImplementation(async (path: string) => {
150-
if (path.startsWith("/governance/proposals?")) {
150+
if (path.startsWith("governance/proposals?")) {
151151
return [
152152
{
153153
tx_hash: "tx-active",
@@ -169,10 +169,37 @@ describe("governanceActiveProposals API", () => {
169169
},
170170
];
171171
}
172-
if (path === "/governance/proposals/tx-active/0/metadata") {
173-
const error = new Error("404") as Error & { status?: number };
174-
error.status = 404;
175-
throw error;
172+
if (path === "governance/proposals/tx-active/0") {
173+
return {
174+
ratified_epoch: null,
175+
enacted_epoch: null,
176+
dropped_epoch: null,
177+
expired_epoch: null,
178+
expiration: 999,
179+
deposit: "1000000",
180+
return_address: "addr_test1...",
181+
};
182+
}
183+
if (path === "governance/proposals/tx-ratified/1") {
184+
return {
185+
ratified_epoch: 530,
186+
enacted_epoch: null,
187+
dropped_epoch: null,
188+
expired_epoch: null,
189+
expiration: 999,
190+
deposit: "1000000",
191+
return_address: "addr_test1...",
192+
};
193+
}
194+
if (path === "governance/proposals/tx-active/0/metadata") {
195+
throw JSON.stringify({
196+
data: {
197+
error: "Not Found",
198+
message: "The requested component has not been found.",
199+
status_code: 404,
200+
},
201+
status: 404,
202+
});
176203
}
177204
return null;
178205
});

src/lib/governance.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ export type BallotChoice = "Yes" | "No" | "Abstain";
55

66
export function getProposalStatus(details?: ProposalDetails | null): ProposalStatus | null {
77
if (!details) return null;
8-
if (details.enacted_epoch !== null) return "enacted";
9-
if (details.dropped_epoch !== null) return "dropped";
10-
if (details.expired_epoch !== null) return "expired";
11-
if (details.ratified_epoch !== null) return "ratified";
8+
if (details.enacted_epoch != null) return "enacted";
9+
if (details.dropped_epoch != null) return "dropped";
10+
if (details.expired_epoch != null) return "expired";
11+
if (details.ratified_epoch != null) return "ratified";
1212
return "active";
1313
}
1414

src/pages/api/v1/governanceActiveProposals.ts

Lines changed: 93 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,28 @@ type BlockfrostProposalListItem = {
1919
ratified_epoch: number | null;
2020
};
2121

22+
type BlockfrostProposalDetailsItem = {
23+
proposed_epoch?: number | null;
24+
activation_epoch?: number | null;
25+
expiration?: number | null;
26+
deposit?: string | null;
27+
return_address?: string | null;
28+
parameters?: unknown;
29+
ratified_epoch?: number | null;
30+
enacted_epoch?: number | null;
31+
dropped_epoch?: number | null;
32+
expired_epoch?: number | null;
33+
};
34+
2235
const getErrorStatus = (error: unknown): number | undefined => {
36+
if (typeof error === "string") {
37+
try {
38+
const parsed = JSON.parse(error) as unknown;
39+
return getErrorStatus(parsed);
40+
} catch {
41+
return undefined;
42+
}
43+
}
2344
if (
2445
error &&
2546
typeof error === "object" &&
@@ -120,52 +141,71 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
120141
try {
121142
const provider = getProvider(Number(network));
122143
const list = (await provider.get(
123-
`/governance/proposals?count=${count}&page=${page}&order=${order}`,
144+
`governance/proposals?count=${count}&page=${page}&order=${order}`,
124145
)) as BlockfrostProposalListItem[];
125146

126-
const active = Array.isArray(list)
127-
? list.filter((item) => {
128-
const status = getProposalStatus({
129-
id: "",
130-
tx_hash: item.tx_hash,
131-
cert_index: Number(item.cert_index),
132-
governance_type: item.governance_type,
133-
deposit: "",
134-
return_address: "",
135-
governance_description: { tag: "" },
136-
ratified_epoch: item.ratified_epoch,
137-
enacted_epoch: item.enacted_epoch,
138-
dropped_epoch: item.dropped_epoch,
139-
expired_epoch: item.expired_epoch,
140-
expiration: null,
141-
});
142-
return status === "active";
143-
})
144-
: [];
147+
const statusResolved = await Promise.all(
148+
(Array.isArray(list) ? list : []).map(async (item) => {
149+
const txHash = item.tx_hash;
150+
const certIndex = Number(item.cert_index);
151+
let detailsForStatus: BlockfrostProposalDetailsItem | null = null;
152+
153+
try {
154+
detailsForStatus = (await provider.get(
155+
`governance/proposals/${txHash}/${certIndex}`,
156+
)) as BlockfrostProposalDetailsItem;
157+
} catch (error) {
158+
const status = getErrorStatus(error);
159+
if (status && status !== 404) throw error;
160+
}
161+
162+
const status = getProposalStatus({
163+
id: "",
164+
tx_hash: txHash,
165+
cert_index: certIndex,
166+
governance_type: item.governance_type,
167+
deposit:
168+
typeof detailsForStatus?.deposit === "string"
169+
? detailsForStatus.deposit
170+
: "",
171+
return_address:
172+
typeof detailsForStatus?.return_address === "string"
173+
? detailsForStatus.return_address
174+
: "",
175+
governance_description: { tag: "" },
176+
ratified_epoch:
177+
detailsForStatus?.ratified_epoch ?? item.ratified_epoch ?? null,
178+
enacted_epoch:
179+
detailsForStatus?.enacted_epoch ?? item.enacted_epoch ?? null,
180+
dropped_epoch:
181+
detailsForStatus?.dropped_epoch ?? item.dropped_epoch ?? null,
182+
expired_epoch:
183+
detailsForStatus?.expired_epoch ?? item.expired_epoch ?? null,
184+
expiration:
185+
typeof detailsForStatus?.expiration === "number"
186+
? detailsForStatus.expiration
187+
: null,
188+
});
189+
190+
return { item, detailsForStatus, status };
191+
}),
192+
);
193+
194+
const active = statusResolved.filter((entry) => entry.status === "active");
145195

146196
const proposals = await Promise.all(
147-
active.map(async (item) => {
197+
active.map(async ({ item, detailsForStatus }) => {
148198
const txHash = item.tx_hash;
149199
const certIndex = Number(item.cert_index);
150200
let metadata: any = null;
151-
let details: any = null;
152201

153202
try {
154-
metadata = await provider.get(`/governance/proposals/${txHash}/${certIndex}/metadata`);
203+
metadata = await provider.get(`governance/proposals/${txHash}/${certIndex}/metadata`);
155204
} catch (error) {
156205
const status = getErrorStatus(error);
157206
if (status !== 404) throw error;
158207
}
159208

160-
if (includeDetails) {
161-
try {
162-
details = await provider.get(`/governance/proposals/${txHash}/${certIndex}`);
163-
} catch (error) {
164-
const status = getErrorStatus(error);
165-
if (status && status !== 404) throw error;
166-
}
167-
}
168-
169209
const body = metadata?.json_metadata?.body ?? {};
170210
const authors = Array.isArray(metadata?.json_metadata?.authors)
171211
? metadata.json_metadata.authors
@@ -187,16 +227,30 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
187227
details: includeDetails
188228
? {
189229
proposedEpoch:
190-
typeof details?.proposed_epoch === "number" ? details.proposed_epoch : null,
230+
typeof detailsForStatus?.proposed_epoch === "number"
231+
? detailsForStatus.proposed_epoch
232+
: null,
191233
activationEpoch:
192-
typeof details?.activation_epoch === "number" ? details.activation_epoch : null,
193-
expiration: typeof details?.expiration === "number" ? details.expiration : null,
194-
deposit: typeof details?.deposit === "string" ? details.deposit : null,
234+
typeof detailsForStatus?.activation_epoch === "number"
235+
? detailsForStatus.activation_epoch
236+
: null,
237+
expiration:
238+
typeof detailsForStatus?.expiration === "number"
239+
? detailsForStatus.expiration
240+
: null,
241+
deposit:
242+
typeof detailsForStatus?.deposit === "string"
243+
? detailsForStatus.deposit
244+
: null,
195245
returnAddress:
196-
typeof details?.return_address === "string" ? details.return_address : null,
246+
typeof detailsForStatus?.return_address === "string"
247+
? detailsForStatus.return_address
248+
: null,
197249
parameters:
198-
details && typeof details === "object" && "parameters" in details
199-
? details.parameters ?? null
250+
detailsForStatus &&
251+
typeof detailsForStatus === "object" &&
252+
"parameters" in detailsForStatus
253+
? detailsForStatus.parameters ?? null
200254
: null,
201255
}
202256
: undefined,

0 commit comments

Comments
 (0)