Skip to content

Feat/scaffold coordinator service api calls #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

JohnGuilding
Copy link
Collaborator

@JohnGuilding JohnGuilding commented May 14, 2025

What does this PR do:

  • Create functions that call all of the coordinator service endpoints required to finalise a poll
    • merge
    • generateProofs
    • submit
  • Add required helper functions from coordinator service to get things building, such as getSigner, getAuthorizationHeader
  • Add zod for api request return types & runtime validation
  • Make a note of where files/logic is copied
    • Also note where changes were made

Open questions

  • I'm not 100% familiar with the configuration required, I'm pretty sure some of what I've written will be wrong, so let me know if you can see any mistakes that should be fixed in this PR
  • Also not 100% how authentication should be handled, my thinking was to solve that in a follow up PR
  • Added minimum required utilities to get everything to build with the 3 finalise functions, but let me know if I missed any

Other notes

I wanted to keep this PR as small as possible, so didn't make it an e2e PR that could be tested. My thinking was that I could at least solve authentication and add any remaining utilities in a follow up PR. That said, please feel free to suggest any improvements here or other suggestions for next steps.

There is a fair amount of copied code in this PR, the new logic is mainly in coordinator.ts. Spoke with Nico and we discussed that we would look to potentially export a lot of the logic that the coordinator uses into an sdk to that devs can just import those instead of copying them over which is what I'm doing now. We agreed that seemed out of scope for now, but let me know if you wanted me to do that first - only issue is that is would be time consuming given we want to get the demo out soon.

@JohnGuilding JohnGuilding marked this pull request as ready for review May 14, 2025 21:31
.eslintrc.cjs Outdated
@@ -18,6 +18,7 @@ module.exports = {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": ["warn", { ignoreRestSiblings: true }],
"@typescript-eslint/prefer-nullish-coalescing": "warn",
"@typescript-eslint/no-duplicate-enum-values": "off",
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I appeared to be getting a false positive here where this warning would throw during pre-commit hooks for the new enums, even though they don't seem to have duplicate values. Adding this to each file also did not work so I was a bit confused why it was getting thrown

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So disabling the rule here worked whereas disabling it in the file did not

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd not disable this eslint rule. Better to fix the issues related to it. It can't be false positive, something can be related to mixed default and defined values.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Didn't give this too much thought as wasn't sure about a lot of the values. Please let me know if you see anything that is definitely wrong

@NicoSerranoP
Copy link
Member

Thank you John! It is looking good, I will add some comments 😊

Copy link
Member

@NicoSerranoP NicoSerranoP left a comment

Choose a reason for hiding this comment

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

Thank you @JohnGuilding I left some comments, try reading them from the "Conversation" tab so they are in order (my idea was to leave the main file /coordinator/coordinator.ts to the end :)

Copy link
Collaborator

@0xmad 0xmad left a comment

Choose a reason for hiding this comment

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

@JohnGuilding thanks, left some comments

.eslintrc.cjs Outdated
@@ -18,6 +18,7 @@ module.exports = {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": ["warn", { ignoreRestSiblings: true }],
"@typescript-eslint/prefer-nullish-coalescing": "warn",
"@typescript-eslint/no-duplicate-enum-values": "off",
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd not disable this eslint rule. Better to fix the issues related to it. It can't be false positive, something can be related to mixed default and defined values.


type GenerateResponse = z.infer<typeof GenerateResponseSchema>;
type SubmitResponse = z.infer<typeof SubmitResponseSchema>;
type CoordinatorServiceResult<T, E = Error> = { success: true; data: T } | { success: false; error: E };
Copy link
Collaborator

Choose a reason for hiding this comment

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

It should be an interface, not type

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

z.infer returns type not an interface. Might remove zod anyway as you can argue it is over-engineering

Do you guys have a style guide or anything that you refer to? Just curious to understand the patterns/preferences you follow so I know for the future

Copy link
Member

Choose a reason for hiding this comment

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

I don't think we have a specific guideline but we should build one 😊 . Most of it is guided by:

  1. Clean code
  2. Contributing to MACI

Copy link
Collaborator

Choose a reason for hiding this comment

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

Instead of types we prefer using interfaces, it's just an agreement (not written somewhere yet).
So you can use it like this:

interface ICoordinatorServiceResult<T, E = Error> {
  success: boolean;
  data?: T;
  error?: E;
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sure, switched to using an interface.

Curious to get your thoughts on the following:

export type CoordinatorServiceResult<T, E = Error> = { success: true; data: T } | { success: false; error: E };
const usingDiscriminatedUnion: CoordinatorServiceResult<boolean> = {
  success: true,
  error: new Error("I'm an error"), // This does not compile
};

export interface ICoordinatorServiceResult<T, E = Error> {
  success: boolean;
  data?: T;
  error?: E;
}
const usingInterface: ICoordinatorServiceResult<boolean> = {
  success: true,
  error: new Error("I'm an error"), // This compiles
};

The discriminated union type here provides a bit more flexibility and type safety in this example. For return types, I think this could be useful. Do you have a preference for this or would you rather stick with interfaces?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Actually, for this case you can keep the type but move inline types to interfaces:

interface ISuccessResult<T> {
  success: true;
  data: T;
}

interface IErrorResult<E = Error> {
  success: false;
  error: E;
}

Or you can get rid of success property and check if there is data or error

Comment on lines 27 to 29
let response: Response;
try {
response = await fetch(`${BASE_URL}/proof/merge`, {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do not use try/catch like this. You can use it like fetch(...).catch((error) => ...)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sure, done. Out of interest, why is there a preference for this way of doing things over try catch?

Copy link
Collaborator

Choose a reason for hiding this comment

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

It's just a more functional approach instead of procedural. With catch method you don't need to have a mutation for response and keep in const, and it makes code cleaner because you can't return the value from catch method so you need to explicitly handle it outside which makes it more obvious when you read the code (not a good practice to have some logic handling in catch).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

you don't need to have a mutation for response and keep in const

I like this benefit

makes code cleaner because you can't return the value from catch method so you need to explicitly handle it outside which makes it more obvious when you read the code (not a good practice to have some logic handling in catch).

Ok yeah I see how this can be cleaner

Thanks for the explanation

Comment on lines 21 to 26
export const merge = async (
maciContractAddress: Address,
pollId: number,
approval: string,
sessionKeyAddress: Address
): Promise<CoordinatorServiceResult<boolean>> => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Use one object instead of four separate params

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

These args have changed now but have done this where multiple args still exist

Copy link
Collaborator

Choose a reason for hiding this comment

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

So this is the idea that if I want to add new argument, I just add a new property to the object, plus I don't need to worry about the order of the arguments. For instance first option looks better than second one:

merge({ maciContractAddress: "0x...", pollId: 0n, approval: "321...", sessionKeyAddress: "123..." });
merge("0x...", 0n,  "321...",  "123...");

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Got it. Yeah I agree named arguments are cleaner - thanks for pointing this out!

Comment on lines 83 to 84
startBlock: Number(blockNumber) - 100,
endBlock: Number(blockNumber) + 100,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why are these values hardcoded?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I copied and pasted code from the coordinator service. Assume these are wrong, I guess I should consume them as arguments into the function?

Copy link
Member

Choose a reason for hiding this comment

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

These values are used to generate a MACI state that would later be used to generate a proof. In our context startBlock and endBlock would be variables. startBlock would be the proposal snapshot block and the end block will be the getFutureBlockNumberAtTimestamp(pollEndDate)

I suggest we send these values as arguments into the function generateProofs 😊

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added as arguments!

Copy link
Collaborator

Choose a reason for hiding this comment

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

But why is 100 hardcoded?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@0xmad here is where I originally copied them from. I'm not sure why 100 was chosen but I assume they were deemed as acceptable e2e test values

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it should be at least moved as a const or to config (.env) and I don't know why this +/- 100 blocks are on tests.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Would something like this work? Or would you want to move the 100 values themselves to config? Something else?

   test("should generate proofs correctly", async () => {
    const blockNumber = await publicClient.getBlockNumber();
    const startBlock = Number(blockNumber) - 100;
    const endBlock = Number(blockNumber) + 100;
    const encryptedCoordinatorPrivateKey = await encryptWithCoordinatorRSAPublicKey(
      coordinatorMACIKeypair.privateKey.serialize(),
    );

    const response = await fetch(`${TEST_URL}/proof/generate`, {
      method: "POST",
      headers: {
        Authorization: encryptedHeader,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        poll: Number(pollId),
        maciContractAddress: maciAddress,
        mode: testPollDeploymentConfig.mode,
        encryptedCoordinatorPrivateKey,
        startBlock,
        endBlock,
        blocksPerBatch: 20,
        chain: CHAIN,
      } as IGenerateArgs),
    });

Comment on lines 91 to 93
let response: Response;
try {
response = await fetch(`${BASE_URL}/proof/generate`, {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here for try/catch

Comment on lines 127 to 132
export const submit = async (
pollId: number,
maciContractAddress: Address,
approval: string,
sessionKeyAddress: Address
): Promise<CoordinatorServiceResult<SubmitResponse>> => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here for one object param

approval: string,
sessionKeyAddress: Address
): Promise<CoordinatorServiceResult<SubmitResponse>> => {
const args: ISubmitProofsArgs = {
Copy link
Collaborator

Choose a reason for hiding this comment

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

No need to specify type here

Comment on lines 141 to 143
let response: Response;
try {
response = await fetch(`${BASE_URL}/proof/submit`, {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here for try/catch

@JohnGuilding
Copy link
Collaborator Author

I'd not disable this eslint rule. Better to fix the issues related to it. It can't be false positive, something can be related to mixed default and defined values.

yeah good point, since I deleted the code based on Nico's comments I could remove this easily

@JohnGuilding
Copy link
Collaborator Author

@NicoSerranoP @0xmad addressed your comments or left a follow up question. let me know if I missed anything!


// TODO: move to env
const BASE_URL = "http://localhost:3000/v1";
const MACI_CONTRACT_ADDRESS = "0x0000000000000000000000000000000000000000";
Copy link
Member

Choose a reason for hiding this comment

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

Instead of MACI_CONTRACT_ADDRESS lets use 🚀

import { PUB_MACI_ADDRESS } from "@/constants";

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Cheers done

Copy link
Collaborator

Choose a reason for hiding this comment

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

Please, use PUBLIC_MACI_ADDRESS name instead

@JohnGuilding JohnGuilding force-pushed the feat/scaffold-coordinator-service-api-calls branch from ac3194c to 0d438d7 Compare May 16, 2025 14:53
@@ -17,6 +17,9 @@ export const PUB_MACI_ADDRESS = (process.env.NEXT_PUBLIC_MACI_ADDRESS ?? "") as
export const PUB_MACI_DEPLOYMENT_BLOCK = Number(process.env.NEXT_PUBLIC_MACI_DEPLOYMENT_BLOCK ?? 0);
export const NEXT_PUBLIC_SECONDS_PER_BLOCK = Number(process.env.NEXT_PUBLIC_SECONDS_PER_BLOCK ?? 1); // ETH Mainnet block takes ~12s

// MACI Coordinator service
export const PUB_COORDINATOR_SERVICE_URL = process.env.NEXT_PUBLIC_COORDINATOR_SERVICE_URL ?? "";
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
export const PUB_COORDINATOR_SERVICE_URL = process.env.NEXT_PUBLIC_COORDINATOR_SERVICE_URL ?? "";
export const PUBLIC_COORDINATOR_SERVICE_URL = process.env.NEXT_PUBLIC_COORDINATOR_SERVICE_URL ?? "";

Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's do it for all the constants here

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

sure, done

Comment on lines +179 to +181
}).catch((error) => {
throw error instanceof Error ? error : new Error(String(error));
});
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there a case where you can have string error instead of Error?
If no, you can just remove this catch (and other catches too).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm not entirely sure but I agree that this could be redundant, removed catch blocks

const publicClient = usePublicClient();
const { signMessageAsync } = useSignMessage();

const getPublicKey = async (): Promise<KeyLike> => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's use useCallback here and add it to the deps for other useCallbacks

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

Comment on lines +99 to +104
}: {
pollId: number;
encryptedCoordinatorPrivateKey: string;
startBlock: number;
endBlock: number;
}): Promise<CoordinatorServiceResult<GenerateResponse>> => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's move it to the interface

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

Comment on lines +22 to +27
}: {
pollId: number;
encryptedCoordinatorPrivateKey: string;
startBlock: number;
endBlock: number;
}) => Promise<CoordinatorServiceResult<GenerateResponse>>;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's move it to the interface and reuse

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

export type SubmitResponse = z.infer<typeof SubmitResponseSchema>;
export type CoordinatorServiceResult<T, E = Error> = { success: true; data: T } | { success: false; error: E };

export interface CoordinatorContextType {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
export interface CoordinatorContextType {
export interface ICoordinatorContextType {

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done


export interface IVoteArgs {
voteOptionIndex: bigint;
newVoteWeight: bigint;
}

export type GenerateResponse = z.infer<typeof GenerateResponseSchema>;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
export type GenerateResponse = z.infer<typeof GenerateResponseSchema>;
export type TGenerateResponse = z.infer<typeof GenerateResponseSchema>;


export interface IVoteArgs {
voteOptionIndex: bigint;
newVoteWeight: bigint;
}

export type GenerateResponse = z.infer<typeof GenerateResponseSchema>;
export type SubmitResponse = z.infer<typeof SubmitResponseSchema>;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
export type SubmitResponse = z.infer<typeof SubmitResponseSchema>;
export type TSubmitResponse = z.infer<typeof SubmitResponseSchema>;

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

@JohnGuilding JohnGuilding changed the base branch from feat/maci-plugin to main May 19, 2025 17:17
@JohnGuilding JohnGuilding changed the base branch from main to feat/maci-plugin May 19, 2025 18:21
@JohnGuilding JohnGuilding changed the base branch from feat/maci-plugin to main May 19, 2025 18:30
@JohnGuilding JohnGuilding changed the base branch from main to feat/maci-plugin May 19, 2025 18:31
@JohnGuilding
Copy link
Collaborator Author

JohnGuilding commented May 19, 2025

@0xmad addressed your newer comments, but in this new PR

Nico and myself thought raising a new PR would be simpler than trying to update the existing one with the latest changes from main

@JohnGuilding
Copy link
Collaborator Author

Replaced with #19

@JohnGuilding JohnGuilding deleted the feat/scaffold-coordinator-service-api-calls branch May 19, 2025 21:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants