diff --git a/src/index.ts b/src/index.ts
index 31f1a13..829ac81 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -150,6 +150,47 @@ export {
ApprovalSpend,
CrossChainOrder,
WithdrawRequest,
+ UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA,
+ UNIVERSAL_EMAIL_RECOVERY_ADDRESS__BASE,
+ MAX_VALIDATORS,
+ MAX_NUMBER_OF_GUARDIANS,
+ MINIMUM_RECOVERY_WINDOW,
+ CANCEL_EXPIRED_RECOVERY_COOLDOWN,
+ getUniversalEmailRecoveryExecutor,
+ getRecoveryConfig,
+ getRecoveryRequest,
+ getPreviousRecoveryRequest,
+ isActivated,
+ canStartRecoveryRequest,
+ getAllowValidatorRecoveryAction,
+ getDisallowValidatorRecoveryAction,
+ getAllowedValidators,
+ getAllowedSelectors,
+ acceptanceCommandTemplates,
+ recoveryCommandTemplates,
+ extractRecoveredAccountFromAcceptanceCommand,
+ extractRecoveredAccountFromRecoveryCommand,
+ computeAcceptanceTemplateId,
+ computeRecoveryTemplateId,
+ getVerifier,
+ getDkim,
+ getEmailAuthImplementation,
+ getUpdateRecoveryConfigAction,
+ getHandleAcceptanceAction,
+ getHandleRecoveryAction,
+ getCompleteRecoveryAction,
+ getCancelRecoveryAction,
+ getCancelExpiredRecoveryAction,
+ computeEmailAuthAddress,
+ getGuardianConfig,
+ getGuardian,
+ getAllGuardians,
+ hasGuardianVoted,
+ getAddGuardianAction,
+ getRemoveGuardianAction,
+ getChangeThresholdAction,
+ EmailAuthMsg,
+ EmailProof,
} from './module'
export type { ModuleType, Module, SigHookInit } from './module'
diff --git a/src/module/index.ts b/src/module/index.ts
index 6c85d50..e779081 100644
--- a/src/module/index.ts
+++ b/src/module/index.ts
@@ -177,6 +177,50 @@ export {
RHINESTONE_ATTESTER_ADDRESS,
} from './registry'
+export {
+ UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA,
+ UNIVERSAL_EMAIL_RECOVERY_ADDRESS__BASE,
+ MAX_VALIDATORS,
+ MAX_NUMBER_OF_GUARDIANS,
+ MINIMUM_RECOVERY_WINDOW,
+ CANCEL_EXPIRED_RECOVERY_COOLDOWN,
+ getUniversalEmailRecoveryExecutor,
+ getRecoveryConfig,
+ getRecoveryRequest,
+ getPreviousRecoveryRequest,
+ isActivated,
+ canStartRecoveryRequest,
+ getAllowValidatorRecoveryAction,
+ getDisallowValidatorRecoveryAction,
+ getAllowedValidators,
+ getAllowedSelectors,
+ acceptanceCommandTemplates,
+ recoveryCommandTemplates,
+ extractRecoveredAccountFromAcceptanceCommand,
+ extractRecoveredAccountFromRecoveryCommand,
+ computeAcceptanceTemplateId,
+ computeRecoveryTemplateId,
+ getVerifier,
+ getDkim,
+ getEmailAuthImplementation,
+ getUpdateRecoveryConfigAction,
+ getHandleAcceptanceAction,
+ getHandleRecoveryAction,
+ getCompleteRecoveryAction,
+ getCancelRecoveryAction,
+ getCancelExpiredRecoveryAction,
+ computeEmailAuthAddress,
+ getGuardianConfig,
+ getGuardian,
+ getAllGuardians,
+ hasGuardianVoted,
+ getAddGuardianAction,
+ getRemoveGuardianAction,
+ getChangeThresholdAction,
+ EmailAuthMsg,
+ EmailProof,
+} from './zk-email-recovery/universal-email-recovery'
+
export type {
ModuleType,
Module,
diff --git a/src/module/zk-email-recovery/universal-email-recovery/abi.ts b/src/module/zk-email-recovery/universal-email-recovery/abi.ts
new file mode 100644
index 0000000..1cbb98a
--- /dev/null
+++ b/src/module/zk-email-recovery/universal-email-recovery/abi.ts
@@ -0,0 +1,1941 @@
+export const abi = [
+ {
+ type: 'constructor',
+ inputs: [
+ {
+ name: 'verifier',
+ type: 'address',
+ internalType: 'address',
+ },
+ {
+ name: 'dkimRegistry',
+ type: 'address',
+ internalType: 'address',
+ },
+ {
+ name: 'emailAuthImpl',
+ type: 'address',
+ internalType: 'address',
+ },
+ {
+ name: 'commandHandler',
+ type: 'address',
+ internalType: 'address',
+ },
+ {
+ name: 'minimumDelay',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'killSwitchAuthorizer',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'CANCEL_EXPIRED_RECOVERY_COOLDOWN',
+ inputs: [],
+ outputs: [
+ {
+ name: '',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'MAX_VALIDATORS',
+ inputs: [],
+ outputs: [
+ {
+ name: '',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'MINIMUM_RECOVERY_WINDOW',
+ inputs: [],
+ outputs: [
+ {
+ name: '',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'acceptanceCommandTemplates',
+ inputs: [],
+ outputs: [
+ {
+ name: '',
+ type: 'string[][]',
+ internalType: 'string[][]',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'addGuardian',
+ inputs: [
+ {
+ name: 'guardian',
+ type: 'address',
+ internalType: 'address',
+ },
+ {
+ name: 'weight',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'allowValidatorRecovery',
+ inputs: [
+ {
+ name: 'validator',
+ type: 'address',
+ internalType: 'address',
+ },
+ {
+ name: 'isInstalledContext',
+ type: 'bytes',
+ internalType: 'bytes',
+ },
+ {
+ name: 'recoverySelector',
+ type: 'bytes4',
+ internalType: 'bytes4',
+ },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'canStartRecoveryRequest',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ internalType: 'address',
+ },
+ {
+ name: 'validator',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ outputs: [
+ {
+ name: '',
+ type: 'bool',
+ internalType: 'bool',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'cancelExpiredRecovery',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'cancelRecovery',
+ inputs: [],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'changeThreshold',
+ inputs: [
+ {
+ name: 'threshold',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'commandHandler',
+ inputs: [],
+ outputs: [
+ {
+ name: '',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'completeRecovery',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ internalType: 'address',
+ },
+ {
+ name: 'recoveryData',
+ type: 'bytes',
+ internalType: 'bytes',
+ },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'computeAcceptanceTemplateId',
+ inputs: [
+ {
+ name: 'templateIdx',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ outputs: [
+ {
+ name: '',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ stateMutability: 'pure',
+ },
+ {
+ type: 'function',
+ name: 'computeEmailAuthAddress',
+ inputs: [
+ {
+ name: 'recoveredAccount',
+ type: 'address',
+ internalType: 'address',
+ },
+ {
+ name: 'accountSalt',
+ type: 'bytes32',
+ internalType: 'bytes32',
+ },
+ ],
+ outputs: [
+ {
+ name: '',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'computeRecoveryTemplateId',
+ inputs: [
+ {
+ name: 'templateIdx',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ outputs: [
+ {
+ name: '',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ stateMutability: 'pure',
+ },
+ {
+ type: 'function',
+ name: 'disallowValidatorRecovery',
+ inputs: [
+ {
+ name: 'validator',
+ type: 'address',
+ internalType: 'address',
+ },
+ {
+ name: 'prevValidator',
+ type: 'address',
+ internalType: 'address',
+ },
+ {
+ name: 'recoverySelector',
+ type: 'bytes4',
+ internalType: 'bytes4',
+ },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'dkim',
+ inputs: [],
+ outputs: [
+ {
+ name: '',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'dkimAddr',
+ inputs: [],
+ outputs: [
+ {
+ name: '',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'emailAuthImplementation',
+ inputs: [],
+ outputs: [
+ {
+ name: '',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'emailAuthImplementationAddr',
+ inputs: [],
+ outputs: [
+ {
+ name: '',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'extractRecoveredAccountFromAcceptanceCommand',
+ inputs: [
+ {
+ name: 'commandParams',
+ type: 'bytes[]',
+ internalType: 'bytes[]',
+ },
+ {
+ name: 'templateIdx',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ outputs: [
+ {
+ name: '',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'extractRecoveredAccountFromRecoveryCommand',
+ inputs: [
+ {
+ name: 'commandParams',
+ type: 'bytes[]',
+ internalType: 'bytes[]',
+ },
+ {
+ name: 'templateIdx',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ outputs: [
+ {
+ name: '',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'getAllGuardians',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ outputs: [
+ {
+ name: '',
+ type: 'address[]',
+ internalType: 'address[]',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'getAllowedSelectors',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ outputs: [
+ {
+ name: '',
+ type: 'bytes4[]',
+ internalType: 'bytes4[]',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'getAllowedValidators',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ outputs: [
+ {
+ name: '',
+ type: 'address[]',
+ internalType: 'address[]',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'getGuardian',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ internalType: 'address',
+ },
+ {
+ name: 'guardian',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ outputs: [
+ {
+ name: '',
+ type: 'tuple',
+ internalType: 'struct GuardianStorage',
+ components: [
+ {
+ name: 'status',
+ type: 'uint8',
+ internalType: 'enum GuardianStatus',
+ },
+ {
+ name: 'weight',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'getGuardianConfig',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ outputs: [
+ {
+ name: '',
+ type: 'tuple',
+ internalType: 'struct IGuardianManager.GuardianConfig',
+ components: [
+ {
+ name: 'guardianCount',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'totalWeight',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'acceptedWeight',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'threshold',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'getPreviousRecoveryRequest',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ outputs: [
+ {
+ name: '',
+ type: 'tuple',
+ internalType: 'struct IEmailRecoveryManager.PreviousRecoveryRequest',
+ components: [
+ {
+ name: 'previousGuardianInitiated',
+ type: 'address',
+ internalType: 'address',
+ },
+ {
+ name: 'cancelRecoveryCooldown',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'getRecoveryConfig',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ outputs: [
+ {
+ name: '',
+ type: 'tuple',
+ internalType: 'struct IEmailRecoveryManager.RecoveryConfig',
+ components: [
+ {
+ name: 'delay',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'expiry',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'getRecoveryRequest',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ outputs: [
+ {
+ name: 'executeAfter',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'executeBefore',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'currentWeight',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'recoveryDataHash',
+ type: 'bytes32',
+ internalType: 'bytes32',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'handleAcceptance',
+ inputs: [
+ {
+ name: 'emailAuthMsg',
+ type: 'tuple',
+ internalType: 'struct EmailAuthMsg',
+ components: [
+ {
+ name: 'templateId',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'commandParams',
+ type: 'bytes[]',
+ internalType: 'bytes[]',
+ },
+ {
+ name: 'skippedCommandPrefix',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'proof',
+ type: 'tuple',
+ internalType: 'struct EmailProof',
+ components: [
+ {
+ name: 'domainName',
+ type: 'string',
+ internalType: 'string',
+ },
+ {
+ name: 'publicKeyHash',
+ type: 'bytes32',
+ internalType: 'bytes32',
+ },
+ {
+ name: 'timestamp',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'maskedCommand',
+ type: 'string',
+ internalType: 'string',
+ },
+ {
+ name: 'emailNullifier',
+ type: 'bytes32',
+ internalType: 'bytes32',
+ },
+ {
+ name: 'accountSalt',
+ type: 'bytes32',
+ internalType: 'bytes32',
+ },
+ {
+ name: 'isCodeExist',
+ type: 'bool',
+ internalType: 'bool',
+ },
+ {
+ name: 'proof',
+ type: 'bytes',
+ internalType: 'bytes',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ name: 'templateIdx',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'handleRecovery',
+ inputs: [
+ {
+ name: 'emailAuthMsg',
+ type: 'tuple',
+ internalType: 'struct EmailAuthMsg',
+ components: [
+ {
+ name: 'templateId',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'commandParams',
+ type: 'bytes[]',
+ internalType: 'bytes[]',
+ },
+ {
+ name: 'skippedCommandPrefix',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'proof',
+ type: 'tuple',
+ internalType: 'struct EmailProof',
+ components: [
+ {
+ name: 'domainName',
+ type: 'string',
+ internalType: 'string',
+ },
+ {
+ name: 'publicKeyHash',
+ type: 'bytes32',
+ internalType: 'bytes32',
+ },
+ {
+ name: 'timestamp',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'maskedCommand',
+ type: 'string',
+ internalType: 'string',
+ },
+ {
+ name: 'emailNullifier',
+ type: 'bytes32',
+ internalType: 'bytes32',
+ },
+ {
+ name: 'accountSalt',
+ type: 'bytes32',
+ internalType: 'bytes32',
+ },
+ {
+ name: 'isCodeExist',
+ type: 'bool',
+ internalType: 'bool',
+ },
+ {
+ name: 'proof',
+ type: 'bytes',
+ internalType: 'bytes',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ name: 'templateIdx',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'hasGuardianVoted',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ internalType: 'address',
+ },
+ {
+ name: 'guardian',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ outputs: [
+ {
+ name: '',
+ type: 'bool',
+ internalType: 'bool',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'isActivated',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ outputs: [
+ {
+ name: '',
+ type: 'bool',
+ internalType: 'bool',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'isInitialized',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ outputs: [
+ {
+ name: '',
+ type: 'bool',
+ internalType: 'bool',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'isModuleType',
+ inputs: [
+ {
+ name: 'typeID',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ outputs: [
+ {
+ name: '',
+ type: 'bool',
+ internalType: 'bool',
+ },
+ ],
+ stateMutability: 'pure',
+ },
+ {
+ type: 'function',
+ name: 'killSwitchEnabled',
+ inputs: [],
+ outputs: [
+ {
+ name: '',
+ type: 'bool',
+ internalType: 'bool',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'minimumDelay',
+ inputs: [],
+ outputs: [
+ {
+ name: '',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'name',
+ inputs: [],
+ outputs: [
+ {
+ name: '',
+ type: 'string',
+ internalType: 'string',
+ },
+ ],
+ stateMutability: 'pure',
+ },
+ {
+ type: 'function',
+ name: 'onInstall',
+ inputs: [
+ {
+ name: 'data',
+ type: 'bytes',
+ internalType: 'bytes',
+ },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'onUninstall',
+ inputs: [
+ {
+ name: '',
+ type: 'bytes',
+ internalType: 'bytes',
+ },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'owner',
+ inputs: [],
+ outputs: [
+ {
+ name: '',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'recoveryCommandTemplates',
+ inputs: [],
+ outputs: [
+ {
+ name: '',
+ type: 'string[][]',
+ internalType: 'string[][]',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'removeGuardian',
+ inputs: [
+ {
+ name: 'guardian',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'renounceOwnership',
+ inputs: [],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'toggleKillSwitch',
+ inputs: [],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'transferOwnership',
+ inputs: [
+ {
+ name: 'newOwner',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'updateRecoveryConfig',
+ inputs: [
+ {
+ name: 'recoveryConfig',
+ type: 'tuple',
+ internalType: 'struct IEmailRecoveryManager.RecoveryConfig',
+ components: [
+ {
+ name: 'delay',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'expiry',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ },
+ ],
+ outputs: [],
+ stateMutability: 'nonpayable',
+ },
+ {
+ type: 'function',
+ name: 'validatorCount',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ outputs: [
+ {
+ name: 'count',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'verifier',
+ inputs: [],
+ outputs: [
+ {
+ name: '',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'verifierAddr',
+ inputs: [],
+ outputs: [
+ {
+ name: '',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ stateMutability: 'view',
+ },
+ {
+ type: 'function',
+ name: 'version',
+ inputs: [],
+ outputs: [
+ {
+ name: '',
+ type: 'string',
+ internalType: 'string',
+ },
+ ],
+ stateMutability: 'pure',
+ },
+ {
+ type: 'event',
+ name: 'AddedGuardian',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ {
+ name: 'guardian',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ {
+ name: 'weight',
+ type: 'uint256',
+ indexed: false,
+ internalType: 'uint256',
+ },
+ ],
+ anonymous: false,
+ },
+ {
+ type: 'event',
+ name: 'ChangedThreshold',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ {
+ name: 'threshold',
+ type: 'uint256',
+ indexed: false,
+ internalType: 'uint256',
+ },
+ ],
+ anonymous: false,
+ },
+ {
+ type: 'event',
+ name: 'GuardianAccepted',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ {
+ name: 'guardian',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ ],
+ anonymous: false,
+ },
+ {
+ type: 'event',
+ name: 'GuardianStatusUpdated',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ {
+ name: 'guardian',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ {
+ name: 'newStatus',
+ type: 'uint8',
+ indexed: false,
+ internalType: 'enum GuardianStatus',
+ },
+ ],
+ anonymous: false,
+ },
+ {
+ type: 'event',
+ name: 'GuardianVoted',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ {
+ name: 'guardian',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ {
+ name: 'currentWeight',
+ type: 'uint256',
+ indexed: false,
+ internalType: 'uint256',
+ },
+ {
+ name: 'guardianWeight',
+ type: 'uint256',
+ indexed: false,
+ internalType: 'uint256',
+ },
+ ],
+ anonymous: false,
+ },
+ {
+ type: 'event',
+ name: 'NewValidatorRecovery',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ {
+ name: 'validator',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ {
+ name: 'recoverySelector',
+ type: 'bytes4',
+ indexed: false,
+ internalType: 'bytes4',
+ },
+ ],
+ anonymous: false,
+ },
+ {
+ type: 'event',
+ name: 'OwnershipTransferred',
+ inputs: [
+ {
+ name: 'previousOwner',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ {
+ name: 'newOwner',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ ],
+ anonymous: false,
+ },
+ {
+ type: 'event',
+ name: 'RecoveryCancelled',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ ],
+ anonymous: false,
+ },
+ {
+ type: 'event',
+ name: 'RecoveryCompleted',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ ],
+ anonymous: false,
+ },
+ {
+ type: 'event',
+ name: 'RecoveryConfigUpdated',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ {
+ name: 'delay',
+ type: 'uint256',
+ indexed: false,
+ internalType: 'uint256',
+ },
+ {
+ name: 'expiry',
+ type: 'uint256',
+ indexed: false,
+ internalType: 'uint256',
+ },
+ ],
+ anonymous: false,
+ },
+ {
+ type: 'event',
+ name: 'RecoveryConfigured',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ {
+ name: 'guardianCount',
+ type: 'uint256',
+ indexed: false,
+ internalType: 'uint256',
+ },
+ {
+ name: 'totalWeight',
+ type: 'uint256',
+ indexed: false,
+ internalType: 'uint256',
+ },
+ {
+ name: 'threshold',
+ type: 'uint256',
+ indexed: false,
+ internalType: 'uint256',
+ },
+ ],
+ anonymous: false,
+ },
+ {
+ type: 'event',
+ name: 'RecoveryDeInitialized',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ ],
+ anonymous: false,
+ },
+ {
+ type: 'event',
+ name: 'RecoveryExecuted',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ {
+ name: 'validator',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ ],
+ anonymous: false,
+ },
+ {
+ type: 'event',
+ name: 'RecoveryRequestComplete',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ {
+ name: 'guardian',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ {
+ name: 'executeAfter',
+ type: 'uint256',
+ indexed: false,
+ internalType: 'uint256',
+ },
+ {
+ name: 'executeBefore',
+ type: 'uint256',
+ indexed: false,
+ internalType: 'uint256',
+ },
+ {
+ name: 'recoveryDataHash',
+ type: 'bytes32',
+ indexed: false,
+ internalType: 'bytes32',
+ },
+ ],
+ anonymous: false,
+ },
+ {
+ type: 'event',
+ name: 'RecoveryRequestStarted',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ {
+ name: 'guardian',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ {
+ name: 'executeBefore',
+ type: 'uint256',
+ indexed: false,
+ internalType: 'uint256',
+ },
+ {
+ name: 'recoveryDataHash',
+ type: 'bytes32',
+ indexed: false,
+ internalType: 'bytes32',
+ },
+ ],
+ anonymous: false,
+ },
+ {
+ type: 'event',
+ name: 'RemovedGuardian',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ {
+ name: 'guardian',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ {
+ name: 'weight',
+ type: 'uint256',
+ indexed: false,
+ internalType: 'uint256',
+ },
+ ],
+ anonymous: false,
+ },
+ {
+ type: 'event',
+ name: 'RemovedValidatorRecovery',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ {
+ name: 'validator',
+ type: 'address',
+ indexed: true,
+ internalType: 'address',
+ },
+ {
+ name: 'recoverySelector',
+ type: 'bytes4',
+ indexed: false,
+ internalType: 'bytes4',
+ },
+ ],
+ anonymous: false,
+ },
+ {
+ type: 'error',
+ name: 'AccountNotConfigured',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'AddressAlreadyGuardian',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'AddressNotGuardianForAccount',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'AlreadyInitialized',
+ inputs: [
+ {
+ name: 'smartAccount',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ },
+ {
+ type: 'error',
+ name: 'DelayLessThanMinimumDelay',
+ inputs: [
+ {
+ name: 'delay',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'minimumDelay',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ },
+ {
+ type: 'error',
+ name: 'DelayMoreThanExpiry',
+ inputs: [
+ {
+ name: 'delay',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'expiry',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ },
+ {
+ type: 'error',
+ name: 'DelayNotPassed',
+ inputs: [
+ {
+ name: 'blockTimestamp',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'executeAfter',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ },
+ {
+ type: 'error',
+ name: 'GuardianAlreadyVoted',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'GuardianMustWaitForCooldown',
+ inputs: [
+ {
+ name: 'guardian',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ },
+ {
+ type: 'error',
+ name: 'IncorrectNumberOfWeights',
+ inputs: [
+ {
+ name: 'guardianCount',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'weightCount',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ },
+ {
+ type: 'error',
+ name: 'InvalidAccountAddress',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'InvalidCommandHandler',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'InvalidDkimRegistry',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'InvalidEmailAuthImpl',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'InvalidFactory',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'InvalidGuardianAddress',
+ inputs: [
+ {
+ name: 'guardian',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ },
+ {
+ type: 'error',
+ name: 'InvalidGuardianStatus',
+ inputs: [
+ {
+ name: 'guardianStatus',
+ type: 'uint8',
+ internalType: 'enum GuardianStatus',
+ },
+ {
+ name: 'expectedGuardianStatus',
+ type: 'uint8',
+ internalType: 'enum GuardianStatus',
+ },
+ ],
+ },
+ {
+ type: 'error',
+ name: 'InvalidGuardianWeight',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'InvalidKillSwitchAuthorizer',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'InvalidOnInstallData',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'InvalidProxyBytecodeHash',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'InvalidRecoveryDataHash',
+ inputs: [
+ {
+ name: 'recoveryDataHash',
+ type: 'bytes32',
+ internalType: 'bytes32',
+ },
+ {
+ name: 'expectedRecoveryDataHash',
+ type: 'bytes32',
+ internalType: 'bytes32',
+ },
+ ],
+ },
+ {
+ type: 'error',
+ name: 'InvalidSelector',
+ inputs: [
+ {
+ name: 'selector',
+ type: 'bytes4',
+ internalType: 'bytes4',
+ },
+ ],
+ },
+ {
+ type: 'error',
+ name: 'InvalidValidator',
+ inputs: [
+ {
+ name: 'validator',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ },
+ {
+ type: 'error',
+ name: 'InvalidVerifier',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'KillSwitchEnabled',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'LinkedList_AlreadyInitialized',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'LinkedList_EntryAlreadyInList',
+ inputs: [
+ {
+ name: 'entry',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ },
+ {
+ type: 'error',
+ name: 'LinkedList_InvalidEntry',
+ inputs: [
+ {
+ name: 'entry',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ },
+ {
+ type: 'error',
+ name: 'LinkedList_InvalidPage',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'MaxNumberOfGuardiansReached',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'MaxValidatorsReached',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'NoRecoveryConfigured',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'NoRecoveryInProcess',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'NotEnoughApprovals',
+ inputs: [
+ {
+ name: 'currentWeight',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'threshold',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ },
+ {
+ type: 'error',
+ name: 'NotInitialized',
+ inputs: [
+ {
+ name: 'smartAccount',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ },
+ {
+ type: 'error',
+ name: 'OwnableInvalidOwner',
+ inputs: [
+ {
+ name: 'owner',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ },
+ {
+ type: 'error',
+ name: 'OwnableUnauthorizedAccount',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ internalType: 'address',
+ },
+ ],
+ },
+ {
+ type: 'error',
+ name: 'RecoveryHasNotExpired',
+ inputs: [
+ {
+ name: 'account',
+ type: 'address',
+ internalType: 'address',
+ },
+ {
+ name: 'blockTimestamp',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'executeBefore',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ },
+ {
+ type: 'error',
+ name: 'RecoveryInProcess',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'RecoveryIsNotActivated',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'RecoveryModuleNotInitialized',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'RecoveryRequestExpired',
+ inputs: [
+ {
+ name: 'blockTimestamp',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'executeBefore',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ },
+ {
+ type: 'error',
+ name: 'RecoveryWindowTooShort',
+ inputs: [
+ {
+ name: 'recoveryWindow',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ },
+ {
+ type: 'error',
+ name: 'SetupAlreadyCalled',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'SetupNotCalled',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'StatusCannotBeTheSame',
+ inputs: [
+ {
+ name: 'newStatus',
+ type: 'uint8',
+ internalType: 'enum GuardianStatus',
+ },
+ ],
+ },
+ {
+ type: 'error',
+ name: 'ThresholdCannotBeZero',
+ inputs: [],
+ },
+ {
+ type: 'error',
+ name: 'ThresholdExceedsAcceptedWeight',
+ inputs: [
+ {
+ name: 'threshold',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'acceptedWeight',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ },
+ {
+ type: 'error',
+ name: 'ThresholdExceedsTotalWeight',
+ inputs: [
+ {
+ name: 'threshold',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ {
+ name: 'totalWeight',
+ type: 'uint256',
+ internalType: 'uint256',
+ },
+ ],
+ },
+ {
+ type: 'error',
+ name: 'TooManyValuesToRemove',
+ inputs: [],
+ },
+] as const
diff --git a/src/module/zk-email-recovery/universal-email-recovery/constants.ts b/src/module/zk-email-recovery/universal-email-recovery/constants.ts
new file mode 100644
index 0000000..8270c26
--- /dev/null
+++ b/src/module/zk-email-recovery/universal-email-recovery/constants.ts
@@ -0,0 +1,23 @@
+import { Address } from 'viem'
+import { base, sepolia } from 'viem/chains'
+
+export const UNIVERSAL_EMAIL_RECOVERY_ADDRESS__BASE: Address =
+ '0x36A470159F8170ad262B9518095a9FeD0824e7dD'
+export const UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA: Address =
+ '0x8ECcb707C4770239D7e95743cd01aaA72d6D313E'
+
+export const MAX_VALIDATORS = 32
+export const MAX_NUMBER_OF_GUARDIANS = 32
+export const MINIMUM_RECOVERY_WINDOW = 2 * 24 * 60 * 60 // 2 days in seconds;
+export const CANCEL_EXPIRED_RECOVERY_COOLDOWN = 24 * 60 * 60 // 1 day in seconds;
+
+/** Helper function get the module address based on chain id */
+export const getModuleAddress = (chainId: number): Address => {
+ if (chainId === sepolia.id) {
+ return UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA
+ }
+ if (chainId === base.id) {
+ return UNIVERSAL_EMAIL_RECOVERY_ADDRESS__BASE
+ }
+ throw new Error('Unsupported chain')
+}
diff --git a/src/module/zk-email-recovery/universal-email-recovery/index.ts b/src/module/zk-email-recovery/universal-email-recovery/index.ts
new file mode 100644
index 0000000..799be5b
--- /dev/null
+++ b/src/module/zk-email-recovery/universal-email-recovery/index.ts
@@ -0,0 +1,3 @@
+export * from './installation'
+export * from './usage'
+export * from './constants'
diff --git a/src/module/zk-email-recovery/universal-email-recovery/installation.ts b/src/module/zk-email-recovery/universal-email-recovery/installation.ts
new file mode 100644
index 0000000..93257d3
--- /dev/null
+++ b/src/module/zk-email-recovery/universal-email-recovery/installation.ts
@@ -0,0 +1,58 @@
+import { Address, encodeAbiParameters, Hex } from 'viem'
+import { Module } from '../../types'
+import { getModuleAddress } from './constants'
+
+export const getUniversalEmailRecoveryExecutor = ({
+ validator,
+ isInstalledContext,
+ initialSelector,
+ guardians,
+ weights,
+ threshold,
+ delay,
+ expiry,
+ chainId,
+ hook,
+}: {
+ validator: Address
+ isInstalledContext: Hex
+ initialSelector: Hex
+ guardians: Array
+ weights: Array
+ threshold: bigint
+ delay: bigint
+ expiry: bigint
+ chainId: number
+ hook?: Address
+}): Module => {
+ return {
+ address: getModuleAddress(chainId),
+ module: getModuleAddress(chainId),
+ initData: encodeAbiParameters(
+ [
+ { name: 'validator', type: 'address' },
+ { name: 'isInstalledContext', type: 'bytes' },
+ { name: 'initialSelector', type: 'bytes4' },
+ { name: 'guardians', type: 'address[]' },
+ { name: 'weights', type: 'uint256[]' },
+ { name: 'delay', type: 'uint256' },
+ { name: 'expiry', type: 'uint256' },
+ { name: 'threshold', type: 'uint256' },
+ ],
+ [
+ validator,
+ isInstalledContext,
+ initialSelector,
+ guardians,
+ weights.map((weight) => BigInt(weight)),
+ BigInt(threshold),
+ BigInt(delay),
+ BigInt(expiry),
+ ],
+ ),
+ deInitData: '0x',
+ additionalContext: '0x',
+ type: 'executor',
+ hook,
+ }
+}
diff --git a/src/module/zk-email-recovery/universal-email-recovery/usage.ts b/src/module/zk-email-recovery/universal-email-recovery/usage.ts
new file mode 100644
index 0000000..d84f723
--- /dev/null
+++ b/src/module/zk-email-recovery/universal-email-recovery/usage.ts
@@ -0,0 +1,731 @@
+import { getModuleAddress } from './constants'
+import { Execution } from '../../../account/types'
+import {
+ Address,
+ encodeFunctionData,
+ Hex,
+ PublicClient,
+ toHex,
+ zeroAddress,
+} from 'viem'
+import { Account } from '../../../account/types'
+import { abi } from './abi'
+
+export type EmailAuthMsg = {
+ templateId: bigint
+ commandParams: Hex[]
+ skippedCommandPrefix: bigint
+ proof: EmailProof
+}
+
+export type EmailProof = {
+ domainName: string
+ publicKeyHash: Hex
+ timestamp: bigint
+ maskedCommand: string
+ emailNullifier: Hex
+ accountSalt: Hex
+ isCodeExist: boolean
+ proof: Hex
+}
+
+export const getRecoveryConfig = async ({
+ account,
+ client,
+}: {
+ account: Account
+ client: PublicClient
+}): Promise<{ delay: bigint; expiry: bigint }> => {
+ const address = getModuleAddress(await client.getChainId())
+ try {
+ return await client.readContract({
+ address,
+ abi,
+ functionName: 'getRecoveryConfig',
+ args: [account.address],
+ })
+ } catch (err) {
+ return { delay: 0n, expiry: 0n }
+ }
+}
+
+export const getRecoveryRequest = async ({
+ account,
+ client,
+}: {
+ account: Account
+ client: PublicClient
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ try {
+ return await client.readContract({
+ address,
+ abi,
+ functionName: 'getRecoveryRequest',
+ args: [account.address],
+ })
+ } catch (err) {
+ return [0n, 0n, 0n, toHex(0, { size: 32 })]
+ }
+}
+
+export const getPreviousRecoveryRequest = async ({
+ account,
+ client,
+}: {
+ account: Account
+ client: PublicClient
+}): Promise<{
+ previousGuardianInitiated: Address
+ cancelRecoveryCooldown: bigint
+}> => {
+ const address = getModuleAddress(await client.getChainId())
+ try {
+ return await client.readContract({
+ address,
+ abi,
+ functionName: 'getPreviousRecoveryRequest',
+ args: [account.address],
+ })
+ } catch (err) {
+ return {
+ previousGuardianInitiated: zeroAddress,
+ cancelRecoveryCooldown: 0n,
+ }
+ }
+}
+
+export const isActivated = async ({
+ account,
+ client,
+}: {
+ account: Account
+ client: PublicClient
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ try {
+ return await client.readContract({
+ address,
+ abi,
+ functionName: 'isActivated',
+ args: [account.address],
+ })
+ } catch (err) {
+ return false
+ }
+}
+
+export const canStartRecoveryRequest = async ({
+ account,
+ client,
+ validator,
+}: {
+ account: Account
+ client: PublicClient
+ validator: Address
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ try {
+ return await client.readContract({
+ address,
+ abi,
+ functionName: 'canStartRecoveryRequest',
+ args: [account.address, validator],
+ })
+ } catch (err) {
+ return false
+ }
+}
+
+export const getAllowValidatorRecoveryAction = async ({
+ client,
+ validator,
+ isInstalledContext,
+ recoverySelector,
+}: {
+ client: PublicClient
+ validator: Address
+ isInstalledContext: Hex
+ recoverySelector: Hex
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ const data = encodeFunctionData({
+ functionName: 'allowValidatorRecovery',
+ abi,
+ args: [validator, isInstalledContext, recoverySelector],
+ })
+
+ return {
+ to: address,
+ target: address,
+ value: 0n,
+ callData: data,
+ data,
+ }
+}
+
+export const getDisallowValidatorRecoveryAction = async ({
+ client,
+ validator,
+ prevValidator,
+ recoverySelector,
+}: {
+ client: PublicClient
+ validator: Address
+ prevValidator: Address
+ recoverySelector: Hex
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ const data = encodeFunctionData({
+ functionName: 'disallowValidatorRecovery',
+ abi,
+ args: [validator, prevValidator, recoverySelector],
+ })
+
+ return {
+ to: address,
+ target: address,
+ value: 0n,
+ callData: data,
+ data,
+ }
+}
+
+export const getAllowedValidators = async ({
+ account,
+ client,
+}: {
+ account: Account
+ client: PublicClient
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ try {
+ return await client.readContract({
+ address,
+ abi,
+ functionName: 'getAllowedValidators',
+ args: [account.address],
+ })
+ } catch (err) {
+ return []
+ }
+}
+
+export const getAllowedSelectors = async ({
+ account,
+ client,
+}: {
+ account: Account
+ client: PublicClient
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ try {
+ return await client.readContract({
+ address,
+ abi,
+ functionName: 'getAllowedSelectors',
+ args: [account.address],
+ })
+ } catch (err) {
+ return []
+ }
+}
+
+export const acceptanceCommandTemplates = async ({
+ client,
+}: {
+ client: PublicClient
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ try {
+ return await client.readContract({
+ address,
+ abi,
+ functionName: 'acceptanceCommandTemplates',
+ })
+ } catch (err) {
+ return []
+ }
+}
+
+export const recoveryCommandTemplates = async ({
+ client,
+}: {
+ client: PublicClient
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ try {
+ return await client.readContract({
+ address,
+ abi,
+ functionName: 'recoveryCommandTemplates',
+ })
+ } catch (err) {
+ return []
+ }
+}
+
+export const extractRecoveredAccountFromAcceptanceCommand = async ({
+ client,
+ commandParams,
+ templateIdx,
+}: {
+ client: PublicClient
+ commandParams: Hex[]
+ templateIdx: bigint
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ try {
+ return await client.readContract({
+ address,
+ abi,
+ functionName: 'extractRecoveredAccountFromAcceptanceCommand',
+ args: [commandParams, templateIdx],
+ })
+ } catch (err) {
+ return zeroAddress
+ }
+}
+
+export const extractRecoveredAccountFromRecoveryCommand = async ({
+ client,
+ commandParams,
+ templateIdx,
+}: {
+ client: PublicClient
+ commandParams: Hex[]
+ templateIdx: bigint
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ try {
+ return await client.readContract({
+ address,
+ abi,
+ functionName: 'extractRecoveredAccountFromRecoveryCommand',
+ args: [commandParams, templateIdx],
+ })
+ } catch (err) {
+ return zeroAddress
+ }
+}
+
+export const computeAcceptanceTemplateId = async ({
+ client,
+ templateIdx,
+}: {
+ client: PublicClient
+ templateIdx: bigint
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ try {
+ return await client.readContract({
+ address,
+ abi,
+ functionName: 'computeAcceptanceTemplateId',
+ args: [templateIdx],
+ })
+ } catch (err) {
+ return 0n
+ }
+}
+
+export const computeRecoveryTemplateId = async ({
+ client,
+ templateIdx,
+}: {
+ client: PublicClient
+ templateIdx: bigint
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ try {
+ return await client.readContract({
+ address,
+ abi,
+ functionName: 'computeRecoveryTemplateId',
+ args: [templateIdx],
+ })
+ } catch (err) {
+ return 0n
+ }
+}
+
+export const getVerifier = async ({
+ client,
+}: {
+ client: PublicClient
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ try {
+ return await client.readContract({
+ address,
+ abi,
+ functionName: 'verifier',
+ })
+ } catch (err) {
+ return zeroAddress
+ }
+}
+
+export const getDkim = async ({
+ client,
+}: {
+ client: PublicClient
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ try {
+ return await client.readContract({
+ address,
+ abi,
+ functionName: 'dkim',
+ })
+ } catch (err) {
+ return zeroAddress
+ }
+}
+
+export const getEmailAuthImplementation = async ({
+ client,
+}: {
+ client: PublicClient
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ try {
+ return await client.readContract({
+ address,
+ abi,
+ functionName: 'emailAuthImplementation',
+ })
+ } catch (err) {
+ return zeroAddress
+ }
+}
+
+export const getUpdateRecoveryConfigAction = async ({
+ client,
+ delay,
+ expiry,
+}: {
+ client: PublicClient
+ delay: bigint
+ expiry: bigint
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ const data = encodeFunctionData({
+ functionName: 'updateRecoveryConfig',
+ abi,
+ args: [{ delay, expiry }],
+ })
+
+ return {
+ to: address,
+ target: address,
+ value: 0n,
+ callData: data,
+ data,
+ }
+}
+
+export const getHandleAcceptanceAction = async ({
+ client,
+ emailAuthMsg,
+ templateIdx,
+}: {
+ client: PublicClient
+ emailAuthMsg: EmailAuthMsg
+ templateIdx: bigint
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ const data = encodeFunctionData({
+ functionName: 'handleAcceptance',
+ abi,
+ args: [emailAuthMsg, templateIdx],
+ })
+
+ return {
+ to: address,
+ target: address,
+ value: 0n,
+ callData: data,
+ data,
+ }
+}
+
+export const getHandleRecoveryAction = async ({
+ client,
+ emailAuthMsg,
+ templateIdx,
+}: {
+ client: PublicClient
+ emailAuthMsg: EmailAuthMsg
+ templateIdx: bigint
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ const data = encodeFunctionData({
+ functionName: 'handleRecovery',
+ abi,
+ args: [emailAuthMsg, templateIdx],
+ })
+
+ return {
+ to: address,
+ target: address,
+ value: 0n,
+ callData: data,
+ data,
+ }
+}
+
+export const getCompleteRecoveryAction = async ({
+ client,
+ account,
+ recoveryData,
+}: {
+ client: PublicClient
+ account: Address
+ recoveryData: Hex
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ const data = encodeFunctionData({
+ functionName: 'completeRecovery',
+ abi,
+ args: [account, recoveryData],
+ })
+
+ return {
+ to: address,
+ target: address,
+ value: 0n,
+ callData: data,
+ data,
+ }
+}
+
+export const getCancelRecoveryAction = async ({
+ client,
+}: {
+ client: PublicClient
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ const data = encodeFunctionData({
+ functionName: 'cancelRecovery',
+ abi,
+ })
+
+ return {
+ to: address,
+ target: address,
+ value: 0n,
+ callData: data,
+ data,
+ }
+}
+
+export const getCancelExpiredRecoveryAction = async ({
+ client,
+ account,
+}: {
+ client: PublicClient
+ account: Address
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ const data = encodeFunctionData({
+ functionName: 'cancelExpiredRecovery',
+ abi,
+ args: [account],
+ })
+
+ return {
+ to: address,
+ target: address,
+ value: 0n,
+ callData: data,
+ data,
+ }
+}
+
+export const computeEmailAuthAddress = async ({
+ client,
+ recoveredAccount,
+ accountSalt,
+}: {
+ client: PublicClient
+ recoveredAccount: Address
+ accountSalt: Hex
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ try {
+ return await client.readContract({
+ address,
+ abi,
+ functionName: 'computeEmailAuthAddress',
+ args: [recoveredAccount, accountSalt],
+ })
+ } catch (err) {
+ return zeroAddress
+ }
+}
+
+export const getGuardianConfig = async ({
+ account,
+ client,
+}: {
+ account: Account
+ client: PublicClient
+}): Promise<{
+ guardianCount: bigint
+ totalWeight: bigint
+ acceptedWeight: bigint
+ threshold: bigint
+}> => {
+ const address = getModuleAddress(await client.getChainId())
+ try {
+ return await client.readContract({
+ address,
+ abi,
+ functionName: 'getGuardianConfig',
+ args: [account.address],
+ })
+ } catch (err) {
+ return {
+ guardianCount: 0n,
+ totalWeight: 0n,
+ acceptedWeight: 0n,
+ threshold: 0n,
+ }
+ }
+}
+
+export const getGuardian = async ({
+ account,
+ client,
+ guardian,
+}: {
+ account: Account
+ client: PublicClient
+ guardian: Address
+}): Promise<{ status: number; weight: bigint }> => {
+ const address = getModuleAddress(await client.getChainId())
+ try {
+ return await client.readContract({
+ address,
+ abi,
+ functionName: 'getGuardian',
+ args: [account.address, guardian],
+ })
+ } catch (err) {
+ return { status: 0, weight: 0n }
+ }
+}
+
+export const getAllGuardians = async ({
+ account,
+ client,
+}: {
+ account: Account
+ client: PublicClient
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ try {
+ return await client.readContract({
+ address,
+ abi,
+ functionName: 'getAllGuardians',
+ args: [account.address],
+ })
+ } catch (err) {
+ return []
+ }
+}
+
+export const hasGuardianVoted = async ({
+ account,
+ client,
+ guardian,
+}: {
+ account: Account
+ client: PublicClient
+ guardian: Address
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ try {
+ return await client.readContract({
+ address,
+ abi,
+ functionName: 'hasGuardianVoted',
+ args: [account.address, guardian],
+ })
+ } catch (err) {
+ return false
+ }
+}
+
+export const getAddGuardianAction = async ({
+ client,
+ guardian,
+ weight,
+}: {
+ client: PublicClient
+ guardian: Address
+ weight: bigint
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ const data = encodeFunctionData({
+ functionName: 'addGuardian',
+ abi,
+ args: [guardian, weight],
+ })
+
+ return {
+ to: address,
+ target: address,
+ value: 0n,
+ callData: data,
+ data,
+ }
+}
+
+export const getRemoveGuardianAction = async ({
+ client,
+ guardian,
+}: {
+ client: PublicClient
+ guardian: Address
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ const data = encodeFunctionData({
+ functionName: 'removeGuardian',
+ abi,
+ args: [guardian],
+ })
+
+ return {
+ to: address,
+ target: address,
+ value: 0n,
+ callData: data,
+ data,
+ }
+}
+
+export const getChangeThresholdAction = async ({
+ client,
+ threshold,
+}: {
+ client: PublicClient
+ threshold: bigint
+}): Promise => {
+ const address = getModuleAddress(await client.getChainId())
+ const data = encodeFunctionData({
+ functionName: 'changeThreshold',
+ abi,
+ args: [threshold],
+ })
+
+ return {
+ to: address,
+ target: address,
+ value: 0n,
+ callData: data,
+ data,
+ }
+}
diff --git a/test/e2e/accounts/7579-ref-implementation.e2e-test.ts b/test/e2e/accounts/7579-ref-implementation.e2e-test.ts
index bd8b915..d3d7ecf 100644
--- a/test/e2e/accounts/7579-ref-implementation.e2e-test.ts
+++ b/test/e2e/accounts/7579-ref-implementation.e2e-test.ts
@@ -14,6 +14,7 @@ import {
testScheduledTransfersExecutor,
testHookMultiPlexer,
testSocialRecoveryValidator,
+ testUniversalEmailRecoveryExecutor,
testSmartSessionsValidator,
} from 'test/e2e/modules'
@@ -76,6 +77,12 @@ describe('Test erc7579 reference implementation', () => {
testClient,
})
+ testUniversalEmailRecoveryExecutor({
+ account,
+ publicClient,
+ testClient,
+ })
+
testRegistryHook({
account,
publicClient,
diff --git a/test/e2e/infra/installModuleActions.ts b/test/e2e/infra/installModuleActions.ts
index 86d357a..d197eeb 100644
--- a/test/e2e/infra/installModuleActions.ts
+++ b/test/e2e/infra/installModuleActions.ts
@@ -12,16 +12,17 @@ import {
Address,
encodeAbiParameters,
encodePacked,
+ getAddress,
Hex,
PublicClient,
toBytes,
+ toFunctionSelector,
toHex,
zeroAddress,
} from 'viem'
import { CallType } from 'src/module/types'
import { REGISTRY_ADDRESS } from 'src/module/registry'
import { SafeHookType } from 'src/account/safe/types'
-import { encodeValidationData } from 'src/module/ownable-validator/usage'
import { getSudoPolicy } from 'src/module/smart-sessions/policies/sudo-policy'
import { privateKeyToAccount } from 'viem/accounts'
import { getOwnableExecutor } from 'src/module/ownable-executor'
@@ -34,6 +35,7 @@ import {
import { getHookMultiPlexer } from 'src/module/hook-multi-plexer'
import { getDeadmanSwitch } from 'src/module/deadman-switch'
import { getMultiFactorValidator } from 'src/module/multi-factor-validator'
+import { getUniversalEmailRecoveryExecutor } from 'src/module/zk-email-recovery/universal-email-recovery'
import { sepolia } from 'viem/chains'
type Params = {
@@ -56,6 +58,7 @@ export const getInstallModuleActions = async ({ account, client }: Params) => {
scheduledTransfersExecutor,
hookMultiPlexer,
smartSessions,
+ universalEmailRecoveryExecutor,
} = getInstallModuleData({
account,
})
@@ -159,6 +162,15 @@ export const getInstallModuleActions = async ({ account, client }: Params) => {
module: getSmartSessionsValidator(smartSessions),
})
+ // install universal email recovery executor
+ const installUniversalEmailRecoveryAction = await installModule({
+ client,
+ account,
+ module: await getUniversalEmailRecoveryExecutor(
+ universalEmailRecoveryExecutor,
+ ),
+ })
+
return [
...installSmartSessionsValidatorAction,
...installOwnableValidatorAction,
@@ -173,6 +185,7 @@ export const getInstallModuleActions = async ({ account, client }: Params) => {
...installScheduledOrdersExecutorAction,
...installScheduledTransfersExecutorAction,
...installHookMultiplexerAction,
+ ...installUniversalEmailRecoveryAction,
]
}
@@ -314,4 +327,19 @@ export const getInstallModuleData = ({ account }: Pick) => ({
],
hook: zeroAddress,
},
+ universalEmailRecoveryExecutor: {
+ validator: OWNABLE_VALIDATOR_ADDRESS,
+ isInstalledContext: toHex(0),
+ initialSelector: toFunctionSelector('function addOwner(address)'),
+ guardians: [
+ getAddress('0x0Cb7EAb54EB751579a82D80Fe2683687deb918f3'),
+ getAddress('0x9FF36a253C70b65122B47c70F2AfaF65F2957118'),
+ ],
+ weights: [1n, 2n],
+ threshold: 2n,
+ delay: 60n * 60n * 6n, // 6 hours
+ expiry: 2n * 7n * 24n * 60n * 60n, // 2 days
+ chainId: sepolia.id,
+ hook: zeroAddress,
+ },
})
diff --git a/test/e2e/infra/unInstallModuleActions.ts b/test/e2e/infra/unInstallModuleActions.ts
index fd73653..98f100a 100644
--- a/test/e2e/infra/unInstallModuleActions.ts
+++ b/test/e2e/infra/unInstallModuleActions.ts
@@ -5,6 +5,7 @@ import {
SCHEDULED_ORDERS_EXECUTOR_ADDRESS,
SCHEDULED_TRANSFERS_EXECUTOR_ADDRESS,
SMART_SESSIONS_ADDRESS,
+ UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA,
} from 'src/module'
import { Account } from 'src/account'
import { Hex, PublicClient } from 'viem'
@@ -160,7 +161,18 @@ export const getUnInstallModuleActions = async ({
}),
})
+ // unInstall universal email recovery executor
+ const unInstallUniversalEmailRecoveryExecutorAction = await uninstallModule({
+ client,
+ account,
+ module: getModule({
+ type: 'executor',
+ module: UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA,
+ }),
+ })
+
return [
+ ...unInstallUniversalEmailRecoveryExecutorAction,
...unInstallHookMultiPlexerHookAction,
...unInstallScheduledTransfersExecutorAction,
...unInstallScheduledOrdersExecutorAction,
diff --git a/test/e2e/modules/index.ts b/test/e2e/modules/index.ts
index b1ffa71..ace7b68 100644
--- a/test/e2e/modules/index.ts
+++ b/test/e2e/modules/index.ts
@@ -10,4 +10,5 @@ export * from './scheduledOrdersExecutor'
export * from './scheduledTransfersExecutor'
export * from './hookMultiPlexer'
export * from './socialRecoveryValidator'
+export * from './universalEmailRecoveryExecutor'
export * from './smartSessionsValidator'
diff --git a/test/e2e/modules/universalEmailRecoveryExecutor.ts b/test/e2e/modules/universalEmailRecoveryExecutor.ts
new file mode 100644
index 0000000..b02c68e
--- /dev/null
+++ b/test/e2e/modules/universalEmailRecoveryExecutor.ts
@@ -0,0 +1,229 @@
+import { Account, isModuleInstalled } from 'src/account'
+import {
+ getAddGuardianAction,
+ getAllGuardians,
+ getAllowedSelectors,
+ getAllowedValidators,
+ getAllowValidatorRecoveryAction,
+ getChangeThresholdAction,
+ getDisallowValidatorRecoveryAction,
+ getGuardian,
+ getGuardianConfig,
+ getModule,
+ getRecoveryConfig,
+ getRemoveGuardianAction,
+ getUpdateRecoveryConfigAction,
+ MULTI_FACTOR_VALIDATOR_ADDRESS,
+ OWNABLE_VALIDATOR_ADDRESS,
+} from 'src/module'
+import {
+ getAddress,
+ PublicClient,
+ TestClient,
+ toFunctionSelector,
+ toHex,
+} from 'viem'
+import { UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA } from 'src/module/zk-email-recovery/universal-email-recovery/constants'
+import { sendUserOp } from '../infra'
+import { SENTINEL_ADDRESS } from 'src/common'
+
+type Params = {
+ account: Account
+ publicClient: PublicClient
+ testClient: TestClient
+}
+
+export const testUniversalEmailRecoveryExecutor = async ({
+ account,
+ publicClient,
+}: Params) => {
+ it('should return true when checking universal email recovery executor isInstalled', async () => {
+ const isUniversalEmailRecoveryInstalled = await isModuleInstalled({
+ account,
+ client: publicClient,
+ module: getModule({
+ type: 'executor',
+ module: UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA,
+ }),
+ })
+
+ expect(isUniversalEmailRecoveryInstalled).toBe(true)
+ }, 20000)
+
+ it('should add guardian', async () => {
+ const newGuardian = getAddress('0x206f270A1eBB6Dd3Bc97581376168014FD6eE57c')
+ const weight = 1n
+
+ const execution = await getAddGuardianAction({
+ client: publicClient,
+ guardian: newGuardian,
+ weight,
+ })
+
+ await sendUserOp({
+ account,
+ actions: [execution],
+ })
+
+ const guardians = await getAllGuardians({
+ account,
+ client: publicClient,
+ })
+ const guardian = await getGuardian({
+ account,
+ client: publicClient,
+ guardian: newGuardian,
+ })
+
+ expect(guardians.length).toBe(3)
+ expect(guardians.includes(newGuardian)).toBe(true)
+ expect(guardian.weight).toBe(weight)
+ expect(guardian.status).toBe(1)
+ }, 20000)
+
+ it('should remove guardian', async () => {
+ const guardianToRemove = getAddress(
+ '0x206f270A1eBB6Dd3Bc97581376168014FD6eE57c',
+ )
+
+ const execution = await getRemoveGuardianAction({
+ client: publicClient,
+ guardian: guardianToRemove,
+ })
+
+ await sendUserOp({
+ account,
+ actions: [execution],
+ })
+
+ const guardians = await getAllGuardians({
+ account,
+ client: publicClient,
+ })
+ const guardian = await getGuardian({
+ account,
+ client: publicClient,
+ guardian: guardianToRemove,
+ })
+
+ expect(guardians.length).toBe(2)
+ expect(guardians.includes(guardianToRemove)).toBe(false)
+ expect(guardian.weight).toBe(0n)
+ expect(guardian.status).toBe(0)
+ }, 20000)
+
+ it('should change threshold', async () => {
+ const newThreshold = 1n
+
+ const execution = await getChangeThresholdAction({
+ client: publicClient,
+ threshold: newThreshold,
+ })
+
+ await sendUserOp({
+ account,
+ actions: [execution],
+ })
+
+ const config = await getGuardianConfig({
+ account,
+ client: publicClient,
+ })
+
+ expect(config.threshold).toBe(newThreshold)
+ }, 20000)
+
+ it('should update recovery config', async () => {
+ const delay = 86400n // 1 day
+ const expiry = 604800n // 1 week
+
+ const execution = await getUpdateRecoveryConfigAction({
+ client: publicClient,
+ delay,
+ expiry,
+ })
+
+ await sendUserOp({
+ account,
+ actions: [execution],
+ })
+
+ const config = await getRecoveryConfig({
+ account,
+ client: publicClient,
+ })
+
+ expect(config.delay).toBe(delay)
+ expect(config.expiry).toBe(expiry)
+ }, 20000)
+
+ it('should allow validator recovery', async () => {
+ const validator = MULTI_FACTOR_VALIDATOR_ADDRESS
+ const isInstalledContext = toHex(0)
+ const recoverySelector = toFunctionSelector(
+ 'function setValidator(address,ValidatorId,bytes)',
+ )
+
+ const execution = await getAllowValidatorRecoveryAction({
+ client: publicClient,
+ validator,
+ isInstalledContext,
+ recoverySelector,
+ })
+
+ await sendUserOp({
+ account,
+ actions: [execution],
+ })
+
+ const validators = await getAllowedValidators({
+ account,
+ client: publicClient,
+ })
+ const selectors = await getAllowedSelectors({
+ account,
+ client: publicClient,
+ })
+
+ expect(validators.length).toBe(2)
+ expect(selectors.length).toBe(2)
+ expect(validators[0]).toBe(validator)
+ expect(selectors[0]).toBe(recoverySelector)
+ }, 20000)
+
+ it('should disallow validator recovery', async () => {
+ const validator = MULTI_FACTOR_VALIDATOR_ADDRESS
+ const prevValidator = SENTINEL_ADDRESS
+ const recoverySelector = toFunctionSelector(
+ 'function setValidator(address,ValidatorId,bytes)',
+ )
+
+ const execution = await getDisallowValidatorRecoveryAction({
+ client: publicClient,
+ validator,
+ prevValidator,
+ recoverySelector,
+ })
+
+ await sendUserOp({
+ account,
+ actions: [execution],
+ })
+
+ const validators = await getAllowedValidators({
+ account,
+ client: publicClient,
+ })
+ const selectors = await getAllowedSelectors({
+ account,
+ client: publicClient,
+ })
+
+ expect(validators.length).toBe(1)
+ expect(selectors.length).toBe(1)
+ expect(validators[0]).toBe(OWNABLE_VALIDATOR_ADDRESS)
+ expect(selectors[0]).toBe(
+ toFunctionSelector('function addOwner(address owner)'),
+ )
+ }, 20000)
+}
diff --git a/test/unit/module/zkEmailRecovery/universalEmailRecovery/universalEmailRecovery.test.ts b/test/unit/module/zkEmailRecovery/universalEmailRecovery/universalEmailRecovery.test.ts
new file mode 100644
index 0000000..fba0678
--- /dev/null
+++ b/test/unit/module/zkEmailRecovery/universalEmailRecovery/universalEmailRecovery.test.ts
@@ -0,0 +1,484 @@
+import { getAddress, toFunctionSelector, toHex, zeroAddress } from 'viem'
+import { sepolia } from 'viem/chains'
+import { getUniversalEmailRecoveryExecutor } from 'src/module/zk-email-recovery/universal-email-recovery/installation'
+import { UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA } from 'src/module/zk-email-recovery/universal-email-recovery/constants'
+import {
+ acceptanceCommandTemplates,
+ canStartRecoveryRequest,
+ computeAcceptanceTemplateId,
+ computeEmailAuthAddress,
+ computeRecoveryTemplateId,
+ EmailAuthMsg,
+ extractRecoveredAccountFromAcceptanceCommand,
+ extractRecoveredAccountFromRecoveryCommand,
+ getAddGuardianAction,
+ getAllGuardians,
+ getAllowedSelectors,
+ getAllowedValidators,
+ getAllowValidatorRecoveryAction,
+ getCancelExpiredRecoveryAction,
+ getCancelRecoveryAction,
+ getChangeThresholdAction,
+ getCompleteRecoveryAction,
+ getDisallowValidatorRecoveryAction,
+ getDkim,
+ getEmailAuthImplementation,
+ getGuardian,
+ getGuardianConfig,
+ getHandleAcceptanceAction,
+ getHandleRecoveryAction,
+ getPreviousRecoveryRequest,
+ getRecoveryConfig,
+ getRecoveryRequest,
+ getRemoveGuardianAction,
+ getUpdateRecoveryConfigAction,
+ getVerifier,
+ hasGuardianVoted,
+ isActivated,
+ recoveryCommandTemplates,
+} from 'src/module/zk-email-recovery/universal-email-recovery/usage'
+import { getClient } from 'src/common/getClient'
+import { MockClient } from '../../../../utils/mocks/client'
+import { getAccount } from 'src/account'
+import { MockAccountDeployed } from '../../../../utils/mocks/account'
+import { OWNABLE_VALIDATOR_ADDRESS } from 'src/module'
+
+describe('Universal Email Recovery Module', () => {
+ // Setup
+ const client = getClient(MockClient)
+ const account = getAccount(MockAccountDeployed)
+
+ const validator = OWNABLE_VALIDATOR_ADDRESS
+ const isInstalledContext = toHex(0)
+ const initialSelector = toFunctionSelector('function addOwner(address)')
+ const guardians = [
+ getAddress('0x0Cb7EAb54EB751579a82D80Fe2683687deb918f3'),
+ getAddress('0x9FF36a253C70b65122B47c70F2AfaF65F2957118'),
+ ]
+ const weights = [1n, 2n]
+ const threshold = 2n
+ const delay = 0n // 0 seconds
+ const expiry = 2n * 7n * 24n * 60n * 60n // 2 weeks in seconds
+
+ it('should get install universal email recovery module', async () => {
+ const installUniversalEmailModule = getUniversalEmailRecoveryExecutor({
+ validator,
+ isInstalledContext,
+ initialSelector,
+ guardians,
+ weights,
+ threshold,
+ delay,
+ expiry,
+ chainId: sepolia.id,
+ })
+
+ expect(installUniversalEmailModule.address).toEqual(
+ UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA,
+ )
+ expect(installUniversalEmailModule.module).toEqual(
+ UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA,
+ )
+ expect(installUniversalEmailModule.initData).toBeDefined()
+ expect(installUniversalEmailModule.deInitData).toEqual('0x')
+ expect(installUniversalEmailModule.additionalContext).toEqual('0x')
+ expect(installUniversalEmailModule.type).toEqual('executor')
+ expect(installUniversalEmailModule.hook).toBeUndefined()
+ })
+
+ it('Should get recovery config', async () => {
+ const config = await getRecoveryConfig({
+ account,
+ client,
+ })
+ expect(config.delay).toEqual(0n)
+ expect(config.expiry).toEqual(0n)
+ })
+
+ it('Should get recovery request', async () => {
+ const request = await getRecoveryRequest({
+ account,
+ client,
+ })
+ expect(request[0]).toEqual(0n)
+ expect(request[1]).toEqual(0n)
+ expect(request[2]).toEqual(0n)
+ expect(request[3]).toEqual(
+ '0x0000000000000000000000000000000000000000000000000000000000000000',
+ )
+ })
+
+ it('Should get previous recovery request', async () => {
+ const prevRequest = await getPreviousRecoveryRequest({
+ account,
+ client,
+ })
+ expect(prevRequest.previousGuardianInitiated).toEqual(zeroAddress)
+ expect(prevRequest.cancelRecoveryCooldown).toEqual(0n)
+ })
+
+ it('Should check if activated', async () => {
+ const activated = await isActivated({
+ account,
+ client,
+ })
+ expect(activated).toEqual(false)
+ })
+
+ it('Should check if can start recovery request', async () => {
+ const canStart = await canStartRecoveryRequest({
+ account,
+ client,
+ validator,
+ })
+ expect(canStart).toEqual(false)
+ })
+
+ it('Should get allow validator recovery action', async () => {
+ const action = await getAllowValidatorRecoveryAction({
+ client,
+ validator,
+ isInstalledContext,
+ recoverySelector: initialSelector,
+ })
+ expect(action.to).toEqual(UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA)
+ expect(action.target).toEqual(UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA)
+ expect(action.value).toEqual(0n)
+ expect(action.callData).toBeDefined()
+ expect(action.data).toBeDefined()
+ })
+
+ it('Should get disallow validator recovery action', async () => {
+ const prevValidator = '0xD990393C670dCcE8b4d8F858FB98c9912dBFAa06'
+ const action = await getDisallowValidatorRecoveryAction({
+ client,
+ validator,
+ prevValidator,
+ recoverySelector: initialSelector,
+ })
+ expect(action.to).toEqual(UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA)
+ expect(action.target).toEqual(UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA)
+ expect(action.value).toEqual(0n)
+ expect(action.callData).toBeDefined()
+ expect(action.data).toBeDefined()
+ })
+
+ it('Should get allowed validators', async () => {
+ const validators = await getAllowedValidators({
+ account,
+ client,
+ })
+ expect(validators).toEqual([])
+ })
+
+ it('Should get allowed selectors', async () => {
+ const selectors = await getAllowedSelectors({
+ account,
+ client,
+ })
+ expect(selectors).toEqual([])
+ })
+
+ it('Should get acceptance command templates', async () => {
+ const templates = await acceptanceCommandTemplates({
+ client,
+ })
+ expect(templates).toEqual([
+ ['Accept', 'guardian', 'request', 'for', '{ethAddr}'],
+ ])
+ })
+
+ it('Should get recovery command templates', async () => {
+ const templates = await recoveryCommandTemplates({
+ client,
+ })
+ expect(templates).toEqual([
+ [
+ 'Recover',
+ 'account',
+ '{ethAddr}',
+ 'using',
+ 'recovery',
+ 'hash',
+ '{string}',
+ ],
+ ])
+ })
+
+ it('Should extract recovered account from acceptance command', async () => {
+ const recoveredAccount = await extractRecoveredAccountFromAcceptanceCommand(
+ {
+ client,
+ commandParams: [account.address],
+ templateIdx: 0n,
+ },
+ )
+ expect(recoveredAccount).toEqual(zeroAddress)
+ })
+
+ it('Should extract recovered account from recovery command', async () => {
+ const recoveredAccount = await extractRecoveredAccountFromRecoveryCommand({
+ client,
+ commandParams: [account.address],
+ templateIdx: 0n,
+ })
+ expect(recoveredAccount).toEqual(zeroAddress)
+ })
+
+ it('Should compute acceptance template id', async () => {
+ const expectedId =
+ 78246708611299769969691317804450782728492512111741514614578044794817483451845n
+ const id = await computeAcceptanceTemplateId({
+ client,
+ templateIdx: 0n,
+ })
+ expect(id).toEqual(expectedId)
+ })
+
+ it('Should compute recovery template id', async () => {
+ const expectedId =
+ 41597252099594059824363833791590872545117890762070757419930713588231239964259n
+ const id = await computeRecoveryTemplateId({
+ client,
+ templateIdx: 0n,
+ })
+ expect(id).toEqual(expectedId)
+ })
+
+ it('Should get verifier', async () => {
+ const expectedVerifier = getAddress(
+ '0x0D5C8bcae3A3589F2CFbb04895933717aA5098e1',
+ )
+ const verifier = await getVerifier({
+ client,
+ })
+ expect(verifier).toEqual(expectedVerifier)
+ })
+
+ it('Should get DKIM', async () => {
+ const expectedDkim = getAddress(
+ '0x1D2B1F8cF98382e53C7735F05ef84d51FEd8Eff6',
+ )
+ const dkim = await getDkim({
+ client,
+ })
+ expect(dkim).toEqual(expectedDkim)
+ })
+
+ it('Should get email auth implementation', async () => {
+ const expectedEmailAuth = getAddress(
+ '0xCa4d16459b7AC7b348016244f1fA49d3f87b6F3F',
+ )
+ const emailAuthImplementation = await getEmailAuthImplementation({
+ client,
+ })
+ expect(emailAuthImplementation).toEqual(expectedEmailAuth)
+ })
+
+ it('Should get update recovery config action', async () => {
+ const action = await getUpdateRecoveryConfigAction({
+ client,
+ delay,
+ expiry,
+ })
+ expect(action.to).toEqual(UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA)
+ expect(action.target).toEqual(UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA)
+ expect(action.value).toEqual(0n)
+ expect(action.callData).toBeDefined()
+ expect(action.data).toBeDefined()
+ })
+
+ it('Should get handle acceptance action', async () => {
+ const emailAuthMsg: EmailAuthMsg = {
+ templateId: 1n,
+ commandParams: [
+ '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
+ '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef',
+ ],
+ skippedCommandPrefix: 0n,
+ proof: {
+ domainName: 'gmail.com',
+ publicKeyHash:
+ '0x9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba',
+ timestamp: 1709251200n, // 1st March 2024
+ maskedCommand: 'recover account',
+ emailNullifier:
+ '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
+ accountSalt:
+ '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
+ isCodeExist: true,
+ proof:
+ '0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc',
+ },
+ }
+
+ const action = await getHandleAcceptanceAction({
+ client,
+ emailAuthMsg,
+ templateIdx: 0n,
+ })
+ expect(action.to).toEqual(UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA)
+ expect(action.target).toEqual(UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA)
+ expect(action.value).toEqual(0n)
+ expect(action.callData).toBeDefined()
+ expect(action.data).toBeDefined()
+ })
+
+ it('Should get handle recovery action', async () => {
+ const emailAuthMsg: EmailAuthMsg = {
+ templateId: 1n,
+ commandParams: [
+ '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
+ '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef',
+ ],
+ skippedCommandPrefix: 0n,
+ proof: {
+ domainName: 'gmail.com',
+ publicKeyHash:
+ '0x9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba',
+ timestamp: 1709251200n, // 1st March 2024
+ maskedCommand: 'recover account',
+ emailNullifier:
+ '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
+ accountSalt:
+ '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
+ isCodeExist: true,
+ proof:
+ '0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc',
+ },
+ }
+
+ const action = await getHandleRecoveryAction({
+ client,
+ emailAuthMsg,
+ templateIdx: 0n,
+ })
+ expect(action.to).toEqual(UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA)
+ expect(action.target).toEqual(UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA)
+ expect(action.value).toEqual(0n)
+ expect(action.callData).toBeDefined()
+ expect(action.data).toBeDefined()
+ })
+
+ it('Should get complete recovery action', async () => {
+ const action = await getCompleteRecoveryAction({
+ client,
+ account: account.address,
+ recoveryData: '0x',
+ })
+ expect(action.to).toEqual(UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA)
+ expect(action.target).toEqual(UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA)
+ expect(action.value).toEqual(0n)
+ expect(action.callData).toBeDefined()
+ expect(action.data).toBeDefined()
+ })
+
+ it('Should get cancel recovery action', async () => {
+ const action = await getCancelRecoveryAction({ client })
+ expect(action.to).toEqual(UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA)
+ expect(action.target).toEqual(UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA)
+ expect(action.value).toEqual(0n)
+ expect(action.callData).toBeDefined()
+ expect(action.data).toBeDefined()
+ })
+
+ it('Should get cancel expired recovery action', async () => {
+ const action = await getCancelExpiredRecoveryAction({
+ client,
+ account: account.address,
+ })
+ expect(action.to).toEqual(UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA)
+ expect(action.target).toEqual(UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA)
+ expect(action.value).toEqual(0n)
+ expect(action.callData).toBeDefined()
+ expect(action.data).toBeDefined()
+ })
+
+ it('Should compute email auth address', async () => {
+ const expectedEmailAuthAddress = getAddress(
+ '0x81e375B511FA247B22D09519f37ddaa661cbd59a',
+ )
+ const emailAuthAddress = await computeEmailAuthAddress({
+ client,
+ recoveredAccount: account.address,
+ accountSalt:
+ '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
+ })
+ expect(emailAuthAddress).toEqual(expectedEmailAuthAddress)
+ })
+
+ it('Should get guardian config', async () => {
+ const config = await getGuardianConfig({
+ account,
+ client,
+ })
+ expect(config.guardianCount).toEqual(0n)
+ expect(config.totalWeight).toEqual(0n)
+ expect(config.acceptedWeight).toEqual(0n)
+ expect(config.threshold).toEqual(0n)
+ })
+
+ it('Should get guardian', async () => {
+ const guardian = await getGuardian({
+ account,
+ client,
+ guardian: guardians[0],
+ })
+ expect(guardian.status).toEqual(0)
+ expect(guardian.weight).toEqual(0n)
+ })
+
+ it('Should get all guardians', async () => {
+ const allGuardians = await getAllGuardians({
+ account,
+ client,
+ })
+ expect(allGuardians).toEqual([])
+ })
+
+ it('Should check if guardian has voted', async () => {
+ const hasVoted = await hasGuardianVoted({
+ account,
+ client,
+ guardian: guardians[0],
+ })
+ expect(hasVoted).toEqual(false)
+ })
+
+ it('Should get add guardian action', async () => {
+ const action = await getAddGuardianAction({
+ client,
+ guardian: guardians[0],
+ weight: weights[0],
+ })
+ expect(action.to).toEqual(UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA)
+ expect(action.target).toEqual(UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA)
+ expect(action.value).toEqual(0n)
+ expect(action.callData).toBeDefined()
+ expect(action.data).toBeDefined()
+ })
+
+ it('Should get remove guardian action', async () => {
+ const action = await getRemoveGuardianAction({
+ client,
+ guardian: guardians[0],
+ })
+ expect(action.to).toEqual(UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA)
+ expect(action.target).toEqual(UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA)
+ expect(action.value).toEqual(0n)
+ expect(action.callData).toBeDefined()
+ expect(action.data).toBeDefined()
+ })
+
+ it('Should get change threshold action', async () => {
+ const action = await getChangeThresholdAction({
+ client,
+ threshold,
+ })
+ expect(action.to).toEqual(UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA)
+ expect(action.target).toEqual(UNIVERSAL_EMAIL_RECOVERY_ADDRESS__ETH_SEPOLIA)
+ expect(action.value).toEqual(0n)
+ expect(action.callData).toBeDefined()
+ expect(action.data).toBeDefined()
+ })
+})