defarm-sdk
Version:
DeFarm SDK - On-premise blockchain data processing and tokenization engine for agriculture supply chain
978 lines (873 loc) • 28.8 kB
text/typescript
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>`;
}
}