UNPKG

@nuwa-ai/identity-kit

Version:

SDK for NIP-1 Agent Single DID Multi-Key Model and NIP-3 CADOP (Custodian-Assisted DID Onboarding Protocol)

1 lines 264 kB
{"version":3,"sources":["../../src/testHelpers/index.ts","../../src/testHelpers/env.ts","../../src/utils/DebugLogger.ts","../../src/cache/InMemoryLRUDIDDocumentCache.ts","../../src/vdr/VDRRegistry.ts","../../src/vdr/roochVDR.ts","../../src/multibase/base.ts","../../src/utils/bytes.ts","../../src/types/crypto.ts","../../src/multibase/key.ts","../../src/multibase/did.ts","../../src/crypto/providers/ed25519.ts","../../src/crypto/providers/secp256k1.ts","../../src/crypto/providers/ecdsa_r1.ts","../../src/crypto/factory.ts","../../src/crypto/utils.ts","../../src/signers/keyStoreUtils.ts","../../src/signers/didAccountSigner.ts","../../src/utils/did.ts","../../src/vdr/abstractVDR.ts","../../src/vdr/roochVDRTypes.ts","../../src/utils/sessionScopes.ts","../../src/testHelpers/rooch.ts","../../src/testHelpers/didFactory.ts","../../src/keys/KeyStore.ts","../../src/keys/StoredKeyCodec.ts","../../src/keys/KeyManager.ts","../../src/vdr/keyVDR.ts","../../src/vdr/index.ts","../../src/IdentityEnv.ts","../../src/IdentityKit.ts","../../src/CadopIdentityKit.ts"],"sourcesContent":["/**\n * Test helpers for Rooch DID integration testing\n *\n * This module provides utilities to simplify DID-related integration tests\n * by abstracting away the complexity of on-chain DID creation and management.\n *\n * @module testHelpers\n * @example\n * ```ts\n * import { TestEnv, createSelfDid, createCadopDid } from '@nuwa-ai/identity-kit/testHelpers';\n *\n * if (TestEnv.skipIfNoNode()) return;\n *\n * const env = await TestEnv.bootstrap();\n * const { did, keyManager } = await createSelfDid(env);\n * ```\n */\n\nexport * from './env';\nexport * from './rooch';\nexport * from './didFactory';\nexport * from './types';\n\n// Re-export key classes and functions for convenience\nexport { TestEnv } from './env';\nexport { createSelfDid, createCadopCustodian, createDidViaCadop } from './didFactory';\n","import { RoochClient } from '@roochnetwork/rooch-sdk';\nimport { DebugLogger } from '../utils/DebugLogger';\nimport { VDRRegistry } from '../vdr/VDRRegistry';\nimport { RoochVDR } from '../vdr/roochVDR';\nimport { KeyManager } from '../keys/KeyManager';\nimport { MemoryKeyStore } from '../keys/KeyStore';\nimport {\n TestEnvOptions,\n EnvironmentCheck,\n CreateSelfDidResult,\n CreateSelfDidOptions,\n CreateCadopDidOptions,\n} from './types';\n\n/**\n * Test environment for Rooch DID integration testing\n *\n * Provides a pre-configured environment with:\n * - Rooch client and VDR registry\n * - Helper methods for creating test identities\n *\n * Note: Each createSelfDid() call returns its own dedicated IdentityEnv,\n * which is preferred for multi-party testing scenarios to avoid conflicts.\n */\nexport class TestEnv {\n private static instance?: TestEnv;\n private logger: DebugLogger;\n\n public readonly rpcUrl: string;\n public readonly network: string;\n public readonly client: RoochClient;\n public readonly vdrRegistry: VDRRegistry;\n public readonly roochVDR: RoochVDR;\n\n private constructor(options: Required<TestEnvOptions>) {\n this.logger = DebugLogger.get('TestEnv');\n this.rpcUrl = options.rpcUrl;\n this.network = options.network;\n\n // Initialize Rooch client\n this.client = new RoochClient({ url: this.rpcUrl });\n\n // Initialize VDR\n this.vdrRegistry = VDRRegistry.getInstance();\n this.roochVDR = new RoochVDR({\n rpcUrl: this.rpcUrl,\n network: options.network as any,\n debug: options.debug,\n });\n\n // Register VDR if not already registered\n if (!this.vdrRegistry.getVDR('rooch')) {\n this.vdrRegistry.registerVDR(this.roochVDR);\n }\n\n if (options.debug) {\n this.logger.debug('TestEnv initialized', {\n rpcUrl: this.rpcUrl,\n network: this.network,\n });\n }\n }\n\n /**\n * Bootstrap test environment\n */\n static async bootstrap(options: TestEnvOptions = {}): Promise<TestEnv> {\n const resolvedOptions = await TestEnv.resolveOptions(options);\n\n // Check environment\n const check = await TestEnv.checkEnvironment(resolvedOptions);\n if (check.shouldSkip) {\n throw new Error(`Test environment not available: ${check.reason}`);\n }\n\n return new TestEnv(resolvedOptions);\n }\n\n /**\n * Check if integration tests should be skipped\n */\n static skipIfNoNode(): boolean {\n const check = TestEnv.checkEnvironmentSync();\n return check.shouldSkip;\n }\n\n /**\n * Synchronous environment check\n */\n static checkEnvironmentSync(): EnvironmentCheck {\n const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';\n const hasRpcUrl = !!process.env.ROOCH_NODE_URL;\n\n if (isCI && !hasRpcUrl) {\n return {\n shouldSkip: true,\n reason: 'ROOCH_NODE_URL not set in CI environment',\n };\n }\n\n return {\n shouldSkip: false,\n rpcUrl: process.env.ROOCH_NODE_URL || 'http://localhost:6767',\n };\n }\n\n /**\n * Async environment check with RPC connectivity test\n */\n static async checkEnvironment(options: Required<TestEnvOptions>): Promise<EnvironmentCheck> {\n try {\n const client = new RoochClient({ url: options.rpcUrl });\n // Try to get chain ID to verify connectivity\n await client.getChainId();\n\n return {\n shouldSkip: false,\n rpcUrl: options.rpcUrl,\n };\n } catch (error) {\n const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';\n\n if (isCI) {\n return {\n shouldSkip: true,\n reason: `Cannot connect to Rooch node at ${options.rpcUrl}: ${error instanceof Error ? error.message : 'Unknown error'}`,\n };\n }\n\n // For local development, we might want to continue with a warning\n console.warn(\n `Warning: Cannot connect to Rooch node at ${options.rpcUrl}. Some tests may fail.`\n );\n return {\n shouldSkip: false,\n rpcUrl: options.rpcUrl,\n };\n }\n }\n\n /**\n * Resolve options with defaults\n */\n private static async resolveOptions(options: TestEnvOptions): Promise<Required<TestEnvOptions>> {\n const defaults = {\n rpcUrl: process.env.ROOCH_NODE_URL || 'http://localhost:6767',\n network: 'test' as const,\n autoStartLocalNode: false,\n faucetAmount: BigInt(1000000), // 1M base units\n debug: false,\n };\n\n return { ...defaults, ...options };\n }\n\n /**\n * Fund an account via faucet (placeholder for future implementation)\n */\n async fundAccount(address: string, amount?: bigint): Promise<void> {\n // This is a placeholder - actual faucet implementation would depend on the network\n this.logger.debug('Funding account', { address, amount });\n // For now, we assume accounts have sufficient funds or skip funding\n }\n}\n","/* eslint-disable no-console */\n/*\n * Lightweight environment-agnostic debug logger.\n *\n * ‑ Works in both Node.js and browser.\n * ‑ Supports level filtering (debug | info | warn | error | silent).\n * ‑ Namespaced: each module/class can request its own logger via DebugLogger.get(\"MyModule\").\n * ‑ Global level can be controlled at runtime via DebugLogger.setGlobalLevel() *or*\n * environment variable NUWA_LOG_LEVEL (node) / window.__NUWA_LOG_LEVEL__ (browser).\n */\n\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';\n\nconst LEVEL_ORDER: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n silent: 50,\n};\n\nfunction detectInitialGlobalLevel(): LogLevel {\n // Node.js: use process.env if available\n if (typeof process !== 'undefined' && (process as any).env) {\n const envLevel = (process as any).env.NUWA_LOG_LEVEL as string | undefined;\n if (envLevel && envLevel in LEVEL_ORDER) return envLevel as LogLevel;\n }\n // Browser: allow runtime override via global variable\n if (typeof window !== 'undefined' && (window as any).__NUWA_LOG_LEVEL__) {\n const envLevel = (window as any).__NUWA_LOG_LEVEL__ as string;\n if (envLevel && envLevel in LEVEL_ORDER) return envLevel as LogLevel;\n }\n return 'info';\n}\n\nexport class DebugLogger {\n // ---------------------------------------------------------------------------\n // Static section\n // ---------------------------------------------------------------------------\n private static globalLevel: LogLevel = detectInitialGlobalLevel();\n private static loggers = new Map<string, DebugLogger>();\n private static defaultNamespace = 'global';\n\n /** Acquire (or create) a logger for the given namespace. */\n static get(namespace: string): DebugLogger {\n if (!DebugLogger.loggers.has(namespace)) {\n DebugLogger.loggers.set(namespace, new DebugLogger(namespace));\n }\n return DebugLogger.loggers.get(namespace)!;\n }\n\n /** Override global log level at runtime. */\n static setGlobalLevel(level: LogLevel): void {\n DebugLogger.globalLevel = level;\n // Propagate to existing instances unless they explicitly override.\n for (const logger of DebugLogger.loggers.values()) {\n if (!logger.levelOverridden) {\n logger.level = level;\n }\n }\n }\n\n /** Read current global level. */\n static getGlobalLevel(): LogLevel {\n return DebugLogger.globalLevel;\n }\n\n /** Set default namespace used by static convenience methods. */\n static setDefaultNamespace(namespace: string): void {\n DebugLogger.defaultNamespace = namespace;\n }\n\n // ---------------------------------------------------------------------------\n // Static convenience logging methods\n // ---------------------------------------------------------------------------\n /**\n * Log using the default namespace. Useful when callers don't need per-module loggers.\n * Example: DebugLogger.debug('hello')\n */\n static debug(...args: unknown[]): void {\n DebugLogger.get(DebugLogger.defaultNamespace).debug(...args);\n }\n\n static info(...args: unknown[]): void {\n DebugLogger.get(DebugLogger.defaultNamespace).info(...args);\n }\n\n static warn(...args: unknown[]): void {\n DebugLogger.get(DebugLogger.defaultNamespace).warn(...args);\n }\n\n static error(...args: unknown[]): void {\n DebugLogger.get(DebugLogger.defaultNamespace).error(...args);\n }\n\n // ---------------------------------------------------------------------------\n // Instance section\n // ---------------------------------------------------------------------------\n private level: LogLevel;\n private levelOverridden = false;\n\n private constructor(private namespace: string) {\n this.level = DebugLogger.globalLevel;\n }\n\n /** Override level for this logger only. */\n setLevel(level: LogLevel): void {\n this.level = level;\n this.levelOverridden = true;\n }\n\n // -------------------------------------------------------\n // Logging helpers\n // -------------------------------------------------------\n debug(...args: unknown[]): void {\n this._log('debug', args);\n }\n\n info(...args: unknown[]): void {\n this._log('info', args);\n }\n\n warn(...args: unknown[]): void {\n this._log('warn', args);\n }\n\n error(...args: unknown[]): void {\n this._log('error', args);\n }\n\n // prettier-ignore\n private _log(level: LogLevel, args: unknown[]): void {\n if (LEVEL_ORDER[level] < LEVEL_ORDER[this.level]) {\n return; // filtered out\n }\n\n const prefix = `[${this.namespace}]`;\n\n // Colorize in browser / modern terminal if desired; keep simple for now.\n switch (level) {\n case 'debug':\n console.debug(prefix, ...args);\n break;\n case 'info':\n console.info(prefix, ...args);\n break;\n case 'warn':\n console.warn(prefix, ...args);\n break;\n case 'error':\n console.error(prefix, ...args);\n break;\n }\n }\n}\n","import { DIDDocument } from '../types';\nimport { DIDDocumentCache } from './index';\n\n/**\n * A lightweight in-memory LRU cache implementation for DID Documents.\n * It is intentionally dependency-free so that the SDK does not pull in\n * additional packages by default. You can replace it with your own\n * implementation (Redis, IndexedDB, etc.) by implementing the\n * `DIDDocumentCache` interface and providing it to `VDRRegistry.setCache()`.\n */\nexport class InMemoryLRUDIDDocumentCache implements DIDDocumentCache {\n private readonly capacity: number;\n private readonly map: Map<string, DIDDocument | null>;\n\n constructor(maxEntries = 1000) {\n this.capacity = maxEntries;\n this.map = new Map<string, DIDDocument | null>();\n }\n\n get(did: string): DIDDocument | null | undefined {\n if (!this.map.has(did)) return undefined;\n const value = this.map.get(did) ?? null;\n // Refresh the recently used key to the end.\n this.map.delete(did);\n this.map.set(did, value);\n return value;\n }\n\n set(did: string, doc: DIDDocument | null): void {\n if (this.map.has(did)) {\n this.map.delete(did);\n } else if (this.map.size >= this.capacity) {\n // Evict the least-recently-used entry (Map iteration order is insertion order).\n const lruKey = this.map.keys().next().value;\n if (lruKey !== undefined) {\n this.map.delete(lruKey);\n }\n }\n this.map.set(did, doc);\n }\n\n has(did: string): boolean {\n return this.map.has(did);\n }\n\n delete(did: string): void {\n this.map.delete(did);\n }\n\n clear(): void {\n this.map.clear();\n }\n}\n","import { DIDDocument, DIDResolver } from '../types';\nimport { DIDDocumentCache } from '../cache';\nimport { VDRInterface, DIDCreationRequest, DIDCreationResult, CADOPCreationRequest } from './types';\n\nimport { InMemoryLRUDIDDocumentCache } from '../cache/InMemoryLRUDIDDocumentCache';\n\n/**\n * Global registry for VDR (Verifiable Data Registry) implementations.\n * This singleton manages all registered VDRs and maintains a DID Document cache.\n */\nexport class VDRRegistry implements DIDResolver {\n private static instance: VDRRegistry;\n private vdrs: Map<string, VDRInterface> = new Map();\n\n private cache: DIDDocumentCache;\n\n private constructor() {\n // Use the default in-memory cache unless overridden by the developer.\n this.cache = new InMemoryLRUDIDDocumentCache();\n }\n\n static getInstance(): VDRRegistry {\n if (!this.instance) {\n this.instance = new VDRRegistry();\n }\n return this.instance;\n }\n\n /** Register a VDR implementation for its DID method (e.g., 'key', 'rooch'). */\n registerVDR(vdr: VDRInterface) {\n this.vdrs.set(vdr.getMethod(), vdr);\n }\n\n /** Retrieve a previously registered VDR implementation by its method. */\n getVDR(method: string): VDRInterface | undefined {\n return this.vdrs.get(method);\n }\n\n /**\n * Override the default cache implementation.\n * This allows developers to provide their own cache (e.g., Redis, browser storage).\n */\n setCache(cache: DIDDocumentCache) {\n this.cache = cache;\n }\n\n /** Returns the currently configured cache instance. */\n getCache(): DIDDocumentCache {\n return this.cache;\n }\n\n async resolveDID(did: string, options?: { forceRefresh?: boolean }): Promise<DIDDocument | null> {\n const method = did.split(':')[1];\n const vdr = this.vdrs.get(method);\n if (!vdr) {\n throw new Error(`No VDR available for method: ${method}`);\n }\n\n // Attempt to serve from cache if allowed.\n if (!options?.forceRefresh) {\n const cached = this.cache.get(did);\n if (cached !== undefined) {\n return cached;\n }\n }\n\n const resolved = await vdr.resolve(did);\n // Cache the resolution result (including null for negative caching).\n this.cache.set(did, resolved);\n return resolved;\n }\n\n async createDID(\n method: string,\n creationRequest: DIDCreationRequest,\n options?: Record<string, any>\n ): Promise<DIDCreationResult> {\n const vdr = this.vdrs.get(method);\n if (!vdr) {\n throw new Error(`No VDR available for method: ${method}`);\n }\n const result = await vdr.create(creationRequest, options);\n if (result.success && result.didDocument) {\n this.cache.set(result.didDocument.id, result.didDocument);\n }\n return result;\n }\n\n async createDIDViaCADOP(\n method: string,\n creationRequest: CADOPCreationRequest,\n options?: Record<string, any>\n ): Promise<DIDCreationResult> {\n const vdr = this.vdrs.get(method);\n if (!vdr) {\n throw new Error(`No VDR available for method: ${method}`);\n }\n const result = await vdr.createViaCADOP(creationRequest, options);\n if (result.success && result.didDocument) {\n this.cache.set(result.didDocument.id, result.didDocument);\n }\n return result;\n }\n\n async exists(did: string): Promise<boolean> {\n const method = did.split(':')[1];\n const vdr = this.vdrs.get(method);\n if (!vdr) {\n throw new Error(`No VDR available for method: ${method}`);\n }\n\n // If we have a positive cache entry, short-circuit the call.\n if (this.cache.has(did)) {\n const doc = this.cache.get(did);\n return doc !== null;\n }\n\n const exists = await vdr.exists(did);\n // We don't cache the existence check result here to avoid stale data.\n return exists;\n }\n}\n","import {\n RoochClient,\n Transaction,\n Args,\n getRoochNodeUrl as sdkGetRoochNodeUrl,\n Signer,\n RoochAddress,\n SignatureScheme,\n Keypair,\n PublicKey,\n Address,\n Bytes,\n Authenticator,\n BitcoinAddress,\n ObjectStateView,\n Ed25519PublicKey,\n Secp256k1PublicKey,\n EventView,\n} from '@roochnetwork/rooch-sdk';\nimport {\n DIDDocument,\n VerificationMethod,\n VerificationRelationship,\n ServiceEndpoint,\n} from '../types/did';\nimport { SignerInterface, DidAccountSigner } from '../signers';\nimport { KeyType, keyTypeToRoochSignatureScheme } from '../types/crypto';\nimport { DIDCreationRequest, DIDCreationResult, CADOPCreationRequest } from './types';\nimport { AbstractVDR } from './abstractVDR';\nimport {\n convertMoveDIDDocumentToInterface,\n formatDIDString,\n parseDIDCreatedEvent,\n resolveDidObjectID,\n} from './roochVDRTypes';\nimport { DebugLogger } from '../utils/DebugLogger';\nimport { parseDid, extractFragmentFromId } from '../utils/did';\nimport { validateScopes, combineScopes } from '../utils/sessionScopes';\n\nexport interface RoochClientConfig {\n url: string;\n transport?: any;\n}\n\nexport interface RoochTransactionResult {\n execution_info: {\n status: {\n type: string; // 'executed' | 'failed'\n };\n gas_used: string;\n };\n output?: {\n events?: Array<{\n event_type: string;\n event_data: string;\n event_index: string;\n decoded_event_data?: any;\n }>;\n };\n transaction: any;\n}\n\n/**\n * Options for RoochVDR configuration\n */\nexport interface RoochVDROptions {\n /**\n * Rooch RPC endpoint URL\n */\n rpcUrl?: string;\n\n /**\n * Network type (local, dev, test, main)\n */\n network?: 'local' | 'dev' | 'test' | 'main';\n\n /**\n * Enable debug mode for detailed logging\n */\n debug?: boolean;\n}\n\n/**\n * Result of store operation with actual DID address\n */\nexport interface StoreResult {\n success: boolean;\n actualDIDAddress?: string;\n}\n\n/**\n * Options for Rooch VDR operations\n */\nexport interface RoochVDROperationOptions {\n /**\n * Signer to use for this operation\n */\n signer?: SignerInterface | Signer;\n\n /**\n * Key ID to use for this operation\n */\n keyId?: string;\n\n /**\n * Custom session key scopes (for authentication VM)\n * Only used when adding a verification method with authentication relationship\n */\n scopes?: string[];\n\n /**\n * Advanced blockchain transaction options\n * For high-level users who need fine-grained control over transaction parameters\n */\n advanced?: RoochTxnOptions;\n}\n\n/**\n * Advanced Rooch blockchain transaction options\n * These options are typically only needed for advanced use cases\n */\nexport interface RoochTxnOptions {\n /**\n * Maximum gas limit for the transaction\n */\n maxGas?: number;\n}\n\n/**\n * VDR implementation for did:rooch method\n *\n * This implementation integrates with Rooch network's DID contract system\n * to provide on-chain DID document storage and management.\n */\nexport class RoochVDR extends AbstractVDR {\n private readonly options: RoochVDROptions;\n private client: RoochClient;\n private readonly didContractAddress: string;\n private readonly debug: boolean;\n private readonly logger: DebugLogger;\n private lastCreatedDIDAddress?: string;\n\n constructor(options: RoochVDROptions) {\n super('rooch');\n this.options = options;\n this.didContractAddress = '0x3::did';\n this.debug = options.debug || false;\n this.logger = DebugLogger.get('RoochVDR');\n if (this.debug) {\n this.logger.setLevel('debug');\n }\n\n let rpcUrl = options.rpcUrl;\n if (!rpcUrl) {\n rpcUrl = RoochVDR.getRoochNodeUrl(options.network || 'test');\n }\n this.logger.debug(`RoochVDR initialized with rpcUrl: ${rpcUrl}`);\n // Initialize Rooch client\n this.client = new RoochClient({ url: rpcUrl });\n }\n\n /**\n * Log message if debug mode is enabled\n */\n private debugLog(message: string, data?: any) {\n if (data !== undefined) {\n this.logger.debug(message, data);\n } else {\n this.logger.debug(message);\n }\n }\n\n /**\n * Log error message (always logged regardless of debug mode)\n */\n private errorLog(message: string, error?: any) {\n if (error !== undefined) {\n this.logger.error(message, error);\n } else {\n this.logger.error(message);\n }\n }\n\n private async convertSigner(signer: SignerInterface | Signer, keyId?: string): Promise<Signer> {\n if (signer instanceof Signer) {\n return signer;\n }\n return DidAccountSigner.create(signer, keyId);\n }\n\n /**\n * Override create method to support Rooch dynamic DID generation\n */\n async create(\n request: DIDCreationRequest,\n options?: RoochVDROperationOptions\n ): Promise<DIDCreationResult> {\n try {\n const signer = options?.signer;\n if (!signer) {\n throw new Error('No signer provided for create operation');\n }\n\n this.debugLog('Creating DID with request:', request);\n\n const didAccountSigner = await this.convertSigner(signer, options?.keyId);\n\n // Always combine base scopes with custom scopes\n const finalScopes = combineScopes(request.customScopes || []);\n\n // Validate all scopes\n const scopeValidation = validateScopes(finalScopes);\n if (!scopeValidation.valid) {\n throw new Error(`Invalid scope format: ${scopeValidation.invalidScopes.join(', ')}`);\n }\n\n // Always use the scopes version since we always have scopes (at minimum base scopes)\n const transaction = this.createTransaction();\n transaction.callFunction({\n target: `${this.didContractAddress}::create_did_object_for_self_with_custom_scopes_entry`,\n args: [Args.string(request.publicKeyMultibase), Args.vec('string', finalScopes)],\n maxGas: options?.advanced?.maxGas || 100000000,\n });\n\n this.debugLog('Creating DID with scopes:', finalScopes);\n\n this.debugLog('Creating DID Transaction:', transaction);\n\n // Execute transaction\n const result = await this.client.signAndExecuteTransaction({\n transaction,\n signer: didAccountSigner,\n option: { withOutput: true },\n });\n\n const success = result.execution_info.status.type === 'executed';\n\n if (!success) {\n // Return preferredDID or generate a failure placeholder on failure\n return {\n success: false,\n error:\n 'Transaction execution failed, execution_info: ' +\n JSON.stringify(result.execution_info),\n debug: {\n requestedDID: request.preferredDID,\n transactionResult: result.execution_info,\n },\n };\n }\n\n // Parse the actual created DID\n const didCreatedEvent = result.output?.events?.find(\n (event: EventView) => event.event_type === '0x3::did::DIDCreatedEvent'\n );\n if (!didCreatedEvent) {\n throw new Error('DIDCreatedEvent not found');\n }\n let actualDID = this.parseDIDCreatedEventAndGetDID(didCreatedEvent);\n\n this.lastCreatedDIDAddress = actualDID;\n\n let didDocument = await this.resolve(actualDID);\n if (!didDocument) {\n throw new Error('DID document not found with DID: ' + actualDID);\n }\n\n return {\n success: true,\n didDocument: didDocument,\n transactionHash: (result as any).transaction_hash,\n debug: {\n requestedDID: request.preferredDID,\n actualDID: actualDID || undefined,\n events: result.output?.events,\n },\n };\n } catch (error) {\n this.errorLog('Error creating DID:', error);\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Unknown error',\n };\n }\n }\n\n /**\n * Override CADOP creation method\n */\n async createViaCADOP(\n request: CADOPCreationRequest,\n options?: RoochVDROperationOptions\n ): Promise<DIDCreationResult> {\n try {\n const signer = options?.signer;\n if (!signer) {\n throw new Error('No custodian signer provided for CADOP operation');\n }\n\n this.debugLog('Creating DID via CADOP with request:', request);\n const didAccountSigner = await this.convertSigner(signer, options?.keyId);\n\n // Always combine base scopes with custom scopes\n const finalScopes = combineScopes(request.customScopes || []);\n\n // Validate all scopes\n const scopeValidation = validateScopes(finalScopes);\n if (!scopeValidation.valid) {\n throw new Error(`Invalid scope format: ${scopeValidation.invalidScopes.join(', ')}`);\n }\n\n // Always use the scopes version since the contract will add base scopes\n const transaction = this.createTransaction();\n transaction.callFunction({\n target: `${this.didContractAddress}::create_did_object_via_cadop_with_did_key_and_scopes_entry`,\n args: [\n Args.string(request.userDidKey),\n Args.string(request.custodianServicePublicKey),\n Args.string(request.custodianServiceVMType),\n Args.vec('string', finalScopes),\n ],\n maxGas: options?.advanced?.maxGas || 100000000,\n });\n\n this.debugLog('Creating DID via CADOP with scopes:', finalScopes);\n\n this.debugLog('Creating DID via CADOP Transaction:', transaction);\n\n // Execute transaction\n const result = await this.client.signAndExecuteTransaction({\n transaction,\n signer: didAccountSigner,\n option: { withOutput: true },\n });\n\n this.debugLog('Creating DID via CADOP Transaction Result:', result);\n\n const success = result.execution_info.status.type === 'executed';\n\n if (!success) {\n return {\n success: false,\n error:\n 'CADOP transaction execution failed, execution_info: ' +\n JSON.stringify(result.execution_info),\n };\n }\n\n // Parse the created DID\n const didCreatedEvent = result.output?.events?.find(\n (event: any) => event.event_type === '0x3::did::DIDCreatedEvent'\n );\n if (!didCreatedEvent) {\n throw new Error('DIDCreatedEvent not found');\n }\n let actualDID = this.parseDIDCreatedEventAndGetDID(didCreatedEvent);\n let didDocument = await this.resolve(actualDID);\n if (!didDocument) {\n throw new Error('DID document not found with DID: ' + actualDID);\n }\n return {\n success: true,\n didDocument: didDocument,\n transactionHash: (result as any).transaction_hash,\n };\n } catch (error) {\n this.errorLog('Error creating DID via CADOP:', error);\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Unknown error',\n };\n }\n }\n\n /**\n * Resolve DID Document from Rooch blockchain\n *\n * @param did The DID to resolve (e.g., \"did:rooch:0x123...\")\n * @returns Promise resolving to the DID Document or null if not found\n */\n async resolve(did: string): Promise<DIDDocument | null> {\n try {\n this.validateDIDMethod(did);\n\n // Extract address from did:rooch:address format\n const { method, identifier } = parseDid(did);\n if (method !== 'rooch') {\n throw new Error('Invalid DID format. Expected did:rooch:address');\n }\n\n // Calculate Object ID from identifier\n const objectId = resolveDidObjectID(identifier);\n this.debugLog(`Resolved DID object ID: ${objectId}`);\n const objectStates = await this.client.getObjectStates({\n ids: [objectId],\n });\n\n if (!objectStates || objectStates.length === 0) {\n return null;\n }\n\n let didDocObject: ObjectStateView = objectStates[0];\n if (!didDocObject) {\n this.debugLog(`Resolved DID document by ${did} is null`);\n return null;\n }\n this.debugLog(`Resolved DID document Move Object:`, JSON.stringify(didDocObject, null, 2));\n return convertMoveDIDDocumentToInterface(didDocObject);\n } catch (error) {\n this.errorLog(`Error resolving DID from Rooch network:`, error);\n return null;\n }\n }\n\n /**\n * Check if a DID exists on the Rooch network\n *\n * @param did The DID to check\n * @returns Promise resolving to true if the DID exists\n */\n async exists(did: string): Promise<boolean> {\n try {\n this.validateDIDMethod(did);\n\n // Extract address from did:rooch:address format\n const { method, identifier } = parseDid(did);\n if (method !== 'rooch') {\n return false;\n }\n\n const address = identifier;\n\n // Call DID contract's exists_did_for_address view function on Rooch network\n const result = await this.client.executeViewFunction({\n target: `${this.didContractAddress}::exists_did_for_address`,\n args: [Args.address(address)],\n });\n\n return result?.vm_status === 'Executed' && result.return_values?.[0]?.decoded_value === true;\n } catch (error) {\n this.errorLog(`Error checking DID existence on Rooch network:`, error);\n return false;\n }\n }\n\n /**\n * Add a verification method to a DID document on Rooch blockchain\n */\n async addVerificationMethod(\n did: string,\n verificationMethod: VerificationMethod,\n relationships?: VerificationRelationship[],\n options?: RoochVDROperationOptions\n ): Promise<boolean> {\n try {\n this.validateDIDMethod(did);\n\n const signer = options?.signer;\n if (!signer) {\n throw new Error('No signer provided for addVerificationMethod operation');\n }\n\n const didAccountSigner = await this.convertSigner(signer, options?.keyId);\n\n // Pre-validate permissions by resolving the DID document\n const currentDoc = await this.resolve(did);\n if (!currentDoc) {\n throw new Error(`DID document ${did} not found`);\n }\n\n this.debugLog(`Adding verification method to DID: ${did}`);\n this.debugLog(\n `Using signer with address: ${didAccountSigner.getRoochAddress().toBech32Address()}`\n );\n\n // Check if signer has capabilityDelegation permission\n const signerAddress = didAccountSigner.getRoochAddress\n ? didAccountSigner.getRoochAddress().toBech32Address()\n : null;\n if (\n signerAddress &&\n !this.hasPermissionForOperation(currentDoc, signerAddress, 'capabilityDelegation')\n ) {\n this.errorLog(`Signer does not have capabilityDelegation permission for ${did}`);\n this.debugLog(\n `Note: DID operations may require the DID account itself to sign, not the controller`\n );\n return false;\n }\n\n // Validate verification method\n if (!verificationMethod.publicKeyMultibase) {\n throw new Error('Verification method must have publicKeyMultibase');\n }\n\n // Convert verification relationships to u8 values\n const relationshipValues = this.convertVerificationRelationships(relationships || []);\n\n // Check if we need to use scopes version for authentication relationship\n const hasAuthentication = relationships?.includes('authentication');\n\n // Create transaction\n const transaction = this.createTransaction();\n\n if (hasAuthentication) {\n // When adding authentication VM, we need to handle scopes\n const finalScopes = combineScopes(options?.scopes || []);\n\n // Validate all scopes\n const scopeValidation = validateScopes(finalScopes);\n if (!scopeValidation.valid) {\n throw new Error(`Invalid scope format: ${scopeValidation.invalidScopes.join(', ')}`);\n }\n\n // Use the scopes version for authentication VM\n transaction.callFunction({\n target: `${this.didContractAddress}::add_verification_method_with_scopes_entry`,\n args: [\n Args.string(extractFragmentFromId(verificationMethod.id)),\n Args.string(verificationMethod.type),\n Args.string(verificationMethod.publicKeyMultibase),\n Args.vec('u8', relationshipValues),\n Args.vec('string', finalScopes),\n ],\n maxGas: options?.advanced?.maxGas || 100000000,\n });\n\n this.debugLog('Using add_verification_method_with_scopes_entry with scopes:', finalScopes);\n } else {\n // Use regular version for non-authentication VM\n transaction.callFunction({\n target: `${this.didContractAddress}::add_verification_method_entry`,\n args: [\n Args.string(extractFragmentFromId(verificationMethod.id)),\n Args.string(verificationMethod.type),\n Args.string(verificationMethod.publicKeyMultibase),\n Args.vec('u8', relationshipValues),\n ],\n maxGas: options?.advanced?.maxGas || 100000000,\n });\n\n this.debugLog(\n 'Using regular add_verification_method_entry (no authentication relationship)'\n );\n }\n\n this.debugLog(`Verification method transaction prepared`);\n this.debugLog(`Fragment:`, extractFragmentFromId(verificationMethod.id));\n this.debugLog(`Type:`, verificationMethod.type);\n this.debugLog(`Relationships:`, relationshipValues);\n\n // Execute transaction\n const result = await this.client.signAndExecuteTransaction({\n transaction,\n signer: didAccountSigner,\n option: { withOutput: true },\n });\n\n this.debugLog(`Transaction execution result:`, {\n status: result.execution_info.status,\n gas_used: result.execution_info.gas_used,\n events_count: result.output?.events?.length || 0,\n });\n\n if (result.execution_info.status.type !== 'executed') {\n this.errorLog(`Transaction failed:`, result.execution_info);\n if (result.execution_info.status.type === 'moveabort') {\n this.errorLog(`Move abort code:`, (result.execution_info.status as any).abort_code);\n this.errorLog(`Move abort location:`, (result.execution_info.status as any).location);\n }\n return false;\n }\n\n this.debugLog(`Verification method added successfully`);\n return true;\n } catch (error) {\n this.errorLog(`Error adding verification method to ${did}:`, error);\n return false;\n }\n }\n\n /**\n * Remove a verification method from a DID document on Rooch blockchain\n */\n async removeVerificationMethod(\n did: string,\n id: string,\n options?: RoochVDROperationOptions\n ): Promise<boolean> {\n try {\n this.validateDIDMethod(did);\n\n const signer = options?.signer;\n if (!signer) {\n throw new Error('No signer provided for removeVerificationMethod operation');\n }\n\n const didAccountSigner = await this.convertSigner(signer, options?.keyId);\n\n // Pre-validate permissions by resolving the DID document\n const currentDoc = await this.resolve(did);\n if (!currentDoc) {\n throw new Error(`DID document ${did} not found`);\n }\n\n // Check if signer has capabilityDelegation permission\n const signerAddress = didAccountSigner.getRoochAddress\n ? didAccountSigner.getRoochAddress().toBech32Address()\n : null;\n if (\n signerAddress &&\n !this.hasPermissionForOperation(currentDoc, signerAddress, 'capabilityDelegation')\n ) {\n this.errorLog(`Signer does not have capabilityDelegation permission for ${did}`);\n return false;\n }\n\n // Create transaction\n const transaction = this.createTransaction();\n transaction.callFunction({\n target: `${this.didContractAddress}::remove_verification_method_entry`,\n args: [Args.string(extractFragmentFromId(id))],\n maxGas: options?.advanced?.maxGas || 100000000,\n });\n\n // Execute transaction\n const result = await this.client.signAndExecuteTransaction({\n transaction,\n signer: didAccountSigner,\n option: { withOutput: true },\n });\n\n return result.execution_info.status.type === 'executed';\n } catch (error) {\n this.errorLog(`Error removing verification method from ${did}:`, error);\n return false;\n }\n }\n\n /**\n * Add a service to a DID document on Rooch blockchain\n */\n async addService(\n did: string,\n service: ServiceEndpoint,\n options?: RoochVDROperationOptions\n ): Promise<boolean> {\n try {\n this.validateDIDMethod(did);\n\n const signer = options?.signer;\n if (!signer) {\n throw new Error('No signer provided for addService operation');\n }\n\n const didAccountSigner = await this.convertSigner(signer, options?.keyId);\n // Pre-validate permissions by resolving the DID document\n const currentDoc = await this.resolve(did);\n if (!currentDoc) {\n throw new Error(`DID document ${did} not found`);\n }\n\n this.debugLog(`Adding service to DID: ${did}`);\n this.debugLog(\n `🗝️ Using signer with address: ${didAccountSigner.getRoochAddress().toBech32Address()}`\n );\n\n // Check if signer has capabilityInvocation permission\n const signerAddress = didAccountSigner.getRoochAddress\n ? didAccountSigner.getRoochAddress().toBech32Address()\n : null;\n if (\n signerAddress &&\n !this.hasPermissionForOperation(currentDoc, signerAddress, 'capabilityInvocation')\n ) {\n this.errorLog(`Signer does not have capabilityInvocation permission for ${did}`);\n this.debugLog(\n `💡 Note: DID operations may require the DID account itself to sign, not the controller`\n );\n return false;\n }\n\n const standardKeys = ['id', 'type', 'serviceEndpoint'];\n const additionalProperties = Object.entries(service).reduce(\n (acc, [key, value]) => {\n if (!standardKeys.includes(key)) {\n acc[key] = value ? value.toString() : '';\n }\n return acc;\n },\n {} as Record<string, string>\n );\n\n const propertyKeys = Object.keys(additionalProperties);\n const propertyValues = Object.values(additionalProperties).map(value =>\n value ? value.toString() : ''\n );\n this.debugLog('service', service);\n this.debugLog('additionalProperties', additionalProperties);\n this.debugLog('propertyKeys', propertyKeys);\n this.debugLog('propertyValues', propertyValues);\n\n // Create transaction for simple service (without properties)\n const transaction = this.createTransaction();\n transaction.callFunction({\n target: `${this.didContractAddress}::add_service_with_properties_entry`,\n args: [\n Args.string(extractFragmentFromId(service.id)),\n Args.string(service.type),\n Args.string(service.serviceEndpoint),\n Args.vec('string', propertyKeys),\n Args.vec('string', propertyValues),\n ],\n maxGas: options?.advanced?.maxGas || 100000000,\n });\n\n this.debugLog('Executing transaction: add_service_entry');\n this.debugLog('Args:', [\n extractFragmentFromId(service.id),\n service.type,\n service.serviceEndpoint,\n ]);\n\n // Execute transaction\n const result = await this.client.signAndExecuteTransaction({\n transaction,\n signer: didAccountSigner,\n option: { withOutput: true },\n });\n\n this.debugLog('Transaction execution result:', {\n status: result.execution_info.status,\n gas_used: result.execution_info.gas_used,\n events_count: result.output?.events?.length || 0,\n });\n\n if (result.execution_info.status.type !== 'executed') {\n this.errorLog('Transaction failed:', result.execution_info);\n if (result.execution_info.status.type === 'moveabort') {\n this.errorLog('Move abort code:', (result.execution_info.status as any).abort_code);\n this.errorLog('Move abort location:', (result.execution_info.status as any).location);\n }\n return false;\n }\n\n this.debugLog('Service added successfully');\n return true;\n } catch (error) {\n this.errorLog(`Error adding service to ${did}:`, error);\n return false;\n }\n }\n\n /**\n * Add a service with properties to a DID document on Rooch blockchain\n */\n async addServiceWithProperties(\n did: string,\n service: ServiceEndpoint & { properties?: Record<string, string> },\n options?: RoochVDROperationOptions\n ): Promise<boolean> {\n try {\n this.validateDIDMethod(did);\n\n const signer = options?.signer;\n if (!signer) {\n throw new Error('No signer provided for addServiceWithProperties operation');\n }\n\n const didAccountSigner = await this.convertSigner(signer, options?.keyId);\n\n // Pre-validate permissions by resolving the DID document\n const currentDoc = await this.resolve(did);\n if (!currentDoc) {\n throw new Error(`DID document ${did} not found`);\n }\n\n // Check if signer has capabilityInvocation permission\n const signerAddress = didAccountSigner.getRoochAddress\n ? didAccountSigner.getRoochAddress().toBech32Address()\n : null;\n if (\n signerAddress &&\n !this.hasPermissionForOperation(currentDoc, signerAddress, 'capabilityInvocation')\n ) {\n this.errorLog(`Signer does not have capabilityInvocation permission for ${did}`);\n return false;\n }\n\n const properties = service.properties || {};\n const propertyKeys = Object.keys(properties);\n const propertyValues = Object.values(properties);\n\n // Create transaction for service with properties\n const transaction = this.createTransaction();\n transaction.callFunction({\n target: `${this.didContractAddress}::add_service_with_properties_entry`,\n args: [\n Args.string(extractFragmentFromId(service.id)),\n Args.string(service.type),\n Args.string(service.serviceEndpoint),\n Args.vec('string', propertyKeys),\n Args.vec('string', propertyValues),\n ],\n maxGas: options?.advanced?.maxGas || 100000000,\n });\n\n // Execute transaction\n const result = await this.client.signAndExecuteTransaction({\n transaction,\n signer: didAccountSigner,\n option: { withOutput: true },\n });\n\n return result.execution_info.status.type === 'executed';\n } catch (error) {\n this.errorLog(`Error adding service with properties to ${did}:`, error);\n return false;\n }\n }\n\n /**\n * Remove a service from a DID document on Rooch blockchain\n */\n async removeService(\n did: string,\n id: string,\n options?: RoochVDROperationOptions\n ): Promise<boolean> {\n try {\n this.validateDIDMethod(did);\n\n const signer = options?.signer;\n if (!signer) {\n throw new Error('No signer provided for removeService operation');\n }\n\n const didAccountSigner = await this.convertSigner(signer, options?.keyId);\n\n // Pre-validate permissions by resolving the DID document\n const currentDoc = await this.resolve(did);\n if (!currentDoc) {\n throw new Error(`DID document ${did} not found`);\n }\n\n // Check if signer has capabilityInvocation permission\n const signerAddress = didAccountSigner.getRoochAddress().toBech32Address();\n if (\n signerAddress &&\n !this.hasPermissionForOperation(currentDoc, signerAddress, 'capabilityInvocation')\n ) {\n this.errorLog(`Signer does not have capabilityInvocation permission for ${did}`);\n return false;\n }\n\n // Create transaction\n const transaction = this.createTransaction();\n transaction.callFunction({\n target: `${this.didContractAddress}::remove_service_entry`,\n args: [Args.string(extractFragmentFromId(id))],\n maxGas: options?.advanced?.maxGas || 100000000,\n });\n\n // Execute transaction\n const result = await this.client.signAndExecuteTransaction({\n transaction,\n signer: didAccountSigner,\n option: { withOutput: true },\n });\n\n return result.execution_info.status.type === 'executed';\n } catch (error) {\n this.errorLog(`Error removing service from ${did}:`, error);\n return false;\n }\n }\n\n /**\n * Update verification relationships for a verification method on Rooch blockchain\n */\n async updateRelationships(\n did: string,\n id: string,\n add: VerificationRelationship[],\n remove: VerificationRelationship[],\n options?: RoochVDROperationOptions\n ): Promise<boolean> {\n try {\n this.validateDIDMethod(did);\n\n const signer = options?.signer;\n if (!signer) {\n throw new Error('No signer provided for updateRelationships operation');\n }\n\n const didAccountSigner = await this.convertSigner(signer, options?.keyId);\n\n // Pre-validate permissions by resolving the DID document\n const currentDoc = await this.resolve(did);\n if (!currentDoc) {\n throw new Error(`DID document ${did} not found`);\n }\n\n // Check if signer has capabilityDelegation permission\n const signerAddress = didAccountSigner.getRoochAddress().toBech32Address();\n if (\n signerAddress &&\n !this.hasPermissionForOperation(currentDoc, signerAddress, 'capabilityDelegation')\n ) {\n this.errorLog(`Signer does not have capabilityDelegation permission for ${did}`);\n return false;\n }\n\n const fragment = extractFragmentFromId(id);\n\n // Add relationships\n for (const relationship of add) {\n const relationshipValue = this.convertVerificationRelationship(relationship);\n const transaction = this.createTransaction();\n transaction.callFunction({\n target: `${this.didContractAddress}::add_to_verification_relationship_entry`,\n args: [Args.string(fragment), Args.u8(relationshipValue)],\n maxGas: options?.advanced?.maxGas || 100000000,\n });\n\n const result = await this.client.signAndExecuteTransaction({\n transaction,\n signer: didAccountSigner,\n option: { withOutput: true },\n });\n\n if (result.execution_info.status.type !== 'executed') {\n return false;\n }\n }\n\n // Remove relationships\n for (const relationship of remove) {\n const relationshipValue = this.convertVerificationRelationship(relationship);\n const transaction = this.createTransaction();\n transaction.callFunction({\n target: `${this.didContractAddress}::remove_from_verification_relationship_entry`,\n args: [Args.string(fragment), Args.u8(relationshipValue)],\n maxGas: options?.advanced?.maxGas || 100000000,\n });\n\n const result = await this.client.signAndExecuteTransaction({\n transaction,\n signer: didAccountSigner,\n option: { withOutput: true },\n });\n\n if (result.execution_info.status.type !== 'executed') {\n return false;\n }\n }\n\n return true;\n } catch (error) {\n this.errorLog(`Error updating relationships for ${did}:`, error);\n return false;\n }\n }\n\n /**\n * Create a new Rooch transaction instance\n */\n private createTransaction(): Transaction {\n return new Transaction();\n }\n\n /**\n * Convert verification relationships to u8 values based on did.move constants\n */\n private convertVerificationRelationships(relationships: VerificationRelationship[]): number[] {\n return relationships.map(rel => this.convertVerificationRelationship(rel));\n }\n\n /**\n * Convert a single verification relationship to u8 value\n */\n private convertVerificationRelationship(relationship: VerificationRelationship): number {\n switch (relationship) {\n case 'authentication':\n re