UNPKG

@lit-protocol/vincent-scaffold-sdk

Version:

Shared build configuration and utilities for Vincent abilities and policies

752 lines (751 loc) 33.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.StateManager = void 0; const chalk_1 = __importDefault(require("chalk")); const fs_1 = require("fs"); const path_1 = __importDefault(require("path")); const crypto_1 = require("crypto"); const ethers_1 = require("ethers"); const STATE_FILE_PATH = path_1.default.join(process.cwd(), ".e2e-state.json"); const STATE_VERSION = "2.0.0"; class StateManager { constructor(network, testFileName, configHash, abilityIpfsCids, abilityPolicyCids) { this.network = network; this.testFileName = testFileName; this.configHash = configHash; this.nestedState = { version: STATE_VERSION, testFiles: {}, }; this.currentConfigState = null; // If ability/policy CIDs are provided, generate the real config hash if (abilityIpfsCids && abilityPolicyCids) { this.configHash = StateManager.generateConfigHash(abilityIpfsCids, abilityPolicyCids); } // Initialize the current configuration state this.currentConfigState = this.getCurrentConfigState(); console.log(`🏗️ StateManager initialized with nested structure v${STATE_VERSION} for ${testFileName}:${this.configHash}`); } /** * Generate configuration hash from ability and policy IPFS CIDs */ static generateConfigHash(abilityIpfsCids, abilityPolicyCids) { // Combine all IPFS CIDs and create a hash const flattenedPolicyCids = abilityPolicyCids.flat(); const allCids = [...abilityIpfsCids, ...flattenedPolicyCids]; const sortedCids = allCids.sort(); const cidString = sortedCids.join(""); const hash = (0, crypto_1.createHash)("sha256") .update(cidString) .digest("hex") .substring(0, 8); return hash; } /** * Auto-detect test filename from call stack */ static autoDetectTestFileName() { const stack = new Error().stack; if (!stack) return "unknown-test.ts"; const stackLines = stack.split("\n"); for (const line of stackLines) { const match = line.match(/\/([^\/\s]+\.ts):/); if (match && match[1] !== "state-manager.ts" && match[1] !== "init.ts") { return match[1]; } } return "unknown-test.ts"; } /** * Get current configuration state, creating if needed */ getCurrentConfigState() { if (!this.nestedState.testFiles[this.testFileName]) { this.nestedState.testFiles[this.testFileName] = { accounts: {}, pkps: {}, capacityCredits: {}, configurations: {}, appVersions: {}, }; } // Ensure all structures exist for this test file if (!this.nestedState.testFiles[this.testFileName].accounts) { this.nestedState.testFiles[this.testFileName].accounts = {}; } if (!this.nestedState.testFiles[this.testFileName].pkps) { this.nestedState.testFiles[this.testFileName].pkps = {}; } if (!this.nestedState.testFiles[this.testFileName].capacityCredits) { this.nestedState.testFiles[this.testFileName].capacityCredits = {}; } if (!this.nestedState.testFiles[this.testFileName].configurations[this.configHash]) { this.nestedState.testFiles[this.testFileName].configurations[this.configHash] = { configHash: this.configHash, abilityIpfsCids: [], abilityPolicyCids: [], lastUsed: new Date().toISOString(), network: this.network, state: {}, }; } return this.nestedState.testFiles[this.testFileName].configurations[this.configHash]; } /** * Set the real configuration hash based on ability/policy CIDs * This should be called before any state operations when CIDs are known */ setConfigurationFromCIDs(abilityIpfsCids, abilityPolicyCids) { const newConfigHash = StateManager.generateConfigHash(abilityIpfsCids, abilityPolicyCids); if (newConfigHash !== this.configHash) { console.log(`🔄 Setting configuration from ${this.configHash} to ${newConfigHash}`); // Update the internal config hash this.configHash = newConfigHash; // Reset current config state since we're switching to a new config this.currentConfigState = null; // Initialize the new configuration state this.currentConfigState = this.getCurrentConfigState(); // Update metadata this.updateConfigurationMetadata(abilityIpfsCids, abilityPolicyCids); } else { // Update metadata even if hash is same (for lastUsed timestamp) this.updateConfigurationMetadata(abilityIpfsCids, abilityPolicyCids); } } /** * Update configuration metadata */ updateConfigurationMetadata(abilityIpfsCids, abilityPolicyCids) { const configState = this.getCurrentConfigState(); configState.abilityIpfsCids = abilityIpfsCids; configState.abilityPolicyCids = abilityPolicyCids; configState.lastUsed = new Date().toISOString(); } /** * Generate app version key from appId and appVersion */ static generateAppVersionKey(appId, appVersion) { return `${appId}-${appVersion}`; } // /** // * Get app version configuration, creating if needed // */ // private getAppVersionConfiguration( // appId: string, // appVersion: string // ): AppVersionConfiguration { // const appVersionKey = StateManager.generateAppVersionKey(appId, appVersion); // if (!this.nestedState.testFiles[this.testFileName]) { // this.nestedState.testFiles[this.testFileName] = { // configurations: {}, // appVersions: {}, // }; // } // if ( // !this.nestedState.testFiles[this.testFileName].appVersions[appVersionKey] // ) { // throw new Error( // `App version configuration ${appVersionKey} not found. It should be created when saving the app.` // ); // } // return this.nestedState.testFiles[this.testFileName].appVersions[ // appVersionKey // ]; // } /** * Save app version configuration */ saveAppVersionConfiguration(appId, appVersion, vincentApp, abilityIpfsCids, abilityPolicyCids) { const appVersionKey = StateManager.generateAppVersionKey(appId, appVersion); if (!this.nestedState.testFiles[this.testFileName]) { this.nestedState.testFiles[this.testFileName] = { configurations: {}, appVersions: {}, }; } this.nestedState.testFiles[this.testFileName].appVersions[appVersionKey] = { appId, appVersion, abilityIpfsCids, abilityPolicyCids, lastUsed: new Date().toISOString(), network: this.network, state: { vincentApp, pkpAppPermissions: [], }, }; console.log(chalk_1.default.green(`💾 Saved app version configuration: ${appVersionKey}`)); } /** * Get or register Vincent app using app versioning approach */ async getOrRegisterVincentAppVersioned(delegateeAddress, registerAppFunction, registerNextVersionFunction, abilityIpfsCids, abilityPolicies, policyParams) { // First, check if we have any existing app for this delegatee const existingApp = this.getExistingVincentApp(delegateeAddress); if (existingApp) { const appId = existingApp.appId; // Check if current abilities/policies match any existing app version const matchingAppVersion = this.findMatchingAppVersion(appId, abilityIpfsCids, abilityPolicies); if (matchingAppVersion) { console.log(chalk_1.default.blue(`🏢 Using existing app version: ${appId} v${matchingAppVersion}`)); return { appId, appVersion: matchingAppVersion, isNew: false, isNewVersion: false, abilityIpfsCids, abilityPolicies, policyParams, }; } else { // Register new version for existing app console.log(chalk_1.default.yellow(`🏢 Registering new version for existing app: ${appId}`)); const { appVersion } = await registerNextVersionFunction(appId); // Save the new app version const vincentApp = { appId, appVersion, delegateeAddress, abilityIpfsCids, abilityPolicies, policyParams, createdAt: new Date().toISOString(), network: this.network, }; this.saveAppVersionConfiguration(appId, appVersion, vincentApp, abilityIpfsCids, abilityPolicies); return { appId, appVersion, isNew: false, isNewVersion: true, abilityIpfsCids, abilityPolicies, policyParams, }; } } else { // Register completely new app console.log(chalk_1.default.yellow(`🏢 Registering new Vincent app...`)); const { appId, appVersion } = await registerAppFunction(); // Save the new app version const vincentApp = { appId, appVersion, delegateeAddress, abilityIpfsCids, abilityPolicies, policyParams, createdAt: new Date().toISOString(), network: this.network, }; this.saveAppVersionConfiguration(appId, appVersion, vincentApp, abilityIpfsCids, abilityPolicies); // Also save to current config for backward compatibility this.saveVincentApp(appId, appVersion, delegateeAddress, abilityIpfsCids, abilityPolicies, policyParams); return { appId, appVersion, isNew: true, isNewVersion: false, abilityIpfsCids, abilityPolicies, policyParams, }; } } /** * Find matching app version based on ability/policy CIDs */ findMatchingAppVersion(appId, abilityIpfsCids, abilityPolicies) { if (!this.nestedState.testFiles[this.testFileName]?.appVersions) { return undefined; } const appVersions = this.nestedState.testFiles[this.testFileName].appVersions; for (const [appVersionKey, config] of Object.entries(appVersions)) { if (config.appId === appId && config.network === this.network) { // Check if ability CIDs match const abilityCidsMatch = JSON.stringify(config.abilityIpfsCids.sort()) === JSON.stringify(abilityIpfsCids.sort()); const policyCidsMatch = JSON.stringify(config.abilityPolicyCids) === JSON.stringify(abilityPolicies); if (abilityCidsMatch && policyCidsMatch) { console.log(chalk_1.default.blue(`🔍 Found matching app version: ${appVersionKey}`)); return config.appVersion; } } } console.log(chalk_1.default.yellow(`🔍 No matching app version found for appId ${appId} with current abilities/policies`)); return undefined; } async loadState() { try { const data = await fs_1.promises.readFile(STATE_FILE_PATH, "utf-8"); const loadedState = JSON.parse(data); // Validate version compatibility if (loadedState.version !== STATE_VERSION) { console.warn(chalk_1.default.yellow(`⚠️ State file version mismatch. Expected ${STATE_VERSION}, got ${loadedState.version}. Creating new state.`)); return; } this.nestedState = loadedState; this.currentConfigState = this.getCurrentConfigState(); console.log(chalk_1.default.blue(`📁 Loaded existing state for ${this.testFileName}:${this.configHash}`)); } catch (error) { // File doesn't exist or is invalid - that's ok, we'll create it console.log(chalk_1.default.blue("📝 No existing state file found. Will create one.")); this.currentConfigState = this.getCurrentConfigState(); } } async saveState() { try { // Update last used timestamp if (this.currentConfigState) { this.currentConfigState.lastUsed = new Date().toISOString(); } await fs_1.promises.writeFile(STATE_FILE_PATH, JSON.stringify(this.nestedState, null, 2), "utf-8"); console.log(chalk_1.default.gray(`💾 Saved state for ${this.testFileName}:${this.configHash}`)); } catch (error) { console.error(chalk_1.default.red("❌ Failed to save state:"), error); throw error; } } generateAccount() { const wallet = ethers_1.ethers.Wallet.createRandom(); return { privateKey: wallet.privateKey, address: wallet.address, createdAt: new Date().toISOString(), network: this.network, }; } getOrGenerateAccount(accountType, existingPrivateKey) { // If private key provided from env, use it if (existingPrivateKey) { const wallet = new ethers_1.ethers.Wallet(existingPrivateKey); return { privateKey: existingPrivateKey, address: wallet.address, isNew: false, }; } // Ensure current config state is initialized (which initializes accounts structure) this.getCurrentConfigState(); // Get accounts for this test file and network const testFileAccounts = this.nestedState.testFiles[this.testFileName].accounts; // Ensure network accounts exist for this test file if (!testFileAccounts[this.network]) { testFileAccounts[this.network] = {}; } // Check if we have a saved account for this type in this test file const savedAccount = testFileAccounts[this.network][accountType]; if (savedAccount && savedAccount.network === this.network) { console.log(chalk_1.default.blue(`🔑 Using ${accountType} from ${this.testFileName}: ${savedAccount.address}`)); return { privateKey: savedAccount.privateKey, address: savedAccount.address, isNew: false, }; } // Check for legacy shared account and migrate if exists const legacyAccount = this.nestedState.sharedAccounts?.[this.network]?.[accountType]; if (legacyAccount && legacyAccount.network === this.network) { console.log(chalk_1.default.blue(`🔄 Migrating shared ${accountType} to ${this.testFileName}: ${legacyAccount.address}`)); testFileAccounts[this.network][accountType] = legacyAccount; return { privateKey: legacyAccount.privateKey, address: legacyAccount.address, isNew: false, }; } // Generate new account for this test file console.log(chalk_1.default.yellow(`🔑 Generating new ${accountType} account for ${this.testFileName}`)); const newAccount = this.generateAccount(); testFileAccounts[this.network][accountType] = newAccount; return { privateKey: newAccount.privateKey, address: newAccount.address, isNew: true, }; } // getGeneratedAccounts(): string[] { // // Get accounts for this test file // const testFileAccounts = // this.nestedState.testFiles[this.testFileName]?.accounts?.[this.network] || // {}; // // Also check legacy shared accounts for backward compatibility // const legacyAccounts = // this.nestedState.sharedAccounts?.[this.network] || {}; // // Combine both sources // const allAccounts = { ...legacyAccounts, ...testFileAccounts }; // return Object.entries(allAccounts) // .filter(([_, account]) => account !== undefined) // .map(([type, account]) => `${type}: ${account!.address}`); // } /** * Get existing PKP or return undefined if needs minting */ getExistingPKP() { // Ensure current config state is initialized this.getCurrentConfigState(); // Get PKP for this test file and network const testFilePkps = this.nestedState.testFiles[this.testFileName].pkps; const existingPkp = testFilePkps[this.network]; if (existingPkp && existingPkp.network === this.network) { console.log(chalk_1.default.blue(`🔑 Using PKP from ${this.testFileName}: ${existingPkp.ethAddress}`)); return existingPkp; } // Check for legacy shared PKP and migrate if exists const legacyPkp = this.nestedState.sharedPkps?.[this.network]; if (legacyPkp && legacyPkp.network === this.network) { console.log(chalk_1.default.blue(`🔄 Migrating shared PKP to ${this.testFileName}: ${legacyPkp.ethAddress}`)); testFilePkps[this.network] = legacyPkp; return legacyPkp; } return undefined; } /** * Save a newly minted PKP to state */ savePKP(pkpInfo) { // Ensure current config state is initialized this.getCurrentConfigState(); // Save PKP for this test file and network const testFilePkps = this.nestedState.testFiles[this.testFileName].pkps; testFilePkps[this.network] = { ...pkpInfo, createdAt: new Date().toISOString(), network: this.network, }; console.log(chalk_1.default.gray(`💾 Saved PKP for ${this.testFileName}: ${pkpInfo.ethAddress}`)); } /** * Get or mint the single testing PKP */ async getOrMintPKP(mintFunction) { // Check if we have an existing PKP const existingPKP = this.getExistingPKP(); if (existingPKP) { return { pkp: { tokenId: existingPKP.tokenId, publicKey: existingPKP.publicKey, ethAddress: existingPKP.ethAddress, }, isNew: false, }; } // Mint new PKP console.log(chalk_1.default.yellow(`🔨 Minting new testing PKP...`)); const newPKP = await mintFunction(); // Save to state this.savePKP(newPKP); return { pkp: newPKP, isNew: true, }; } /** * Check if capacity credits are still valid (not expired within 1 day) */ isCapacityCreditValid(capacityCredit) { const expirationDate = new Date(capacityCredit.expiresAt); const now = new Date(); const oneDayFromNow = new Date(now.getTime() + 24 * 60 * 60 * 1000); // Consider expired if it expires within the next 24 hours const isValid = expirationDate > oneDayFromNow; // Add helpful logging when expired/expiring if (!isValid) { console.log(chalk_1.default.yellow(` ↳ Capacity credits expire at: ${expirationDate.toISOString()}`)); console.log(chalk_1.default.yellow(` ↳ Current time: ${now.toISOString()}`)); } return isValid; } /** * Get existing capacity credits or return undefined if needs minting */ getExistingCapacityCredits() { // Ensure current config state is initialized this.getCurrentConfigState(); // Get capacity credits for this test file and network const testFileCC = this.nestedState.testFiles[this.testFileName].capacityCredits; const existingCC = testFileCC[this.network]; if (existingCC && existingCC.network === this.network) { if (this.isCapacityCreditValid(existingCC)) { console.log(chalk_1.default.blue(`🎫 Using capacity credits from ${this.testFileName}: ${existingCC.capacityTokenIdStr}`)); console.log(chalk_1.default.blue(` ↳ Expires: ${existingCC.expiresAt}`)); return existingCC; } else { console.log(chalk_1.default.yellow(`⚠️ Capacity credits found but expired/expiring soon for ${this.testFileName}`)); console.log(chalk_1.default.yellow(` ↳ Token ID: ${existingCC.capacityTokenIdStr}`)); } } // Check for legacy shared capacity credits and migrate if exists const legacyCC = this.nestedState.sharedCapacityCredits?.[this.network]; if (legacyCC && legacyCC.network === this.network) { if (this.isCapacityCreditValid(legacyCC)) { console.log(chalk_1.default.blue(`🔄 Migrating shared capacity credits to ${this.testFileName}: ${legacyCC.capacityTokenIdStr}`)); testFileCC[this.network] = legacyCC; return legacyCC; } } return undefined; } /** * Save newly minted capacity credits to state */ saveCapacityCredits(capacityCreditInfo) { // Calculate expiration date using the same logic as mint-cc.ts const mintedDate = new Date(capacityCreditInfo.mintedAtUtc); const expirationDate = new Date(Date.UTC(mintedDate.getUTCFullYear(), mintedDate.getUTCMonth(), mintedDate.getUTCDate() + capacityCreditInfo.daysUntilUTCMidnightExpiration, 0, 0, 0, 0)); // Ensure current config state is initialized this.getCurrentConfigState(); // Save capacity credits for this test file and network const testFileCC = this.nestedState.testFiles[this.testFileName].capacityCredits; testFileCC[this.network] = { ...capacityCreditInfo, network: this.network, expiresAt: expirationDate.toISOString(), }; console.log(chalk_1.default.green(`💾 Saved capacity credits for ${this.testFileName}: ${capacityCreditInfo.capacityTokenIdStr}`)); console.log(chalk_1.default.green(` ↳ Expires: ${expirationDate.toISOString()}`)); } /** * Get or mint capacity credits for testing */ async getOrMintCapacityCredits(mintFunction) { // Check if we have existing valid capacity credits const existingCC = this.getExistingCapacityCredits(); if (existingCC) { return { capacityCredits: { capacityTokenIdStr: existingCC.capacityTokenIdStr, capacityTokenId: existingCC.capacityTokenId, requestsPerKilosecond: existingCC.requestsPerKilosecond, daysUntilUTCMidnightExpiration: existingCC.daysUntilUTCMidnightExpiration, mintedAtUtc: existingCC.mintedAtUtc, }, isNew: false, }; } // Mint new capacity credits console.log(chalk_1.default.yellow(`🎫 Minting new capacity credits...`)); const newCC = await mintFunction(); // Save to state this.saveCapacityCredits(newCC); return { capacityCredits: newCC, isNew: true, }; } /** * Get existing Vincent app or return undefined if needs registration * First checks current config, then searches all configs for apps with same delegatee */ getExistingVincentApp(delegateeAddress) { const configState = this.getCurrentConfigState(); const existingApp = configState.state.vincentApp; if (existingApp && existingApp.network === this.network && existingApp.delegateeAddress === delegateeAddress) { console.log(chalk_1.default.blue(`🏢 Using saved Vincent app for config ${this.configHash}: ${existingApp.appId}`)); console.log(chalk_1.default.blue(` ↳ Version: ${existingApp.appVersion}`)); console.log(chalk_1.default.blue(` ↳ Delegatee: ${existingApp.delegateeAddress}`)); return existingApp; } // If no app in current config, search all configs for apps with same delegatee // This handles the case where the smart contract prevents the same delegatee from registering multiple apps if (this.nestedState.testFiles[this.testFileName]) { const allConfigs = this.nestedState.testFiles[this.testFileName].configurations; for (const [configHash, config] of Object.entries(allConfigs)) { if (configHash === this.configHash) continue; // Skip current config (already checked) const appInOtherConfig = config.state.vincentApp; if (appInOtherConfig && appInOtherConfig.network === this.network && appInOtherConfig.delegateeAddress === delegateeAddress) { console.log(chalk_1.default.yellow(`🏢 Found existing Vincent app in different config ${configHash}: ${appInOtherConfig.appId}`)); console.log(chalk_1.default.yellow(` ↳ Version: ${appInOtherConfig.appVersion}`)); console.log(chalk_1.default.yellow(` ↳ Delegatee: ${appInOtherConfig.delegateeAddress}`)); console.log(chalk_1.default.yellow(` ↳ Copying app to current config ${this.configHash}`)); // Copy the app to current configuration since same delegatee can't register twice configState.state.vincentApp = { ...appInOtherConfig, // Update metadata to reflect current configuration createdAt: new Date().toISOString(), }; return appInOtherConfig; } } } return undefined; } /** * Save a newly registered Vincent app to state */ saveVincentApp(appId, appVersion, delegateeAddress, abilityIpfsCids, abilityPolicies, policyParams) { const configState = this.getCurrentConfigState(); configState.state.vincentApp = { appId, appVersion, delegateeAddress, abilityIpfsCids, abilityPolicies, policyParams, createdAt: new Date().toISOString(), network: this.network, }; console.log(chalk_1.default.green(`💾 Saved Vincent app for config ${this.configHash}: ${appId}`)); console.log(chalk_1.default.green(` ↳ Version: ${appVersion}`)); console.log(chalk_1.default.green(` ↳ Delegatee: ${delegateeAddress}`)); } // /** // * Get or register Vincent app for testing // */ // async getOrRegisterVincentApp( // delegateeAddress: string, // registerFunction: () => Promise<{ appId: string; appVersion: string }>, // abilityIpfsCids: string[], // abilityPolicies: string[][], // abilityPolicyParameterNames: string[][], // abilityPolicyParameterTypes: number[][], // abilityPolicyParameterValues?: string[][] // ): Promise<{ // appId: string; // appVersion: string; // isNew: boolean; // abilityIpfsCids: string[]; // abilityPolicies: string[][]; // abilityPolicyParameterNames: string[][]; // abilityPolicyParameterTypes: number[][]; // abilityPolicyParameterValues?: string[][]; // }> { // // Check if we have an existing Vincent app for this delegatee // const existingApp = this.getExistingVincentApp(delegateeAddress); // if (existingApp) { // // Update the current configuration with the new ability/policy information // // since we're reusing an existing app but with potentially different abilities // const configState = this.getCurrentConfigState(); // if (configState.state.vincentApp) { // console.log( // chalk.blue( // `🔄 Updating reused app ${existingApp.appId} with current ability/policy configuration` // ) // ); // configState.state.vincentApp.abilityIpfsCids = abilityIpfsCids; // configState.state.vincentApp.abilityPolicies = abilityPolicies; // configState.state.vincentApp.abilityPolicyParameterNames = // abilityPolicyParameterNames; // configState.state.vincentApp.abilityPolicyParameterTypes = // abilityPolicyParameterTypes; // configState.state.vincentApp.abilityPolicyParameterValues = // abilityPolicyParameterValues; // } // return { // appId: existingApp.appId, // appVersion: existingApp.appVersion, // isNew: false, // abilityIpfsCids, // abilityPolicies, // abilityPolicyParameterNames, // abilityPolicyParameterTypes, // abilityPolicyParameterValues, // }; // } // // Register new Vincent app // console.log(chalk.yellow(`🏢 Registering new Vincent app...`)); // const { appId, appVersion } = await registerFunction(); // // Save to state // this.saveVincentApp( // appId, // appVersion, // delegateeAddress, // abilityIpfsCids, // abilityPolicies, // abilityPolicyParameterNames, // abilityPolicyParameterTypes, // abilityPolicyParameterValues // ); // return { // appId, // appVersion, // isNew: true, // abilityIpfsCids, // abilityPolicies, // abilityPolicyParameterNames, // abilityPolicyParameterTypes, // abilityPolicyParameterValues, // }; // } /** * Update existing Vincent app state with parameter values */ updateVincentAppParameterValues(appId, policyParams) { const configState = this.getCurrentConfigState(); if (configState.state.vincentApp && configState.state.vincentApp.appId === appId) { configState.state.vincentApp.policyParams = policyParams; console.log(chalk_1.default.green(`💾 Updated Vincent app parameter values for config ${this.configHash}: ${appId}`)); } } /** * Check if PKP is already permitted for the given app version */ isPKPPermittedForAppVersion(pkpEthAddress, appId, appVersion) { const configState = this.getCurrentConfigState(); if (!configState.state.pkpAppPermissions) { return false; } const existingPermission = configState.state.pkpAppPermissions.find((permission) => permission.pkpEthAddress === pkpEthAddress && permission.appId === appId && permission.appVersion === appVersion && permission.network === this.network); if (existingPermission) { console.log(chalk_1.default.blue(`🔐 PKP ${pkpEthAddress} already permitted for app ${appId} v${appVersion} in config ${this.configHash}`)); console.log(chalk_1.default.blue(` ↳ Permitted at: ${existingPermission.permittedAt}`)); return true; } return false; } /** * Save PKP app permission to state */ savePKPAppPermission(pkpEthAddress, appId, appVersion) { const configState = this.getCurrentConfigState(); if (!configState.state.pkpAppPermissions) { configState.state.pkpAppPermissions = []; } // Check if permission already exists to avoid duplicates const existingIndex = configState.state.pkpAppPermissions.findIndex((permission) => permission.pkpEthAddress === pkpEthAddress && permission.appId === appId && permission.appVersion === appVersion && permission.network === this.network); const newPermission = { pkpEthAddress, appId, appVersion, permittedAt: new Date().toISOString(), network: this.network, }; if (existingIndex >= 0) { // Update existing permission configState.state.pkpAppPermissions[existingIndex] = newPermission; console.log(chalk_1.default.gray(`💾 Updated PKP app permission for config ${this.configHash}: PKP ${pkpEthAddress} for app ${appId} v${appVersion}`)); } else { // Add new permission configState.state.pkpAppPermissions.push(newPermission); console.log(chalk_1.default.gray(`💾 Saved PKP app permission for config ${this.configHash}: PKP ${pkpEthAddress} for app ${appId} v${appVersion}`)); } } } exports.StateManager = StateManager;