UNPKG

defarm-sdk

Version:

DeFarm SDK - On-premise blockchain data processing and tokenization engine for agriculture supply chain

978 lines (873 loc) 28.8 kB
import { EventEmitter } from 'events'; import type { DeFarmConfig, Asset, AssetQuery, ProcessingResult, DataProcessor, EventHandler, DeFarmEvent, StorageAdapter, ExportOptions, } from '../types'; import { LivestockProcessor } from '../processors/LivestockProcessor'; import { CropsProcessor } from '../processors/CropsProcessor'; import { ValidationService } from './ValidationService'; import { BlockchainService } from './BlockchainService'; import { PremiumAPIClient } from './PremiumAPIClient'; import { TokenizationService } from './TokenizationService'; import type { TokenizationConfig, TokenizationResult } from './TokenizationService'; import { version } from '../../package.json'; import { Logger, LogLevel } from '../utils/logger'; import { DeFarmSDKError, ErrorCode, ValidationError, StorageError, ProcessingError } from '../errors'; import { Validator } from '../validation'; import { withRetry } from '../utils/retry'; import { MiddlewarePipeline, requestLoggerMiddleware, requestIdMiddleware, Middleware } from '../middleware'; import { RateLimiter } from '../utils/rate-limiter'; /** * Main SDK class for DeFarm on-premise processing */ export class DeFarmSDK extends EventEmitter { private config: DeFarmConfig; private storage: StorageAdapter | null = null; private processors: Map<string, DataProcessor> = new Map(); private validationService: ValidationService; private blockchainService?: BlockchainService; private premiumClient?: PremiumAPIClient; private tokenizationService?: TokenizationService; private initialized = false; private logger: Logger; private middleware: MiddlewarePipeline; private rateLimiter?: RateLimiter; constructor(config: DeFarmConfig) { super(); // Validate configuration const validation = Validator.validateConfig(config); if (!validation.success) { throw new ValidationError( `Invalid SDK configuration: ${validation.errors?.join(', ')}`, validation.errors ); } this.config = config; this.validationService = new ValidationService(config); // Initialize logger this.logger = new Logger({ level: config.environment === 'production' ? LogLevel.INFO : LogLevel.DEBUG, prefix: 'DeFarmSDK' }); // Initialize middleware pipeline this.middleware = new MiddlewarePipeline(); this.middleware.use(requestIdMiddleware()); this.middleware.use(requestLoggerMiddleware(this.logger)); // Initialize rate limiter if configured if (config.processing?.rateLimit) { this.rateLimiter = new RateLimiter({ maxRequests: config.processing.rateLimit.maxRequests || 100, windowMs: config.processing.rateLimit.windowMs || 60000 }); } // Initialize default processors this.registerProcessor('livestock', new LivestockProcessor()); this.registerProcessor('crops', new CropsProcessor()); // Initialize blockchain service if enabled if (config.blockchain?.enabled) { this.blockchainService = new BlockchainService(config.blockchain); } // Initialize premium API client if configured if (config.premiumAPI) { this.premiumClient = new PremiumAPIClient(config.premiumAPI); } // Initialize tokenization service if configured if (config.tokenization) { this.tokenizationService = new TokenizationService(config.tokenization); } this.logger.info('SDK initialized', { version, environment: config.environment }); } /** * Initialize the SDK with storage connection */ async initialize(): Promise<void> { if (this.initialized) { throw new DeFarmSDKError( ErrorCode.ALREADY_INITIALIZED, 'SDK already initialized' ); } this.logger.debug('Starting SDK initialization'); try { // Connect to storage with retry if (this.config.storage.customAdapter) { this.storage = this.config.storage.customAdapter; } else { throw new StorageError( 'Built-in storage adapters not yet implemented. Please provide customAdapter.' ); } await withRetry( () => this.storage!.connect(), { maxRetries: this.config.processing.maxRetries, onRetry: (error, attempt) => { this.logger.warn(`Storage connection failed, retrying (${attempt + 1})`, error); } } ); this.logger.info('Storage connected successfully'); // Initialize blockchain service if configured if (this.blockchainService) { await this.blockchainService.initialize(); this.logger.info('Blockchain service initialized'); } // Initialize tokenization service if configured if (this.tokenizationService) { await this.tokenizationService.initialize(); this.logger.info('Tokenization service initialized'); } this.initialized = true; this.emit('initialized', { version, config: this.getPublicConfig() }); this.logger.info('SDK initialization complete'); } catch (error) { this.logger.error('SDK initialization failed', error); this.emit('error', { type: 'initialization', error }); throw error; } } /** * Register a custom data processor */ registerProcessor(category: string, processor: DataProcessor): void { this.processors.set(category, processor); this.emit('processor.registered', { category, processor: processor.name }); } /** * Process a single asset */ async processAsset(asset: Asset): Promise<Asset> { this.assertInitialized(); // Check rate limit if enabled if (this.rateLimiter) { const rateLimitResult = await this.rateLimiter.checkLimit({ operation: 'processAsset' }); if (!rateLimitResult.allowed) { throw new DeFarmSDKError( ErrorCode.RATE_LIMIT_EXCEEDED, `Rate limit exceeded. Retry after ${rateLimitResult.resetAt.toISOString()}` ); } } const startTime = Date.now(); this.emitEvent('processing.started', { assetId: asset.id, category: asset.category }); this.logger.debug('Processing asset', { id: asset.id, category: asset.category, name: asset.name }); try { // Enhanced validation with better error messages const validationResult = Validator.validateAsset(asset); if (!validationResult.success) { throw new ValidationError( `Asset validation failed: ${validationResult.errors?.join(', ')}`, validationResult.errors ); } // Additional business logic validation const businessValidation = await this.validationService.validateAsset(asset); if (!businessValidation.valid) { throw new ValidationError( 'Business rule validation failed', businessValidation.errors ); } // Get appropriate processor const processor = this.processors.get(asset.category); if (!processor) { throw new ProcessingError( `No processor found for category: ${asset.category}` ); } // Process asset with retry let processedAsset = await withRetry( () => processor.process(asset), { maxRetries: this.config.processing.maxRetries, shouldRetry: (error) => { // Don't retry validation errors return !(error instanceof ValidationError); } } ); // Generate DFID if not present if (!processedAsset.dfid) { processedAsset.dfid = this.generateDFID(processedAsset); } // Save to storage with retry const savedId = await withRetry( () => this.storage!.saveAsset(processedAsset), { maxRetries: this.config.processing.maxRetries, onRetry: (error, attempt) => { this.logger.warn(`Storage save failed, retrying (${attempt + 1})`, error); } } ); processedAsset.id = savedId; // Submit to blockchain if enabled if (this.config.blockchain?.enabled && this.blockchainService) { try { const blockchainResult = await this.blockchainService.submitAsset(processedAsset); processedAsset.blockchain = blockchainResult; } catch (error) { this.logger.error('Blockchain submission failed', error); // Don't fail the entire operation if blockchain fails } } const duration = Date.now() - startTime; this.emitEvent('processing.completed', { assetId: savedId, category: asset.category, duration }); this.logger.info('Asset processed successfully', { id: savedId, dfid: processedAsset.dfid, duration }); return processedAsset; } catch (error) { const duration = Date.now() - startTime; this.logger.error('Asset processing failed', { error, asset: asset.id, duration }); this.emitEvent('error', { type: 'processing', assetId: asset.id, error: error instanceof Error ? error.message : 'Unknown error' }); throw error; } } /** * Process multiple assets in batch */ async processBatch(assets: Asset[]): Promise<ProcessingResult> { this.assertInitialized(); const startTime = Date.now(); const errors: any[] = []; let processedCount = 0; // Process in configured batch sizes const batchSize = this.config.processing.batchSize; const batches = this.chunkArray(assets, batchSize); for (const batch of batches) { const transaction = await this.storage!.beginTransaction(); try { const processedAssets = await Promise.all( batch.map(asset => this.processAsset(asset).catch(error => { errors.push({ assetId: asset.id, code: 'PROCESSING_ERROR', message: error.message, details: error }); return null; })) ); processedCount += processedAssets.filter(a => a !== null).length; await transaction.commit(); } catch (error) { await transaction.rollback(); errors.push({ code: 'BATCH_ERROR', message: error instanceof Error ? error.message : 'Unknown error', details: error }); } } return { success: errors.length === 0, processedCount, errors, duration: Date.now() - startTime }; } /** * Query assets from storage */ async queryAssets(query: AssetQuery): Promise<Asset[]> { this.assertInitialized(); return this.storage!.queryAssets(query); } /** * Export assets in specified format */ async exportAssets(options: ExportOptions): Promise<any> { this.assertInitialized(); // Query assets based on filters const assets = await this.queryAssets(options.filters || {}); // Apply date range if specified let filteredAssets = assets; if (options.dateRange) { filteredAssets = assets.filter(asset => { const created = new Date(asset.metadata.created); return created >= options.dateRange!.start && created <= options.dateRange!.end; }); } // Format based on requested format switch (options.format) { case 'json': return this.exportJSON(filteredAssets, options.includeMetadata); case 'csv': return this.exportCSV(filteredAssets, options.includeMetadata); case 'xml': return this.exportXML(filteredAssets, options.includeMetadata); case 'custom': if (!options.customFormatter) { throw new Error('Custom formatter not provided'); } return options.customFormatter(filteredAssets); default: throw new Error(`Unsupported export format: ${options.format}`); } } /** * Subscribe to SDK events */ onEvent(handler: EventHandler): void { this.on('sdk.event', handler); } /** * Get SDK version and configuration info */ getInfo() { return { version, initialized: this.initialized, processors: Array.from(this.processors.keys()), blockchain: this.config.blockchain?.enabled ? { enabled: true, network: this.config.blockchain.network } : { enabled: false }, storage: { type: this.config.storage.type, connected: this.initialized }, logging: { level: this.logger ? LogLevel[this.logger.level] : 'INFO' }, rateLimit: this.rateLimiter ? { enabled: true, maxRequests: this.config.processing?.rateLimit?.maxRequests || 100, windowMs: this.config.processing?.rateLimit?.windowMs || 60000 } : { enabled: false } }; } /** * Set log level dynamically */ setLogLevel(level: LogLevel): void { this.logger.setLevel(level); this.logger.info(`Log level changed to ${LogLevel[level]}`); } /** * Get logger instance */ getLogger(): Logger { return this.logger; } /** * Add custom middleware */ use(middleware: Middleware): void { this.middleware.use(middleware); this.logger.debug('Custom middleware added'); } /** * Cleanup and disconnect */ async disconnect(): Promise<void> { if (this.storage) { await this.storage.disconnect(); } if (this.blockchainService) { await this.blockchainService.disconnect(); } this.initialized = false; this.emit('disconnected'); } /** * Get premium API client if configured */ getPremiumClient(): PremiumAPIClient | undefined { return this.premiumClient; } /** * Get tokenization service if configured */ getTokenizationService(): TokenizationService | undefined { return this.tokenizationService; } /** * Tokenize an asset - convert to NFT with IPFS metadata * Enhanced with encryption, DFID generation, and DeFarm notification */ async tokenizeAsset(asset: Asset): Promise<TokenizationResult> { this.assertInitialized(); if (!this.tokenizationService) { return { success: false, dfid: asset.dfid || '', error: 'Tokenization not configured. Please add tokenization config to DeFarmConfig.' }; } this.emitEvent('tokenization.requested', { dfid: asset.dfid, category: asset.category, encrypted: this.config.tokenization?.encryption?.enabled || false }); try { // TokenizationService now handles: // 1. DFID generation via DeFarm API if not present // 2. Optional encryption based on config // 3. IPFS upload via DeFarm gateway // 4. NFT minting with sponsor model // 5. CID mapping on blockchain // 6. Notification to DeFarm with CID/hash only const result = await this.tokenizationService.tokenizeAsset(asset); if (result.success) { this.emitEvent('tokenization.completed', { dfid: result.dfid, tokenId: result.tokenId, transactionHash: result.transactionHash, ipfsCid: result.ipfsCid, walletAddress: result.walletAddress, encrypted: this.config.tokenization?.encryption?.enabled || false }); } else { this.emitEvent('tokenization.failed', { dfid: result.dfid, error: result.error }); } return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown tokenization error'; this.emitEvent('tokenization.error', { dfid: asset.dfid, error: errorMessage }); return { success: false, dfid: asset.dfid || '', error: errorMessage }; } } /** * Tokenize multiple assets in batch */ async tokenizeBatch(assets: Asset[]): Promise<TokenizationResult[]> { this.assertInitialized(); if (!this.tokenizationService) { return assets.map(asset => ({ success: false, dfid: asset.dfid!, error: 'Tokenization not configured' })); } this.emitEvent('tokenization.batch.started', { count: assets.length }); try { const results = await this.tokenizationService.tokenizeBatch(assets); const successCount = results.filter(r => r.success).length; this.emitEvent('tokenization.batch.completed', { total: assets.length, successful: successCount, failed: assets.length - successCount }); return results; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown batch tokenization error'; this.emitEvent('tokenization.batch.error', { error: errorMessage }); return assets.map(asset => ({ success: false, dfid: asset.dfid!, error: errorMessage })); } } /** * Get tokenization status for an asset */ async getTokenizationStatus(dfid: string): Promise<{ isTokenized: boolean; tokenId?: string; contractAddress?: string; ipfsCid?: string; walletAddress?: string; error?: string; }> { this.assertInitialized(); if (!this.tokenizationService) { return { isTokenized: false, error: 'Tokenization not configured' }; } try { return await this.tokenizationService.getTokenizationStatus(dfid); } catch (error) { return { isTokenized: false, error: error instanceof Error ? error.message : 'Unknown status error' }; } } /** * Get wallet information for an asset */ async getWalletInfo(dfid: string): Promise<{ address?: string; balance?: string; network: string; error?: string; } | null> { this.assertInitialized(); if (!this.tokenizationService) { return { network: 'none', error: 'Tokenization not configured' }; } try { return await this.tokenizationService.getWalletInfo(dfid); } catch (error) { return { network: 'unknown', error: error instanceof Error ? error.message : 'Unknown wallet error' }; } } /** * Process and tokenize asset in one operation */ async processAndTokenizeAsset(asset: Asset): Promise<{ processedAsset: Asset; tokenizationResult?: TokenizationResult; error?: string; }> { this.assertInitialized(); try { // First process the asset normally const processedAsset = await this.processAsset(asset); // Then tokenize if tokenization is configured if (this.tokenizationService) { const tokenizationResult = await this.tokenizeAsset(processedAsset); return { processedAsset, tokenizationResult }; } else { return { processedAsset }; } } catch (error) { return { processedAsset: asset, error: error instanceof Error ? error.message : 'Unknown processing error' }; } } // Private helper methods private assertInitialized(): void { if (!this.initialized) { throw new DeFarmSDKError( ErrorCode.NOT_INITIALIZED, 'SDK not initialized. Call initialize() first.' ); } } private generateDFID(asset: Asset): string { // Simple DFID generation - in production would be more sophisticated const timestamp = Date.now().toString(36); const random = Math.random().toString(36).substring(2, 8); const category = asset.category.substring(0, 2).toUpperCase(); return `DF${category}${timestamp}${random}`.toUpperCase(); } private emitEvent(type: string, data: any): void { const event: DeFarmEvent = { type: type as any, timestamp: new Date(), data, source: 'sdk' }; this.emit('sdk.event', event); } /** * Check if encryption is enabled */ isEncryptionEnabled(): boolean { return this.config.tokenization?.encryption?.enabled || false; } /** * Get encryption type */ getEncryptionType(): string | undefined { return this.config.tokenization?.encryption?.type; } /** * Check if tokenization is configured */ isTokenizationEnabled(): boolean { return !!this.config.tokenization && !!this.tokenizationService; } /** * Get SDK version */ getVersion(): string { return version; } /** * Get SDK statistics */ async getSDKStats(): Promise<{ version: string; initialized: boolean; processors: string[]; tokenizationEnabled: boolean; encryptionEnabled: boolean; blockchainEnabled: boolean; }> { return { version, initialized: this.initialized, processors: Array.from(this.processors.keys()), tokenizationEnabled: this.isTokenizationEnabled(), encryptionEnabled: this.isEncryptionEnabled(), blockchainEnabled: this.config.blockchain?.enabled || false }; } /** * Perform health check on all configured services */ async healthCheck(): Promise<{ status: 'healthy' | 'degraded' | 'unhealthy'; services: { storage: { status: boolean; message?: string; latency?: number }; blockchain?: { status: boolean; message?: string; latency?: number }; tokenization?: { status: boolean; message?: string; latency?: number }; premium?: { status: boolean; message?: string; latency?: number }; }; timestamp: Date; }> { this.logger.info('Starting health check'); const results: any = { storage: { status: false }, blockchain: undefined, tokenization: undefined, premium: undefined }; let overallStatus: 'healthy' | 'degraded' | 'unhealthy' = 'healthy'; // Check storage try { const start = Date.now(); if (this.storage) { // Simple connectivity test - try to query with limit 1 await this.storage.queryAssets({ limit: 1 }); results.storage = { status: true, latency: Date.now() - start }; } else { results.storage = { status: false, message: 'Storage not initialized' }; overallStatus = 'unhealthy'; } } catch (error) { results.storage = { status: false, message: error instanceof Error ? error.message : 'Storage health check failed' }; overallStatus = 'unhealthy'; } // Check blockchain if enabled if (this.config.blockchain?.enabled && this.blockchainService) { try { const start = Date.now(); // Assume blockchain service has a health method const isHealthy = await this.blockchainService.isHealthy?.() ?? false; results.blockchain = { status: isHealthy, latency: Date.now() - start }; if (!isHealthy) overallStatus = 'degraded'; } catch (error) { results.blockchain = { status: false, message: error instanceof Error ? error.message : 'Blockchain health check failed' }; overallStatus = 'degraded'; } } // Check tokenization service if (this.tokenizationService) { try { const start = Date.now(); // Test DFID generation endpoint const response = await fetch( `${this.config.tokenization?.defarmServices.baseUrl}/health`, { method: 'GET', headers: { 'X-API-Key': this.config.tokenization?.defarmServices.apiKey || '' }, signal: AbortSignal.timeout(5000) } ); results.tokenization = { status: response.ok, latency: Date.now() - start, message: response.ok ? undefined : `HTTP ${response.status}` }; if (!response.ok) overallStatus = 'degraded'; } catch (error) { results.tokenization = { status: false, message: error instanceof Error ? error.message : 'Tokenization health check failed' }; overallStatus = 'degraded'; } } // Check premium API if (this.premiumClient) { try { const start = Date.now(); const response = await fetch( `${this.config.premiumAPI?.baseUrl}/health`, { method: 'GET', headers: { 'Authorization': `Bearer ${this.config.premiumAPI?.apiKey || ''}` }, signal: AbortSignal.timeout(5000) } ); results.premium = { status: response.ok, latency: Date.now() - start, message: response.ok ? undefined : `HTTP ${response.status}` }; if (!response.ok) overallStatus = 'degraded'; } catch (error) { results.premium = { status: false, message: error instanceof Error ? error.message : 'Premium API health check failed' }; overallStatus = 'degraded'; } } const healthCheckResult = { status: overallStatus, services: results, timestamp: new Date() }; this.logger.info('Health check completed', { status: overallStatus }); return healthCheckResult; } private getPublicConfig(): any { // Return config without sensitive information return { environment: this.config.environment, processing: this.config.processing, storage: { type: this.config.storage.type }, blockchain: this.config.blockchain ? { enabled: this.config.blockchain.enabled, network: this.config.blockchain.network } : undefined, premiumAPI: this.config.premiumAPI ? { baseUrl: this.config.premiumAPI.baseUrl, version: this.config.premiumAPI.version } : undefined, tokenization: this.config.tokenization ? { network: this.config.tokenization.blockchain.network, encryptionEnabled: this.config.tokenization.encryption?.enabled || false, encryptionType: this.config.tokenization.encryption?.type } : undefined, security: { encryptionEnabled: this.config.security.encryptionEnabled, dataClassification: this.config.security.dataClassification } }; } private chunkArray<T>(array: T[], size: number): T[][] { const chunks: T[][] = []; for (let i = 0; i < array.length; i += size) { chunks.push(array.slice(i, i + size)); } return chunks; } private exportJSON(assets: Asset[], includeMetadata: boolean): any { if (!includeMetadata) { return assets.map(({ metadata, blockchain, ...asset }) => asset); } return assets; } private exportCSV(assets: Asset[], includeMetadata: boolean): string { // Simplified CSV export - in production would use proper CSV library const headers = ['id', 'dfid', 'name', 'category', 'subcategory', 'ownerId', 'ownerName']; if (includeMetadata) { headers.push('source', 'fromProducer', 'visibility', 'created'); } const rows = assets.map(asset => { const row = [ asset.id, asset.dfid, asset.name, asset.category, asset.subcategory, asset.ownership.ownerId, asset.ownership.ownerName ]; if (includeMetadata) { row.push( asset.metadata.source, asset.metadata.fromProducer.toString(), asset.metadata.visibility, asset.metadata.created.toISOString() ); } return row.map(v => `"${v || ''}"`).join(','); }); return [headers.join(','), ...rows].join('\n'); } private exportXML(assets: Asset[], includeMetadata: boolean): string { // Simplified XML export - in production would use proper XML library const xmlAssets = assets.map(asset => { const assetXml = ` <asset> <id>${asset.id}</id> <dfid>${asset.dfid}</dfid> <name>${asset.name}</name> <category>${asset.category}</category> <subcategory>${asset.subcategory}</subcategory> <ownership> <ownerId>${asset.ownership.ownerId}</ownerId> <ownerName>${asset.ownership.ownerName}</ownerName> </ownership> ${includeMetadata ? ` <metadata> <source>${asset.metadata.source}</source> <fromProducer>${asset.metadata.fromProducer}</fromProducer> <visibility>${asset.metadata.visibility}</visibility> <created>${asset.metadata.created.toISOString()}</created> </metadata> ` : ''} </asset>`; return assetXml; }).join('\n'); return `<?xml version="1.0" encoding="UTF-8"?> <assets> ${xmlAssets} </assets>`; } }