@lit-protocol/vincent-scaffold-sdk
Version:
Shared build configuration and utilities for Vincent abilities and policies
1,214 lines (1,089 loc) • 35.4 kB
text/typescript
import chalk from "chalk";
import { promises as fs } from "fs";
import path from "path";
import { createHash } from "crypto";
import { ethers } from "ethers";
import { CapacityCreditInfo } from "../utils/mint-cc";
import { PKPInfo } from "../utils/mint-pkp";
import { PermissionData } from "@lit-protocol/vincent-contracts-sdk";
interface AccountState {
privateKey: string;
address: string;
createdAt: string;
network: string;
}
interface PKPState extends PKPInfo {
createdAt: string;
network: string;
}
interface CapacityCreditState extends CapacityCreditInfo {
network: string;
expiresAt: string; // ISO timestamp of when the capacity credits expire
}
interface VincentAppState {
appId: number;
appVersion: number;
createdAt: string;
network: string;
delegateeAddress: string;
abilityIpfsCids: string[];
abilityPolicies: string[][];
policyParams: PermissionData;
}
interface PKPAppPermissionState {
pkpEthAddress: string;
appId: number;
appVersion: number;
permittedAt: string;
network: string;
}
interface ConfigurationState {
configHash: string;
abilityIpfsCids: string[];
abilityPolicyCids: string[][];
lastUsed: string;
network: string;
state: {
vincentApp?: VincentAppState;
pkpAppPermissions?: PKPAppPermissionState[];
};
}
interface AppVersionConfiguration {
appId: number;
appVersion: number;
abilityIpfsCids: string[];
abilityPolicyCids: string[][];
lastUsed: string;
network: string;
state: {
vincentApp: VincentAppState;
pkpAppPermissions?: PKPAppPermissionState[];
};
}
interface NestedE2EState {
version: string;
// Keep legacy structures for backward compatibility
sharedAccounts?: {
[network: string]: {
appManager?: AccountState;
appDelegatee?: AccountState;
agentWalletPkpOwner?: AccountState;
};
};
sharedPkps?: {
[network: string]: PKPState;
};
sharedCapacityCredits?: {
[network: string]: CapacityCreditState;
};
testFiles: {
[fileName: string]: {
// Per-test-file accounts (isolated)
accounts?: {
[network: string]: {
appManager?: AccountState;
appDelegatee?: AccountState;
agentWalletPkpOwner?: AccountState;
};
};
// Per-test-file PKPs (isolated)
pkps?: {
[network: string]: PKPState;
};
// Per-test-file capacity credits (isolated)
capacityCredits?: {
[network: string]: CapacityCreditState;
};
configurations: {
[configHash: string]: ConfigurationState;
};
// New app-version-based configurations
appVersions: {
[appVersionKey: string]: AppVersionConfiguration; // key format: "appId-appVersion"
};
};
};
}
const STATE_FILE_PATH = path.join(process.cwd(), ".e2e-state.json");
const STATE_VERSION = "2.0.0";
export class StateManager {
nestedState: NestedE2EState = {
version: STATE_VERSION,
testFiles: {},
};
private currentConfigState: ConfigurationState | null = null;
constructor(
private network: string,
private testFileName: string,
private configHash: string,
abilityIpfsCids?: string[],
abilityPolicyCids?: string[][]
) {
// 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: string[],
abilityPolicyCids: string[][]
): string {
// 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 = createHash("sha256")
.update(cidString)
.digest("hex")
.substring(0, 8);
return hash;
}
/**
* Auto-detect test filename from call stack
*/
static autoDetectTestFileName(): string {
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
*/
private getCurrentConfigState(): ConfigurationState {
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: string[],
abilityPolicyCids: string[][]
): void {
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 as any).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: string[],
abilityPolicyCids: string[][]
) {
const configState = this.getCurrentConfigState();
configState.abilityIpfsCids = abilityIpfsCids;
configState.abilityPolicyCids = abilityPolicyCids;
configState.lastUsed = new Date().toISOString();
}
/**
* Generate app version key from appId and appVersion
*/
private static generateAppVersionKey(
appId: number,
appVersion: number
): string {
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
*/
private saveAppVersionConfiguration(
appId: number,
appVersion: number,
vincentApp: VincentAppState,
abilityIpfsCids: string[],
abilityPolicyCids: string[][]
): void {
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.green(`💾 Saved app version configuration: ${appVersionKey}`)
);
}
/**
* Get or register Vincent app using app versioning approach
*/
async getOrRegisterVincentAppVersioned(
delegateeAddress: string,
registerAppFunction: () => Promise<{ appId: number; appVersion: number }>,
registerNextVersionFunction: (
appId: number
) => Promise<{ appId: number; appVersion: number }>,
abilityIpfsCids: string[],
abilityPolicies: string[][],
policyParams: PermissionData,
): Promise<{
appId: number;
appVersion: number;
isNew: boolean;
isNewVersion: boolean;
abilityIpfsCids: string[];
abilityPolicies: string[][];
policyParams: PermissionData;
}> {
// 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.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.yellow(`🏢 Registering new version for existing app: ${appId}`)
);
const { appVersion } = await registerNextVersionFunction(appId);
// Save the new app version
const vincentApp: VincentAppState = {
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.yellow(`🏢 Registering new Vincent app...`));
const { appId, appVersion } = await registerAppFunction();
// Save the new app version
const vincentApp: VincentAppState = {
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
*/
private findMatchingAppVersion(
appId: number,
abilityIpfsCids: string[],
abilityPolicies: string[][]
): number | undefined {
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.blue(`🔍 Found matching app version: ${appVersionKey}`)
);
return config.appVersion;
}
}
}
console.log(
chalk.yellow(
`🔍 No matching app version found for appId ${appId} with current abilities/policies`
)
);
return undefined;
}
async loadState(): Promise<void> {
try {
const data = await fs.readFile(STATE_FILE_PATH, "utf-8");
const loadedState = JSON.parse(data);
// Validate version compatibility
if (loadedState.version !== STATE_VERSION) {
console.warn(
chalk.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.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.blue("📝 No existing state file found. Will create one.")
);
this.currentConfigState = this.getCurrentConfigState();
}
}
async saveState(): Promise<void> {
try {
// Update last used timestamp
if (this.currentConfigState) {
this.currentConfigState.lastUsed = new Date().toISOString();
}
await fs.writeFile(
STATE_FILE_PATH,
JSON.stringify(this.nestedState, null, 2),
"utf-8"
);
console.log(
chalk.gray(`💾 Saved state for ${this.testFileName}:${this.configHash}`)
);
} catch (error) {
console.error(chalk.red("❌ Failed to save state:"), error);
throw error;
}
}
generateAccount(): AccountState {
const wallet = ethers.Wallet.createRandom();
return {
privateKey: wallet.privateKey,
address: wallet.address,
createdAt: new Date().toISOString(),
network: this.network,
};
}
getOrGenerateAccount(
accountType: "appManager" | "appDelegatee" | "agentWalletPkpOwner",
existingPrivateKey?: string
): { privateKey: string; address: string; isNew: boolean } {
// If private key provided from env, use it
if (existingPrivateKey) {
const wallet = new 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.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.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.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(): PKPState | undefined {
// 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.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.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: PKPInfo): void {
// 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.gray(`💾 Saved PKP for ${this.testFileName}: ${pkpInfo.ethAddress}`)
);
}
/**
* Get or mint the single testing PKP
*/
async getOrMintPKP(
mintFunction: () => Promise<PKPInfo>
): Promise<{ pkp: PKPInfo; isNew: boolean }> {
// 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.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)
*/
private isCapacityCreditValid(capacityCredit: CapacityCreditState): boolean {
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.yellow(
` ↳ Capacity credits expire at: ${expirationDate.toISOString()}`
)
);
console.log(chalk.yellow(` ↳ Current time: ${now.toISOString()}`));
}
return isValid;
}
/**
* Get existing capacity credits or return undefined if needs minting
*/
getExistingCapacityCredits(): CapacityCreditState | undefined {
// 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.blue(
`🎫 Using capacity credits from ${this.testFileName}: ${existingCC.capacityTokenIdStr}`
)
);
console.log(chalk.blue(` ↳ Expires: ${existingCC.expiresAt}`));
return existingCC;
} else {
console.log(
chalk.yellow(
`⚠️ Capacity credits found but expired/expiring soon for ${this.testFileName}`
)
);
console.log(
chalk.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.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: CapacityCreditInfo): void {
// 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.green(
`💾 Saved capacity credits for ${this.testFileName}: ${capacityCreditInfo.capacityTokenIdStr}`
)
);
console.log(chalk.green(` ↳ Expires: ${expirationDate.toISOString()}`));
}
/**
* Get or mint capacity credits for testing
*/
async getOrMintCapacityCredits(
mintFunction: () => Promise<CapacityCreditInfo>
): Promise<{ capacityCredits: CapacityCreditInfo; isNew: boolean }> {
// 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.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: string): VincentAppState | undefined {
const configState = this.getCurrentConfigState();
const existingApp = configState.state.vincentApp;
if (
existingApp &&
existingApp.network === this.network &&
existingApp.delegateeAddress === delegateeAddress
) {
console.log(
chalk.blue(
`🏢 Using saved Vincent app for config ${this.configHash}: ${existingApp.appId}`
)
);
console.log(chalk.blue(` ↳ Version: ${existingApp.appVersion}`));
console.log(
chalk.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.yellow(
`🏢 Found existing Vincent app in different config ${configHash}: ${appInOtherConfig.appId}`
)
);
console.log(
chalk.yellow(` ↳ Version: ${appInOtherConfig.appVersion}`)
);
console.log(
chalk.yellow(` ↳ Delegatee: ${appInOtherConfig.delegateeAddress}`)
);
console.log(
chalk.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: number,
appVersion: number,
delegateeAddress: string,
abilityIpfsCids: string[],
abilityPolicies: string[][],
policyParams: PermissionData,
): void {
const configState = this.getCurrentConfigState();
configState.state.vincentApp = {
appId,
appVersion,
delegateeAddress,
abilityIpfsCids,
abilityPolicies,
policyParams,
createdAt: new Date().toISOString(),
network: this.network,
};
console.log(
chalk.green(
`💾 Saved Vincent app for config ${this.configHash}: ${appId}`
)
);
console.log(chalk.green(` ↳ Version: ${appVersion}`));
console.log(chalk.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: number,
policyParams: PermissionData,
): void {
const configState = this.getCurrentConfigState();
if (
configState.state.vincentApp &&
configState.state.vincentApp.appId === appId
) {
configState.state.vincentApp.policyParams = policyParams;
console.log(
chalk.green(
`💾 Updated Vincent app parameter values for config ${this.configHash}: ${appId}`
)
);
}
}
/**
* Check if PKP is already permitted for the given app version
*/
isPKPPermittedForAppVersion(
pkpEthAddress: string,
appId: number,
appVersion: number
): boolean {
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.blue(
`🔐 PKP ${pkpEthAddress} already permitted for app ${appId} v${appVersion} in config ${this.configHash}`
)
);
console.log(
chalk.blue(` ↳ Permitted at: ${existingPermission.permittedAt}`)
);
return true;
}
return false;
}
/**
* Save PKP app permission to state
*/
savePKPAppPermission(
pkpEthAddress: string,
appId: number,
appVersion: number
): void {
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: PKPAppPermissionState = {
pkpEthAddress,
appId,
appVersion,
permittedAt: new Date().toISOString(),
network: this.network,
};
if (existingIndex >= 0) {
// Update existing permission
configState.state.pkpAppPermissions[existingIndex] = newPermission;
console.log(
chalk.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.gray(
`💾 Saved PKP app permission for config ${this.configHash}: PKP ${pkpEthAddress} for app ${appId} v${appVersion}`
)
);
}
}
}