UNPKG

@keypo/synapse-storage-sdk

Version:
407 lines (406 loc) 19.9 kB
/** * Smart contract management for Synapse Storage SDK */ import { Contract, ZeroAddress } from 'ethers'; import { encodeFunctionData } from 'viem'; import { PermissionsRegistryAbi } from './abis.js'; import { createContractError } from '../../errors/index.js'; import { deployPermissionedData } from './deployPermissionedData.js'; import { mintOwnerNFT } from './mintOwnerNFT.js'; import { RETRY_CONFIG, calculateBackoffDelay } from '../../constants/index.js'; export class ContractManager { config; constructor(config) { this.config = config; } /** * Deploy permissioned data contract and optionally mint NFT */ async deployPermissionsAndMintNFT(dataIdentifier, metadata, kernelClient, userAddress, isPublic = false, debug) { try { // Create custom parameters based on public/private access const customParameters = [{ permissionType: 0, permissionAddress: userAddress, tokenQuantity: isPublic ? 0 : 1, // 0 for public (anyone can access), 1 for private (NFT required) timeLimitBlockNumber: 0, operator: 0, }]; // Deploy the permissioned data if (debug) { console.log(`🚀 Deploying ${isPublic ? 'public' : 'private'} permission contract...`); } const deployTxHash = await deployPermissionedData(dataIdentifier, JSON.stringify(metadata), kernelClient, userAddress, this.config.registryAddress, this.config.validationAddress, PermissionsRegistryAbi, customParameters, debug); if (debug) { console.log('✅ Permission contract deployed'); } // Only mint NFT for private files if (!isPublic) { if (debug) { console.log('🎫 Minting owner NFT...'); } await mintOwnerNFT(kernelClient, this.config.registryAddress, dataIdentifier, PermissionsRegistryAbi, debug); if (debug) { console.log('✅ Owner NFT minted'); } } else if (debug) { console.log('📢 Public file - no NFT needed (anyone can decrypt)'); } return deployTxHash; } catch (error) { if (debug) { console.error('❌ Smart contract operation failed:', error); } throw error; // Re-throw so caller can handle appropriately } } /** * Mint NFT for file owner */ async mintOwnerNFT(dataIdentifier, kernelClient) { try { const registryContract = new Contract(this.config.registryAddress, PermissionsRegistryAbi, kernelClient); const tx = await registryContract.mintFileNFT(dataIdentifier, 1); await tx.wait(); } catch (error) { throw createContractError('Failed to mint owner NFT', { cause: error, userMessage: 'Could not mint NFT for file ownership' }); } } /** * Share file with another user */ async shareFile(dataIdentifier, recipientAddress, _quantity = 1, // Keep for backward compatibility but not used kernelClient, // ZeroDev kernel client for account abstraction debug = false) { try { if (debug) { console.log(`[DEBUG] ShareFile - Registry Contract: ${this.config.registryAddress}`); console.log(`[DEBUG] ShareFile - Data Identifier: ${dataIdentifier}`); console.log(`[DEBUG] ShareFile - Recipient: ${recipientAddress}`); console.log(`[DEBUG] ShareFile - Kernel Client Address: ${kernelClient.account.address}`); } // Encode the function data using viem const txData = encodeFunctionData({ abi: PermissionsRegistryAbi, functionName: "mintFromPermissionedFileForOwner", args: [dataIdentifier, [recipientAddress]] // Function expects array of addresses }); if (debug) { console.log(`[DEBUG] ShareFile - Encoded transaction data: ${txData}`); } // Retry logic similar to mintOwnerNFT const retryAttempts = RETRY_CONFIG.DEFAULT_ATTEMPTS; const retryDelay = RETRY_CONFIG.BASE_DELAY_MS; let lastError; for (let attempt = 1; attempt <= retryAttempts; attempt++) { try { if (debug && attempt > 1) { console.log(`[DEBUG] ShareFile - Retry attempt ${attempt}/${retryAttempts}`); } // Add exponential backoff delay between retry attempts if (attempt > 1) { const backoffDelay = calculateBackoffDelay(attempt - 1, retryDelay); if (debug) { console.log(`[DEBUG] ShareFile - Waiting ${backoffDelay}ms before retry attempt ${attempt}`); } await new Promise(resolve => setTimeout(resolve, backoffDelay)); } // Prepare the user operation const userOperation = { callData: await kernelClient.account.encodeCalls([{ to: this.config.registryAddress, value: BigInt(0), data: txData, }]), maxFeePerGas: undefined, // Let the bundler estimate maxPriorityFeePerGas: undefined, // Let the bundler estimate }; if (debug) { console.log(`[DEBUG] ShareFile - Sending user operation with callData: ${userOperation.callData}`); } // Send user operation with timeout const userOpHash = await Promise.race([ kernelClient.sendUserOperation(userOperation), new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout: ShareFile sendUserOperation took too long')), 30000)) ]); if (debug) { console.log(`[DEBUG] ShareFile - User operation hash: ${userOpHash}`); console.log(`[DEBUG] ShareFile - Waiting for confirmation...`); } // Wait for receipt with timeout const { receipt } = await Promise.race([ kernelClient.waitForUserOperationReceipt({ hash: userOpHash, }), new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout: ShareFile waitForUserOperationReceipt took too long')), 60000)) ]); if (debug) { console.log(`[DEBUG] ShareFile - Transaction confirmed in block: ${receipt.blockNumber}`); console.log(`[DEBUG] ShareFile - Gas used: ${receipt.gasUsed?.toString()}`); console.log(`[DEBUG] ShareFile - Transaction hash: ${receipt.transactionHash}`); console.log(`[DEBUG] ShareFile - Successful on attempt ${attempt}`); } // Success - exit retry loop return; } catch (error) { lastError = error; console.error(`[DEBUG] ShareFile - Error on attempt ${attempt}/${retryAttempts}:`, error.message); // Check for specific error types if (error.message && error.message.includes("UserOperation reverted during simulation")) { console.error("[DEBUG] ShareFile - UserOperation simulation failed - this could be due to:"); console.error("1. Insufficient permissions - user may not own this file"); console.error("2. Invalid recipient address"); console.error("3. File may not exist or be accessible"); console.error("4. Network congestion or gas estimation issues"); } // If this is not the last attempt, continue to retry if (attempt < retryAttempts) { if (debug) { console.log(`[DEBUG] ShareFile - Will retry in ${retryDelay}ms...`); } continue; } // If all attempts failed, break and throw error break; } } // If we get here, all attempts failed throw lastError; } catch (error) { // Extract detailed error information let errorDetails = { contract: this.config.registryAddress, dataIdentifier, recipientAddress }; // Add specific error details based on error type if (error.code) { errorDetails.errorCode = error.code; } if (error.reason) { errorDetails.reason = error.reason; } if (error.data) { errorDetails.contractError = error.data; } if (error.transaction) { errorDetails.transaction = error.transaction; } if (debug) { console.error(`[DEBUG] ShareFile - Error details:`, errorDetails); console.error(`[DEBUG] ShareFile - Full error:`, error); } throw createContractError('Failed to share file', { cause: error, userMessage: `Could not share file with ${recipientAddress}. ${error.reason || error.message || 'Unknown contract error'}`, details: errorDetails }); } } /** * Revoke file access for a user */ async revokeAccess(dataIdentifier, userAddress, kernelClient // ZeroDev kernel client for account abstraction ) { try { // Encode the function data using viem const txData = encodeFunctionData({ abi: PermissionsRegistryAbi, functionName: "revokePermission", args: [dataIdentifier, userAddress] }); // Prepare the user operation const userOperation = { callData: await kernelClient.account.encodeCalls([{ to: this.config.registryAddress, value: BigInt(0), data: txData, }]), maxFeePerGas: undefined, // Let the bundler estimate maxPriorityFeePerGas: undefined, // Let the bundler estimate }; // Send user operation with timeout const userOpHash = await Promise.race([ kernelClient.sendUserOperation(userOperation), new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout: RevokeAccess sendUserOperation took too long')), 30000)) ]); // Wait for receipt with timeout await Promise.race([ kernelClient.waitForUserOperationReceipt({ hash: userOpHash, }), new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout: RevokeAccess waitForUserOperationReceipt took too long')), 60000)) ]); } catch (error) { throw createContractError('Failed to revoke access', { cause: error, userMessage: `Could not revoke access for ${userAddress}` }); } } /** * Check if a user has permission to access a file */ async checkPermission(dataIdentifier, userAddress, provider) { try { const registryContract = new Contract(this.config.registryAddress, PermissionsRegistryAbi, provider); return await registryContract.checkPermission(dataIdentifier, userAddress); } catch (error) { console.warn('Failed to check permission:', error); return false; } } /** * Get file contract address from data identifier */ async getFileContractAddress(dataIdentifier, provider) { try { const registryContract = new Contract(this.config.registryAddress, PermissionsRegistryAbi, provider); const address = await registryContract.fileIdentifierToFileContract(dataIdentifier); return address === ZeroAddress ? null : address; } catch (error) { console.warn('Failed to get file contract address:', error); return null; } } /** * Delete a file from the permissions registry */ async deleteFile(dataIdentifier, kernelClient, // ZeroDev kernel client for account abstraction debug = false) { try { if (debug) { console.log(`[DEBUG] DeleteFile - Registry Contract: ${this.config.registryAddress}`); console.log(`[DEBUG] DeleteFile - Data Identifier: ${dataIdentifier}`); console.log(`[DEBUG] DeleteFile - Kernel Client Address: ${kernelClient.account.address}`); } // Encode the function data using viem const txData = encodeFunctionData({ abi: PermissionsRegistryAbi, functionName: "deletePermissionedFile", args: [dataIdentifier] }); if (debug) { console.log(`[DEBUG] DeleteFile - Encoded transaction data: ${txData}`); } // Retry logic similar to other contract operations const retryAttempts = RETRY_CONFIG.DEFAULT_ATTEMPTS; const retryDelay = RETRY_CONFIG.BASE_DELAY_MS; let lastError; for (let attempt = 1; attempt <= retryAttempts; attempt++) { try { if (debug && attempt > 1) { console.log(`[DEBUG] DeleteFile - Retry attempt ${attempt}/${retryAttempts}`); } // Add exponential backoff delay between retry attempts if (attempt > 1) { const backoffDelay = calculateBackoffDelay(attempt - 1, retryDelay); if (debug) { console.log(`[DEBUG] DeleteFile - Waiting ${backoffDelay}ms before retry attempt ${attempt}`); } await new Promise(resolve => setTimeout(resolve, backoffDelay)); } // Prepare the user operation const userOperation = { callData: await kernelClient.account.encodeCalls([{ to: this.config.registryAddress, value: BigInt(0), data: txData, }]), maxFeePerGas: undefined, // Let the bundler estimate maxPriorityFeePerGas: undefined, // Let the bundler estimate }; if (debug) { console.log(`[DEBUG] DeleteFile - Sending user operation with callData: ${userOperation.callData}`); } // Send user operation with timeout const userOpHash = await Promise.race([ kernelClient.sendUserOperation(userOperation), new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout: DeleteFile sendUserOperation took too long')), 30000)) ]); if (debug) { console.log(`[DEBUG] DeleteFile - User operation hash: ${userOpHash}`); console.log(`[DEBUG] DeleteFile - Waiting for confirmation...`); } // Wait for receipt with timeout const { receipt } = await Promise.race([ kernelClient.waitForUserOperationReceipt({ hash: userOpHash, }), new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout: DeleteFile waitForUserOperationReceipt took too long')), 60000)) ]); if (debug) { console.log(`[DEBUG] DeleteFile - Transaction confirmed in block: ${receipt.blockNumber}`); console.log(`[DEBUG] DeleteFile - Gas used: ${receipt.gasUsed?.toString()}`); console.log(`[DEBUG] DeleteFile - Transaction hash: ${receipt.transactionHash}`); console.log(`[DEBUG] DeleteFile - Successful on attempt ${attempt}`); } // Success - return transaction hash return receipt.transactionHash; } catch (error) { lastError = error; console.error(`[DEBUG] DeleteFile - Error on attempt ${attempt}/${retryAttempts}:`, error.message); // Check for specific error types if (error.message && error.message.includes("UserOperation reverted during simulation")) { console.error("[DEBUG] DeleteFile - UserOperation simulation failed - this could be due to:"); console.error("1. Insufficient permissions - user may not own this file"); console.error("2. File may not exist or already be deleted"); console.error("3. Network congestion or gas estimation issues"); } // If this is not the last attempt, continue to retry if (attempt < retryAttempts) { if (debug) { console.log(`[DEBUG] DeleteFile - Will retry in ${retryDelay}ms...`); } continue; } // If all attempts failed, break and throw error break; } } // If we get here, all attempts failed throw lastError; } catch (error) { // Extract detailed error information let errorDetails = { contract: this.config.registryAddress, dataIdentifier }; // Add specific error details based on error type if (error.code) { errorDetails.errorCode = error.code; } if (error.reason) { errorDetails.reason = error.reason; } if (error.data) { errorDetails.contractError = error.data; } if (error.transaction) { errorDetails.transaction = error.transaction; } if (debug) { console.error(`[DEBUG] DeleteFile - Error details:`, errorDetails); console.error(`[DEBUG] DeleteFile - Full error:`, error); } throw createContractError('Failed to delete file', { cause: error, userMessage: `Could not delete file with data identifier ${dataIdentifier}. ${error.reason || error.message || 'Unknown contract error'}`, details: errorDetails }); } } }