Skip to content

Commit

Permalink
feat: add deployer service
Browse files Browse the repository at this point in the history
  • Loading branch information
ctrlc03 committed Aug 20, 2024
1 parent f9ced97 commit 7fca3ee
Show file tree
Hide file tree
Showing 6 changed files with 603 additions and 55 deletions.
10 changes: 5 additions & 5 deletions packages/coordinator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@
"hardhat": "^2.22.6",
"helmet": "^7.1.0",
"lowdb": "^1.0.0",
"maci-circuits": "^2.1.0",
"maci-cli": "^2.1.0",
"maci-contracts": "^2.1.0",
"maci-domainobjs": "^2.0.0",
"maci-subgraph": "^2.1.0",
"maci-circuits": "^2.2.0",
"maci-cli": "^2.2.0",
"maci-contracts": "^2.2.1",
"maci-domainobjs": "^2.2.0",
"maci-subgraph": "^2.2.0",
"mustache": "^4.2.0",
"permissionless": "^0.1.44",
"reflect-metadata": "^0.2.0",
Expand Down
82 changes: 82 additions & 0 deletions packages/coordinator/ts/common/accountAbstraction.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
import { createPublicClient, http, HttpTransport, TransactionReceipt, Transport, type PublicClient } from "viem";
import { toECDSASigner } from "@zerodev/permissions/signers";
import { privateKeyToAccount } from "viem/accounts";
import { deserializePermissionAccount } from "@zerodev/permissions";
import { ENTRYPOINT_ADDRESS_V07 } from "permissionless";
import { KERNEL_V3_1 } from "@zerodev/sdk/constants";
import { createKernelAccountClient, KernelAccountClient, KernelSmartAccount } from "@zerodev/sdk";
import { ENTRYPOINT_ADDRESS_V07_TYPE } from "permissionless/types";

/**
* Generate the RPCUrl for Pimlico based on the chain we need to interact with
* @param network - the network we want to interact with
Expand All @@ -12,3 +21,76 @@ export const genPimlicoRPCUrl = (network: string): string => {

return `https://api.pimlico.io/v2/${network}/rpc?apikey=${pimlicoAPIKey}`;
};

/**
* Get a public client
* @param rpcUrl - the RPC URL
* @returns the public client
*/
export const getPublicClient = (rpcUrl: string): PublicClient<HttpTransport> => {
return createPublicClient({
transport: http(rpcUrl),
});
}

/**
* Get a Kernel account handle given a session key
* @param sessionKey
* @param chainId
*/
export const getKernelClient = async (
sessionKey: `0x${string}`,
approval: string,
chain: string,
): Promise<
KernelAccountClient<
ENTRYPOINT_ADDRESS_V07_TYPE,
Transport,
undefined,
KernelSmartAccount<ENTRYPOINT_ADDRESS_V07_TYPE, HttpTransport, undefined>
>
> => {
const bundlerUrl = genPimlicoRPCUrl(chain);
const publicClient = createPublicClient({
transport: http(bundlerUrl),
});

// Using a stored private key
const sessionKeySigner = toECDSASigner({
signer: privateKeyToAccount(sessionKey),
});

const sessionKeyAccount = await deserializePermissionAccount(
publicClient,
ENTRYPOINT_ADDRESS_V07,
KERNEL_V3_1,
approval,
sessionKeySigner,
);

const kernelClient = createKernelAccountClient({
bundlerTransport: http(bundlerUrl),
entryPoint: ENTRYPOINT_ADDRESS_V07,
account: sessionKeyAccount,
});

return kernelClient;
};

/**
* The topic for the contract creation event
*/
export const contractCreationEventTopic = "0x4db17dd5e4732fb6da34a148104a592783ca119a1e7bb8829eba6cbadef0b511";

/**
* Get the address of the newly deployed contract from a transaction receipt
* @param receipt - The transaction receipt
* @returns The address of the newly deployed contract
*/
export const getDeployedContractAddress = (receipt: TransactionReceipt): string | undefined => {
const addr = receipt.logs.find((log) => log.topics[0] === contractCreationEventTopic);

const deployedAddress = addr ? "0x" + addr.topics[1]?.slice(26) : undefined;

return deployedAddress;
};
4 changes: 4 additions & 0 deletions packages/coordinator/ts/common/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@ export enum ErrorCodes {
FILE_NOT_FOUND = "6",
SUBGRAPH_DEPLOY = "7",
SESSION_KEY_NOT_FOUND = "8",
UNSUPPORTED_VOICE_CREDIT_PROXY = "9",
UNSUPPORTED_GATEKEEPER = "10",
FAILED_TO_DEPLOY_GATEKEEPER = "11",
FAILED_TO_DEPLOY_VOICE_CREDIT_PROXY = "12",
}
234 changes: 234 additions & 0 deletions packages/coordinator/ts/deployer/deployer.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import { Injectable, Logger } from "@nestjs/common";
import { toECDSASigner } from "@zerodev/permissions/signers";
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
import {
ConstantInitialVoiceCreditProxy__factory as ConstantInitialVoiceCreditProxyFactory,
ContractStorage,
EGatekeepers,
FreeForAllGatekeeper__factory as FreeForAllGatekeeperFactory,
EASGatekeeper__factory as EASGatekeeperFactory,
ZupassGatekeeper__factory as ZupassGatekeeperFactory,
HatsGatekeeperSingle__factory as HatsGatekeeperSingleFactory,
SemaphoreGatekeeper__factory as SemaphoreGatekeeperFactory,
GitcoinPassportGatekeeper__factory as GitcoinPassportGatekeeperFactory,
EContracts,
EInitialVoiceCreditProxies,
} from "maci-contracts";

import path from "path";

import { FileService } from "../file/file.service";

import { IDeployMaciArgs, IDeployPollArgs, IGatekeeperArgs } from "./types";
import { getDeployedContractAddress, getKernelClient, getPublicClient } from "../common/accountAbstraction";
import { ErrorCodes } from "../common";
import { Abi } from "viem";
import { BaseContract, InterfaceAbi } from "ethers";
/**
* DeployerService is responsible for deploying contracts.
*/
@Injectable()
export class DeployerService {
/**
* Logger
*/
private readonly logger = new Logger(DeployerService.name);

/**
* Contract storage instance
*/
private readonly storage: ContractStorage;

/**
* Create a new instance of DeployerService
* @param fileService
*/
constructor(
private readonly fileService: FileService,
) {
this.fileService = fileService;
this.logger = new Logger(DeployerService.name);
this.storage = ContractStorage.getInstance(path.join(__dirname, "..", "..", "deployed-contracts.json"));
}

/**
* Get the gatekeeper abi and bytecode based on the gatekeeper type
* @param gatekeeperType - the gatekeeper type
* @returns - the gatekeeper abi and bytecode
*/
private getGatekeeperAbiAndBytecode(gatekeeperType: EGatekeepers): { abi: Abi; bytecode: `0x${string}` } {
// based on the gatekeeper type, we need to deploy the correct gatekeeper
switch (gatekeeperType) {
case EGatekeepers.FreeForAll:
return {
abi: FreeForAllGatekeeperFactory.abi,
bytecode: FreeForAllGatekeeperFactory.bytecode,
};
case EGatekeepers.EAS:
return {
abi: EASGatekeeperFactory.abi,
bytecode: EASGatekeeperFactory.bytecode,
};
case EGatekeepers.Zupass:
return {
abi: ZupassGatekeeperFactory.abi,
bytecode: ZupassGatekeeperFactory.bytecode,
};
case EGatekeepers.HatsSingle:
return {
abi: HatsGatekeeperSingleFactory.abi,
bytecode: HatsGatekeeperSingleFactory.bytecode,
};
case EGatekeepers.Semaphore:
return {
abi: SemaphoreGatekeeperFactory.abi,
bytecode: SemaphoreGatekeeperFactory.bytecode,
};
case EGatekeepers.GitcoinPassport:
return {
abi: GitcoinPassportGatekeeperFactory.abi,
bytecode: GitcoinPassportGatekeeperFactory.bytecode,
};
default:
throw new Error(ErrorCodes.UNSUPPORTED_GATEKEEPER);
}
}

/**
* Get the voice credit proxy abi and bytecode based on the voice credit proxy type
* @param voiceCreditProxyType - the voice credit proxy type
* @returns - the voice credit proxy abi and bytecode
*/
private getVoiceCreditProxyAbiAndBytecode(voiceCreditProxyType: EInitialVoiceCreditProxies): { abi: Abi; bytecode: `0x${string}` } {
switch (voiceCreditProxyType) {
case EInitialVoiceCreditProxies.Constant:
return {
abi: ConstantInitialVoiceCreditProxyFactory.abi,
bytecode: ConstantInitialVoiceCreditProxyFactory.bytecode,
};
default:
throw new Error(ErrorCodes.UNSUPPORTED_VOICE_CREDIT_PROXY);
}
}

/**
* Flatten an object into an array of strings
* @param obj - the object to flatten
* @returns - the flattened array
*/
private flattenObject(obj: Record<string, any>): string[] {
return Object.values(obj).flatMap((value) =>
typeof value === "object" && value !== null ? this.flattenObject(value) : String(value),
);
}

/**
* Check if the gatekeeper is already deployed based on the name and the args
* @param gatekeeperType - the gatekeeper type
* @param args - the gatekeeper args
* @returns - true if the gatekeeper is already deployed, false otherwise
*/
private checkIfAlreadyDeployed(gatekeeperType: EContracts, args: IGatekeeperArgs, network: string): boolean {
const storedArgs = this.storage.getContractArgs(gatekeeperType, network);
if (storedArgs) {
const flattenedArgs = this.flattenObject(args);
return (
storedArgs.length === flattenedArgs.length &&
storedArgs.every((storedArg, index) => storedArg === flattenedArgs[index])
);
}

return false;
}

/**
* Deploy MACI contracts
*
* @param args - deploy maci arguments
* @param options - ws hooks
* @returns - deployed maci contract
* @throws error if deploy is not successful
*/
async deployMaci({ approval, sessionKeyAddress, chain, config }: IDeployMaciArgs) {
const publicClient = getPublicClient(process.env.COORDINATOR_RPC_URL!)

// get the session key from storage
const sessionKey = this.fileService.getSessionKey(sessionKeyAddress);

if (!sessionKey) {
this.logger.error(`Session key not found: ${sessionKeyAddress}`);
throw new Error(ErrorCodes.SESSION_KEY_NOT_FOUND);
}

const kernelClient = await getKernelClient(sessionKey, approval, chain);

// if the initial voice credit proxy is not already deployed, we need to deploy it
if (!this.checkIfAlreadyDeployed(config.initialVoiceCreditsProxy.type as unknown as EContracts, config.initialVoiceCreditsProxy.args, chain)) {
const voiceCreditProxyAbiAndBytecode = this.getVoiceCreditProxyAbiAndBytecode(config.initialVoiceCreditsProxy.type);

const voiceCreditProxyDeployTx = await kernelClient.deployContract({
abi: voiceCreditProxyAbiAndBytecode.abi,
args: Object.values(config.initialVoiceCreditsProxy.args),
bytecode: voiceCreditProxyAbiAndBytecode.bytecode,
account: kernelClient.account.address,
});

const receipt = await publicClient.waitForTransactionReceipt({
hash: voiceCreditProxyDeployTx
})

// get the voice credit proxy address from the event log
const voiceCreditProxyAddress = getDeployedContractAddress(receipt)

// if the voice credit proxy address is not found, we need to throw an error
if (!voiceCreditProxyAddress) {
throw new Error(ErrorCodes.FAILED_TO_DEPLOY_VOICE_CREDIT_PROXY);
}

await this.storage.register({
id: config.initialVoiceCreditsProxy.type as unknown as EContracts,
contract: new BaseContract(voiceCreditProxyAddress, ConstantInitialVoiceCreditProxyFactory.abi),
args: Object.values(config.initialVoiceCreditsProxy.args),
network: chain,
});
}

// if the gatekeeper is not already deployed, we need to deploy it
if (!this.checkIfAlreadyDeployed(config.gatekeeper.type as unknown as EContracts, config.gatekeeper.args, chain)) {
const gatekeeperAbiAndBytecode = this.getGatekeeperAbiAndBytecode(config.gatekeeper.type);

const gatekeeperDeployTx = await kernelClient.deployContract({
abi: gatekeeperAbiAndBytecode.abi,
args: Object.values(config.gatekeeper.args),
bytecode: gatekeeperAbiAndBytecode.bytecode,
account: kernelClient.account.address,
});

const receipt = await publicClient.waitForTransactionReceipt({
hash: gatekeeperDeployTx
})

// get the gatekeeper address from the event log
const gatekeeperAddress = getDeployedContractAddress(receipt)

// if the gatekeeper address is not found, we need to throw an error
if (!gatekeeperAddress) {
throw new Error(ErrorCodes.FAILED_TO_DEPLOY_GATEKEEPER);
}

// store the gatekeeper address in the storage
await this.storage.register({
id: config.gatekeeper.type as unknown as EContracts,
contract: new BaseContract(gatekeeperAddress, gatekeeperAbiAndBytecode.abi as InterfaceAbi),
args: Object.values(config.gatekeeper.args),
network: chain,
});
}
}

async deployPoll({ approval, sessionKeyAddress, chain, config }: IDeployPollArgs) {
// // get the session key from storage
// const sessionKey = this.fileService.getSessionKey(sessionKeyAddress);
// const kernelClient = await getKernelClient(sessionKey, approval, chain);
}
}
Loading

0 comments on commit 7fca3ee

Please sign in to comment.