Skip to content

Commit

Permalink
[RetroFunding R5 API] Batch ballot update endpoint (#486)
Browse files Browse the repository at this point in the history
* add a post route to update all projects in ballot

* update swagger docs
  • Loading branch information
stepandel authored Sep 5, 2024
1 parent 67253d6 commit cd920d2
Show file tree
Hide file tree
Showing 3 changed files with 222 additions and 3 deletions.
51 changes: 48 additions & 3 deletions spec/oas_v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2228,6 +2228,51 @@ paths:
description: Unauthorized
"500":
description: Internal Server Error
/retrofunding/rounds/{roundId}/ballots/{addressOrEnsName}/projects:
post:
summary: Updates all projects data for a specific RetroFunding ballot
description: >
Updates all projects data for a specific ballot for a RetroFunding round on Agora.
operationId: updateRetroFundingRoundProjects
tags:
- RetroFunding
- ballots
- Round 5
parameters:
- $ref: "#/components/parameters/roundIdParam"
- $ref: "#/components/parameters/addressOrEnsName"
requestBody:
content:
application/json:
schema:
type: object
properties:
projects:
type: array
items:
type: object
properties:
projectId:
type: string
allocation:
type: string
impact:
type: number
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/Round5Ballot"
"400":
description: Bad Request
"401":
description: Unauthorized
"404":
description: Not Found
"500":
description: Internal Server Error
/retrofunding/rounds/{roundId}/ballots/{addressOrEnsName}/projects/{projectId}/allocation/{allocation}:
post:
summary: Updates allocation for a specific project for a RetroFunding round
Expand All @@ -2237,7 +2282,7 @@ paths:
operationId: updateRetroFundingRoundProjectAllocation
tags:
- RetroFunding
- projects
- ballots
- Round 5
parameters:
- $ref: "#/components/parameters/roundIdParam"
Expand Down Expand Up @@ -2274,7 +2319,7 @@ paths:
operationId: updateRetroFundingRoundProjectImpact
tags:
- RetroFunding
- projects
- ballots
- Round 5
parameters:
- $ref: "#/components/parameters/roundIdParam"
Expand Down Expand Up @@ -2305,7 +2350,7 @@ paths:
operationId: updateRetroFundingRoundProjectPosition
tags:
- RetroFunding
- projects
- ballots
- Round 5
parameters:
- $ref: "#/components/parameters/roundIdParam"
Expand Down
107 changes: 107 additions & 0 deletions src/app/api/common/ballots/updateBallotProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,110 @@ async function updateBallotProjectPositionForAddress({
export const updateBallotProjectPosition = cache(
updateBallotProjectPositionApi
);

const updateAllProjectsInBallotApi = async (
projects: {
projectId: string;
allocation: string;
impact: number;
}[],
category: string,
roundId: number,
ballotCasterAddressOrEns: string
) =>
addressOrEnsNameWrap(
updateAllProjectsInBallotForAddress,
ballotCasterAddressOrEns,
{
projects,
category,
roundId,
}
);

async function updateAllProjectsInBallotForAddress({
projects,
category,
roundId,
address,
}: {
projects: {
projectId: string;
allocation: string;
impact: number;
}[];
category: string;
roundId: number;
address: string;
}) {
const categoryProjects = await prisma.mockProjects.findMany({
where: {
category_slug: category,
},
});

// check if all projects are valid
const isValid =
projects.every((project) =>
categoryProjects.some(
(categoryProject) => categoryProject.id === project.projectId
)
) && projects.length === categoryProjects.length;

if (!isValid) {
throw new Error("Invalid projects for badgeholder category");
}

// Sort projects by impact and allocation lowest to highest
projects.sort(
(a, b) => a.impact - b.impact || Number(a.allocation) - Number(b.allocation)
);

// Create ballot if it doesn't exist
await prisma.ballots.upsert({
where: {
address_round: {
address,
round: roundId,
},
},
update: {
updated_at: new Date(),
},
create: {
round: roundId,
address,
},
});

await Promise.all(
projects.map((project, i) =>
prisma.projectAllocations.upsert({
where: {
address_round_project_id: {
project_id: project.projectId,
round: roundId,
address,
},
},
update: {
allocation: project.allocation,
updated_at: new Date(),
},
create: {
project_id: project.projectId,
round: roundId,
address,
allocation: project.allocation,
impact: project.impact,
rank: (500_000 / projects.length) * (i + 1),
},
})
)
);

// Return full ballot
return fetchBallot(roundId, address, category);
}

export const updateAllProjectsInBallot = cache(updateAllProjectsInBallotApi);
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { updateAllProjectsInBallot } from "@/app/api/common/ballots/updateBallotProject";
import { traceWithUserId } from "@/app/api/v1/apiUtils";
import {
authenticateApiUser,
getCategoryScope,
validateAddressScope,
} from "@/app/lib/auth/serverAuth";
import { NextResponse, type NextRequest } from "next/server";
import { z } from "zod";

const ballotPayloadSchema = z.object({
projects: z.array(
z.object({
projectId: z.string(),
allocation: z.string(z.number().min(0).max(100)),
impact: z.number(),
})
),
});

export async function POST(
request: NextRequest,
route: { params: { roundId: string; ballotCasterAddressOrEns: string } }
) {
const authResponse = await authenticateApiUser(request);

if (!authResponse.authenticated) {
return new Response(authResponse.failReason, { status: 401 });
}

const { roundId, ballotCasterAddressOrEns } = route.params;
const scopeError = await validateAddressScope(
ballotCasterAddressOrEns,
authResponse
);
if (scopeError) return scopeError;

return await traceWithUserId(authResponse.userId as string, async () => {
try {
const categoryScope = getCategoryScope(authResponse);

if (!categoryScope) {
return new Response(
"This user does not have a category scope. Regenerate the JWT token",
{
status: 401,
}
);
}

const payload = await request.json();
const parsedPayload = ballotPayloadSchema.parse(payload);

const ballot = await updateAllProjectsInBallot(
parsedPayload.projects,
categoryScope,
Number(roundId),
ballotCasterAddressOrEns
);
return NextResponse.json(ballot);
} catch (e: any) {
return new Response("Internal server error: " + e.toString(), {
status: 500,
});
}
});
}

0 comments on commit cd920d2

Please sign in to comment.