import { PlantBlock } from './PlantBlock'; import { PlantData, PlantLineage, NearbyPlant, PlantNetwork } from './types'; /** * PlantChain - The main blockchain for tracking plant lineage and ownership * This blockchain records every plant, its clones, seeds, and ownership transfers */ export class PlantChain { public chain: PlantBlock[]; public difficulty: number; private plantIndex: Map; // Quick lookup by plant ID constructor(difficulty: number = 4) { this.chain = [this.createGenesisBlock()]; this.difficulty = difficulty; this.plantIndex = new Map(); } /** * Create the first block in the chain */ private createGenesisBlock(): PlantBlock { const genesisPlant: PlantData = { id: 'genesis-plant-0', commonName: 'Genesis Plant', scientificName: 'Blockchain primordialis', propagationType: 'original', generation: 0, plantedDate: new Date().toISOString(), status: 'mature', location: { latitude: 0, longitude: 0, address: 'The Beginning', }, owner: { id: 'system', name: 'LocalGreenChain', email: 'system@localgreenchain.org', }, childPlants: [], registeredAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; const block = new PlantBlock(0, new Date().toISOString(), genesisPlant, '0'); this.plantIndex.set(genesisPlant.id, block); return block; } /** * Get the latest block in the chain */ getLatestBlock(): PlantBlock { return this.chain[this.chain.length - 1]; } /** * Register a new original plant (not a clone or seed) */ registerPlant(plant: PlantData): PlantBlock { // Ensure plant has required fields if (!plant.id || !plant.commonName || !plant.owner) { throw new Error('Plant must have id, commonName, and owner'); } // Check if plant already exists if (this.plantIndex.has(plant.id)) { throw new Error(`Plant with ID ${plant.id} already exists`); } // Set defaults plant.generation = 0; plant.propagationType = plant.propagationType || 'original'; plant.childPlants = []; plant.registeredAt = new Date().toISOString(); plant.updatedAt = new Date().toISOString(); const newBlock = new PlantBlock( this.chain.length, new Date().toISOString(), plant, this.getLatestBlock().hash ); newBlock.mineBlock(this.difficulty); this.chain.push(newBlock); this.plantIndex.set(plant.id, newBlock); return newBlock; } /** * Register a clone or seed from an existing plant */ clonePlant( parentPlantId: string, newPlant: Partial, propagationType: 'seed' | 'clone' | 'cutting' | 'division' | 'grafting' ): PlantBlock { // Find parent plant const parentBlock = this.plantIndex.get(parentPlantId); if (!parentBlock) { throw new Error(`Parent plant ${parentPlantId} not found`); } const parentPlant = parentBlock.plant; // Create new plant with inherited properties const clonedPlant: PlantData = { id: newPlant.id || `${parentPlantId}-${propagationType}-${Date.now()}`, commonName: newPlant.commonName || parentPlant.commonName, scientificName: newPlant.scientificName || parentPlant.scientificName, species: newPlant.species || parentPlant.species, genus: newPlant.genus || parentPlant.genus, family: newPlant.family || parentPlant.family, // Lineage parentPlantId: parentPlantId, propagationType: propagationType, generation: parentPlant.generation + 1, // Required fields from newPlant plantedDate: newPlant.plantedDate || new Date().toISOString(), status: newPlant.status || 'sprouted', location: newPlant.location!, owner: newPlant.owner!, // Initialize child tracking childPlants: [], // Optional fields notes: newPlant.notes, images: newPlant.images, plantsNetId: newPlant.plantsNetId, registeredAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; // Validate required fields if (!clonedPlant.location || !clonedPlant.owner) { throw new Error('Cloned plant must have location and owner'); } // Add to blockchain const newBlock = new PlantBlock( this.chain.length, new Date().toISOString(), clonedPlant, this.getLatestBlock().hash ); newBlock.mineBlock(this.difficulty); this.chain.push(newBlock); this.plantIndex.set(clonedPlant.id, newBlock); // Update parent plant's child list parentPlant.childPlants.push(clonedPlant.id); parentPlant.updatedAt = new Date().toISOString(); return newBlock; } /** * Update plant status (growing, flowering, etc.) */ updatePlantStatus( plantId: string, updates: Partial ): PlantBlock { const existingBlock = this.plantIndex.get(plantId); if (!existingBlock) { throw new Error(`Plant ${plantId} not found`); } const updatedPlant: PlantData = { ...existingBlock.plant, ...updates, id: existingBlock.plant.id, // Cannot change ID parentPlantId: existingBlock.plant.parentPlantId, // Cannot change lineage childPlants: existingBlock.plant.childPlants, // Cannot change children generation: existingBlock.plant.generation, // Cannot change generation registeredAt: existingBlock.plant.registeredAt, // Cannot change registration updatedAt: new Date().toISOString(), }; const newBlock = new PlantBlock( this.chain.length, new Date().toISOString(), updatedPlant, this.getLatestBlock().hash ); newBlock.mineBlock(this.difficulty); this.chain.push(newBlock); this.plantIndex.set(plantId, newBlock); // Update index to latest block return newBlock; } /** * Get a plant by ID (returns latest version) */ getPlant(plantId: string): PlantData | null { const block = this.plantIndex.get(plantId); return block ? block.plant : null; } /** * Get complete lineage for a plant */ getPlantLineage(plantId: string): PlantLineage | null { const plant = this.getPlant(plantId); if (!plant) return null; // Get all ancestors const ancestors: PlantData[] = []; let currentPlant = plant; while (currentPlant.parentPlantId) { const parent = this.getPlant(currentPlant.parentPlantId); if (!parent) break; ancestors.push(parent); currentPlant = parent; } // Get all descendants recursively const descendants: PlantData[] = []; const getDescendants = (p: PlantData) => { for (const childId of p.childPlants) { const child = this.getPlant(childId); if (child) { descendants.push(child); getDescendants(child); } } }; getDescendants(plant); // Get siblings (other plants from same parent) const siblings: PlantData[] = []; if (plant.parentPlantId) { const parent = this.getPlant(plant.parentPlantId); if (parent) { for (const siblingId of parent.childPlants) { if (siblingId !== plantId) { const sibling = this.getPlant(siblingId); if (sibling) siblings.push(sibling); } } } } return { plantId, ancestors, descendants, siblings, generation: plant.generation, }; } /** * Find plants near a location (within radius in km) */ findNearbyPlants( latitude: number, longitude: number, radiusKm: number = 50 ): NearbyPlant[] { const nearbyPlants: NearbyPlant[] = []; // Get all unique plants (latest version of each) const allPlants = Array.from(this.plantIndex.values()).map(block => block.plant); for (const plant of allPlants) { const distance = this.calculateDistance( latitude, longitude, plant.location.latitude, plant.location.longitude ); if (distance <= radiusKm) { nearbyPlants.push({ plant, distance, owner: plant.owner, }); } } // Sort by distance return nearbyPlants.sort((a, b) => a.distance - b.distance); } /** * Calculate distance between two coordinates using Haversine formula */ private calculateDistance( lat1: number, lon1: number, lat2: number, lon2: number ): number { const R = 6371; // Earth's radius in km const dLat = this.toRadians(lat2 - lat1); const dLon = this.toRadians(lon2 - lon1); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } private toRadians(degrees: number): number { return degrees * (Math.PI / 180); } /** * Get network statistics */ getNetworkStats(): PlantNetwork { const allPlants = Array.from(this.plantIndex.values()).map(block => block.plant); const owners = new Set(); const species: { [key: string]: number } = {}; const countries: { [key: string]: number } = {}; for (const plant of allPlants) { owners.add(plant.owner.id); if (plant.scientificName) { species[plant.scientificName] = (species[plant.scientificName] || 0) + 1; } if (plant.location.country) { countries[plant.location.country] = (countries[plant.location.country] || 0) + 1; } } return { totalPlants: allPlants.length, totalOwners: owners.size, species, globalDistribution: countries, }; } /** * Validate the entire blockchain */ isChainValid(): boolean { for (let i = 1; i < this.chain.length; i++) { const currentBlock = this.chain[i]; const previousBlock = this.chain[i - 1]; if (!currentBlock.isValid(previousBlock)) { return false; } } return true; } /** * Export chain to JSON */ toJSON(): any { return { difficulty: this.difficulty, chain: this.chain.map(block => block.toJSON()), }; } /** * Import chain from JSON */ static fromJSON(data: any): PlantChain { const plantChain = new PlantChain(data.difficulty); plantChain.chain = data.chain.map((blockData: any) => PlantBlock.fromJSON(blockData) ); // Rebuild index plantChain.plantIndex.clear(); for (const block of plantChain.chain) { plantChain.plantIndex.set(block.plant.id, block); } return plantChain; } }