-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
603 additions
and
55 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
Oops, something went wrong.