/** * Transport Tracker for LocalGreenChain * Manages seed-to-seed transport events and blockchain recording */ import crypto from 'crypto'; import { TransportEvent, TransportBlock, TransportLocation, TransportMethod, PlantJourney, EnvironmentalImpact, TransportQRData, CARBON_FACTORS, SeedAcquisitionEvent, PlantingEvent, GrowingTransportEvent, HarvestEvent, ProcessingEvent, DistributionEvent, ConsumerDeliveryEvent, SeedSavingEvent, SeedSharingEvent, TransportEventType } from './types'; /** * TransportChain - Blockchain for transport events */ export class TransportChain { public chain: TransportBlock[]; public difficulty: number; private eventIndex: Map; private plantEvents: Map; private batchEvents: Map; constructor(difficulty: number = 3) { this.chain = [this.createGenesisBlock()]; this.difficulty = difficulty; this.eventIndex = new Map(); this.plantEvents = new Map(); this.batchEvents = new Map(); } private createGenesisBlock(): TransportBlock { const genesisEvent: SeedAcquisitionEvent = { id: 'genesis-transport-0', timestamp: new Date().toISOString(), eventType: 'seed_acquisition', fromLocation: { latitude: 0, longitude: 0, locationType: 'seed_bank', facilityName: 'LocalGreenChain Genesis' }, toLocation: { latitude: 0, longitude: 0, locationType: 'seed_bank', facilityName: 'LocalGreenChain Genesis' }, distanceKm: 0, durationMinutes: 0, transportMethod: 'walking', carbonFootprintKg: 0, senderId: 'system', receiverId: 'system', status: 'verified', seedBatchId: 'genesis-seed-batch', sourceType: 'seed_bank', species: 'Blockchain primordialis', quantity: 1, quantityUnit: 'seeds', generation: 0 }; return { index: 0, timestamp: new Date().toISOString(), transportEvent: genesisEvent, previousHash: '0', hash: this.calculateHash(0, new Date().toISOString(), genesisEvent, '0', 0), nonce: 0, cumulativeCarbonKg: 0, cumulativeFoodMiles: 0, chainLength: 1 }; } private calculateHash( index: number, timestamp: string, event: TransportEvent, previousHash: string, nonce: number ): string { const data = `${index}${timestamp}${JSON.stringify(event)}${previousHash}${nonce}`; return crypto.createHash('sha256').update(data).digest('hex'); } private mineBlock(block: TransportBlock): void { const target = '0'.repeat(this.difficulty); while (block.hash.substring(0, this.difficulty) !== target) { block.nonce++; block.hash = this.calculateHash( block.index, block.timestamp, block.transportEvent, block.previousHash, block.nonce ); } } getLatestBlock(): TransportBlock { return this.chain[this.chain.length - 1]; } /** * Record a new transport event */ recordEvent(event: TransportEvent): TransportBlock { const latestBlock = this.getLatestBlock(); // Calculate carbon footprint if not provided if (!event.carbonFootprintKg) { event.carbonFootprintKg = this.calculateCarbon( event.transportMethod, event.distanceKm, this.estimateWeight(event) ); } const newBlock: TransportBlock = { index: this.chain.length, timestamp: new Date().toISOString(), transportEvent: event, previousHash: latestBlock.hash, hash: '', nonce: 0, cumulativeCarbonKg: latestBlock.cumulativeCarbonKg + event.carbonFootprintKg, cumulativeFoodMiles: latestBlock.cumulativeFoodMiles + event.distanceKm, chainLength: this.chain.length + 1 }; newBlock.hash = this.calculateHash( newBlock.index, newBlock.timestamp, event, newBlock.previousHash, newBlock.nonce ); this.mineBlock(newBlock); this.chain.push(newBlock); this.indexEvent(event, newBlock); return newBlock; } private indexEvent(event: TransportEvent, block: TransportBlock): void { // Index by event ID const eventBlocks = this.eventIndex.get(event.id) || []; eventBlocks.push(block); this.eventIndex.set(event.id, eventBlocks); // Index by plant IDs const plantIds = this.extractPlantIds(event); for (const plantId of plantIds) { const events = this.plantEvents.get(plantId) || []; events.push(event); this.plantEvents.set(plantId, events); } // Index by batch IDs const batchIds = this.extractBatchIds(event); for (const batchId of batchIds) { const events = this.batchEvents.get(batchId) || []; events.push(event); this.batchEvents.set(batchId, events); } } private extractPlantIds(event: TransportEvent): string[] { switch (event.eventType) { case 'planting': return (event as PlantingEvent).plantIds; case 'growing_transport': return (event as GrowingTransportEvent).plantIds; case 'harvest': return (event as HarvestEvent).plantIds; case 'seed_saving': return (event as SeedSavingEvent).parentPlantIds; default: return []; } } private extractBatchIds(event: TransportEvent): string[] { const batchIds: string[] = []; switch (event.eventType) { case 'seed_acquisition': batchIds.push((event as SeedAcquisitionEvent).seedBatchId); break; case 'planting': batchIds.push((event as PlantingEvent).seedBatchId); break; case 'harvest': batchIds.push((event as HarvestEvent).harvestBatchId); if ((event as HarvestEvent).seedBatchIdCreated) { batchIds.push((event as HarvestEvent).seedBatchIdCreated!); } break; case 'processing': batchIds.push(...(event as ProcessingEvent).harvestBatchIds); batchIds.push((event as ProcessingEvent).processingBatchId); break; case 'distribution': batchIds.push(...(event as DistributionEvent).batchIds); break; case 'consumer_delivery': batchIds.push(...(event as ConsumerDeliveryEvent).batchIds); break; case 'seed_saving': batchIds.push((event as SeedSavingEvent).newSeedBatchId); break; case 'seed_sharing': batchIds.push((event as SeedSharingEvent).seedBatchId); break; } return batchIds; } private estimateWeight(event: TransportEvent): number { // Estimate weight in kg based on event type switch (event.eventType) { case 'seed_acquisition': case 'seed_saving': case 'seed_sharing': return 0.1; // Seeds are light case 'planting': return 0.5; case 'growing_transport': return 2; case 'harvest': return (event as HarvestEvent).netWeight || 5; case 'processing': return (event as ProcessingEvent).outputWeight || 5; case 'distribution': case 'consumer_delivery': return 5; default: return 1; } } /** * Calculate carbon footprint */ calculateCarbon(method: TransportMethod, distanceKm: number, weightKg: number): number { const factor = CARBON_FACTORS[method] || 0.1; return factor * distanceKm * weightKg; } /** * Calculate distance between two locations using Haversine formula */ static calculateDistance(from: TransportLocation, to: TransportLocation): number { const R = 6371; // Earth's radius in km const dLat = TransportChain.toRadians(to.latitude - from.latitude); const dLon = TransportChain.toRadians(to.longitude - from.longitude); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(TransportChain.toRadians(from.latitude)) * Math.cos(TransportChain.toRadians(to.latitude)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } private static toRadians(degrees: number): number { return degrees * (Math.PI / 180); } /** * Get complete journey for a plant */ getPlantJourney(plantId: string): PlantJourney | null { const events = this.plantEvents.get(plantId); if (!events || events.length === 0) return null; // Sort events by timestamp const sortedEvents = [...events].sort( (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() ); const lastEvent = sortedEvents[sortedEvents.length - 1]; // Calculate metrics let totalFoodMiles = 0; let totalCarbonKg = 0; let daysInTransit = 0; for (const event of sortedEvents) { totalFoodMiles += event.distanceKm; totalCarbonKg += event.carbonFootprintKg; daysInTransit += event.durationMinutes / (60 * 24); } // Find seed batch origin const seedAcquisition = sortedEvents.find(e => e.eventType === 'seed_acquisition') as SeedAcquisitionEvent | undefined; const planting = sortedEvents.find(e => e.eventType === 'planting') as PlantingEvent | undefined; // Calculate growing days const plantingDate = planting ? new Date(planting.timestamp) : null; const lastDate = new Date(lastEvent.timestamp); const daysGrowing = plantingDate ? Math.floor((lastDate.getTime() - plantingDate.getTime()) / (1000 * 60 * 60 * 24)) : 0; // Determine current stage let currentStage: PlantJourney['currentStage'] = 'seed'; if (sortedEvents.some(e => e.eventType === 'seed_saving')) { currentStage = 'seed_saving'; } else if (sortedEvents.some(e => e.eventType === 'harvest')) { currentStage = 'post_harvest'; } else if (sortedEvents.some(e => e.eventType === 'growing_transport')) { const lastGrowing = sortedEvents.filter(e => e.eventType === 'growing_transport').pop() as GrowingTransportEvent; currentStage = lastGrowing.plantStage; } else if (sortedEvents.some(e => e.eventType === 'planting')) { currentStage = 'seedling'; } return { plantId, seedBatchOrigin: seedAcquisition?.seedBatchId || planting?.seedBatchId || 'unknown', currentCustodian: lastEvent.receiverId, currentLocation: lastEvent.toLocation, currentStage, events: sortedEvents, totalFoodMiles, totalCarbonKg, daysInTransit: Math.round(daysInTransit), daysGrowing, generation: seedAcquisition?.generation || 0, ancestorPlantIds: seedAcquisition?.parentPlantIds || [], descendantSeedBatches: sortedEvents .filter(e => e.eventType === 'seed_saving') .map(e => (e as SeedSavingEvent).newSeedBatchId) }; } /** * Get environmental impact summary for a user */ getEnvironmentalImpact(userId: string): EnvironmentalImpact { const userEvents = this.chain .filter(block => block.transportEvent.senderId === userId || block.transportEvent.receiverId === userId ) .map(block => block.transportEvent); let totalCarbonKg = 0; let totalFoodMiles = 0; let totalWeight = 0; const breakdownByMethod: EnvironmentalImpact['breakdownByMethod'] = {} as any; const breakdownByEventType: EnvironmentalImpact['breakdownByEventType'] = {} as any; for (const event of userEvents) { totalCarbonKg += event.carbonFootprintKg; totalFoodMiles += event.distanceKm; totalWeight += this.estimateWeight(event); // Method breakdown if (!breakdownByMethod[event.transportMethod]) { breakdownByMethod[event.transportMethod] = { distance: 0, carbon: 0 }; } breakdownByMethod[event.transportMethod].distance += event.distanceKm; breakdownByMethod[event.transportMethod].carbon += event.carbonFootprintKg; // Event type breakdown if (!breakdownByEventType[event.eventType]) { breakdownByEventType[event.eventType] = { count: 0, carbon: 0 }; } breakdownByEventType[event.eventType].count++; breakdownByEventType[event.eventType].carbon += event.carbonFootprintKg; } // Conventional comparison (assume 1500 miles avg, 2.5 kg CO2/lb) const conventionalMiles = totalWeight * 1500; const conventionalCarbon = totalWeight * 2.5; return { totalCarbonKg, totalFoodMiles, carbonPerKgProduce: totalWeight > 0 ? totalCarbonKg / totalWeight : 0, milesPerKgProduce: totalWeight > 0 ? totalFoodMiles / totalWeight : 0, breakdownByMethod, breakdownByEventType, comparisonToConventional: { carbonSaved: Math.max(0, conventionalCarbon - totalCarbonKg), milesSaved: Math.max(0, conventionalMiles - totalFoodMiles), percentageReduction: conventionalCarbon > 0 ? Math.round((1 - totalCarbonKg / conventionalCarbon) * 100) : 0 } }; } /** * Generate QR code data for a plant or batch */ generateQRData(plantId?: string, batchId?: string): TransportQRData { const events = plantId ? this.plantEvents.get(plantId) : batchId ? this.batchEvents.get(batchId) : []; const lastEvent = events && events.length > 0 ? events[events.length - 1] : null; const lineageHash = crypto.createHash('sha256') .update(JSON.stringify(events)) .digest('hex') .substring(0, 16); return { plantId, batchId, blockchainAddress: this.getLatestBlock().hash.substring(0, 42), quickLookupUrl: `https://localgreenchain.org/track/${plantId || batchId}`, lineageHash, currentCustodian: lastEvent?.receiverId || 'unknown', lastEventType: lastEvent?.eventType || 'seed_acquisition', lastEventTimestamp: lastEvent?.timestamp || new Date().toISOString(), verificationCode: crypto.randomBytes(4).toString('hex').toUpperCase() }; } /** * Verify chain integrity */ isChainValid(): boolean { for (let i = 1; i < this.chain.length; i++) { const currentBlock = this.chain[i]; const previousBlock = this.chain[i - 1]; // Verify hash const expectedHash = this.calculateHash( currentBlock.index, currentBlock.timestamp, currentBlock.transportEvent, currentBlock.previousHash, currentBlock.nonce ); if (currentBlock.hash !== expectedHash) { return false; } // Verify chain link if (currentBlock.previousHash !== previousBlock.hash) { return false; } } return true; } /** * Export to JSON */ toJSON(): object { return { difficulty: this.difficulty, chain: this.chain }; } /** * Import from JSON */ static fromJSON(data: any): TransportChain { const chain = new TransportChain(data.difficulty); chain.chain = data.chain; // Rebuild indexes for (const block of chain.chain) { chain.indexEvent(block.transportEvent, block); } return chain; } } // Singleton instance let transportChainInstance: TransportChain | null = null; export function getTransportChain(): TransportChain { if (!transportChainInstance) { transportChainInstance = new TransportChain(); } return transportChainInstance; } export function setTransportChain(chain: TransportChain): void { transportChainInstance = chain; }