/** * PlantChainDB - Database-backed blockchain for plant lineage tracking * This implementation uses PostgreSQL via Prisma for persistence while * maintaining blockchain data integrity guarantees */ import { PlantBlock } from './PlantBlock'; import type { PlantData, PlantLineage, NearbyPlant, PlantNetwork } from './types'; import * as db from '../db'; import type { Plant, BlockchainBlock } from '../db/types'; /** * Convert database Plant model to PlantData format */ function plantToPlantData(plant: Plant & { owner?: { id: string; name: string; email: string } }): PlantData { return { id: plant.id, commonName: plant.commonName, scientificName: plant.scientificName || undefined, species: plant.species || undefined, genus: plant.genus || undefined, family: plant.family || undefined, parentPlantId: plant.parentPlantId || undefined, propagationType: plant.propagationType.toLowerCase() as PlantData['propagationType'], generation: plant.generation, plantedDate: plant.plantedDate.toISOString(), harvestedDate: plant.harvestedDate?.toISOString(), status: plant.status.toLowerCase() as PlantData['status'], location: { latitude: plant.latitude, longitude: plant.longitude, address: plant.address || undefined, city: plant.city || undefined, country: plant.country || undefined, }, owner: plant.owner ? { id: plant.owner.id, name: plant.owner.name, email: plant.owner.email, } : { id: plant.ownerId, name: 'Unknown', email: 'unknown@localgreenchain.io', }, childPlants: [], // Will be populated separately if needed environment: plant.environment as PlantData['environment'], growthMetrics: plant.growthMetrics as PlantData['growthMetrics'], notes: plant.notes || undefined, images: plant.images || undefined, plantsNetId: plant.plantsNetId || undefined, registeredAt: plant.registeredAt.toISOString(), updatedAt: plant.updatedAt.toISOString(), }; } /** * PlantChainDB - Database-backed plant blockchain */ export class PlantChainDB { public difficulty: number; constructor(difficulty: number = 4) { this.difficulty = difficulty; } /** * Initialize the chain with genesis block if needed */ async initialize(): Promise { const latestBlock = await db.getLatestBlockchainBlock(); if (!latestBlock) { await this.createGenesisBlock(); } } /** * Create the genesis block */ private async createGenesisBlock(): Promise { 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'); return db.createBlockchainBlock({ index: 0, timestamp: new Date(), previousHash: '0', hash: block.hash, nonce: 0, blockType: 'genesis', content: { plant: genesisPlant }, }); } /** * Get the latest block */ async getLatestBlock(): Promise { return db.getLatestBlockchainBlock(); } /** * Register a new plant and add to blockchain */ async registerPlant(plantData: { id?: string; commonName: string; scientificName?: string; species?: string; genus?: string; family?: string; plantedDate?: Date; status?: 'SPROUTED' | 'GROWING' | 'MATURE' | 'FLOWERING' | 'FRUITING' | 'DORMANT' | 'DECEASED'; latitude: number; longitude: number; address?: string; city?: string; country?: string; ownerId: string; notes?: string; images?: string[]; plantsNetId?: string; }): Promise<{ plant: Plant; block: BlockchainBlock }> { // Create plant in database const plant = await db.createPlant({ ...plantData, plantedDate: plantData.plantedDate || new Date(), propagationType: 'ORIGINAL', generation: 0, }); // Get latest block for previous hash const latestBlock = await this.getLatestBlock(); const previousHash = latestBlock?.hash || '0'; const nextIndex = (latestBlock?.index ?? -1) + 1; // Create PlantBlock for mining const plantDataForBlock = await this.getPlantData(plant.id); const newBlock = new PlantBlock( nextIndex, new Date().toISOString(), plantDataForBlock!, previousHash ); newBlock.mineBlock(this.difficulty); // Store block in database const blockchainBlock = await db.createBlockchainBlock({ index: nextIndex, timestamp: new Date(), previousHash, hash: newBlock.hash, nonce: newBlock.nonce, blockType: 'plant', plantId: plant.id, content: { plant: plantDataForBlock, action: 'REGISTER' }, }); // Update plant with blockchain reference await db.updatePlantBlockchain(plant.id, nextIndex, newBlock.hash); return { plant, block: blockchainBlock }; } /** * Clone a plant and add to blockchain */ async clonePlant( parentPlantId: string, ownerId: string, propagationType: 'SEED' | 'CLONE' | 'CUTTING' | 'DIVISION' | 'GRAFTING', overrides?: { latitude?: number; longitude?: number; address?: string; city?: string; country?: string; notes?: string; } ): Promise<{ plant: Plant; block: BlockchainBlock }> { // Create cloned plant in database const plant = await db.clonePlant( parentPlantId, ownerId, propagationType, overrides ); // Get latest block for previous hash const latestBlock = await this.getLatestBlock(); const previousHash = latestBlock?.hash || '0'; const nextIndex = (latestBlock?.index ?? -1) + 1; // Create PlantBlock for mining const plantDataForBlock = await this.getPlantData(plant.id); const newBlock = new PlantBlock( nextIndex, new Date().toISOString(), plantDataForBlock!, previousHash ); newBlock.mineBlock(this.difficulty); // Store block in database const blockchainBlock = await db.createBlockchainBlock({ index: nextIndex, timestamp: new Date(), previousHash, hash: newBlock.hash, nonce: newBlock.nonce, blockType: 'plant', plantId: plant.id, content: { plant: plantDataForBlock, action: 'CLONE', parentPlantId, }, }); // Update plant with blockchain reference await db.updatePlantBlockchain(plant.id, nextIndex, newBlock.hash); return { plant, block: blockchainBlock }; } /** * Update plant status and add to blockchain */ async updatePlantStatus( plantId: string, status: 'SPROUTED' | 'GROWING' | 'MATURE' | 'FLOWERING' | 'FRUITING' | 'DORMANT' | 'DECEASED', harvestedDate?: Date ): Promise<{ plant: Plant; block: BlockchainBlock }> { // Update plant in database const plant = await db.updatePlantStatus(plantId, status, harvestedDate); // Get latest block for previous hash const latestBlock = await this.getLatestBlock(); const previousHash = latestBlock?.hash || '0'; const nextIndex = (latestBlock?.index ?? -1) + 1; // Create PlantBlock for mining const plantDataForBlock = await this.getPlantData(plant.id); const newBlock = new PlantBlock( nextIndex, new Date().toISOString(), plantDataForBlock!, previousHash ); newBlock.mineBlock(this.difficulty); // Store block in database const blockchainBlock = await db.createBlockchainBlock({ index: nextIndex, timestamp: new Date(), previousHash, hash: newBlock.hash, nonce: newBlock.nonce, blockType: 'plant', plantId: plant.id, content: { plant: plantDataForBlock, action: 'UPDATE_STATUS', newStatus: status, }, }); // Update plant with blockchain reference await db.updatePlantBlockchain(plant.id, nextIndex, newBlock.hash); return { plant, block: blockchainBlock }; } /** * Get plant data in PlantData format */ async getPlantData(plantId: string): Promise { const plant = await db.getPlantWithOwner(plantId); if (!plant) return null; const plantData = plantToPlantData(plant as Plant & { owner: { id: string; name: string; email: string } }); // Get child plant IDs const lineage = await db.getPlantLineage(plantId); plantData.childPlants = lineage.descendants.map(d => d.id); return plantData; } /** * Get a plant by ID */ async getPlant(plantId: string): Promise { return db.getPlantById(plantId); } /** * Get plant with owner */ async getPlantWithOwner(plantId: string) { return db.getPlantWithOwner(plantId); } /** * Get complete lineage for a plant */ async getPlantLineage(plantId: string): Promise { const result = await db.getPlantLineage(plantId); if (!result.plant) return null; const plant = result.plant as Plant & { owner: { id: string; name: string; email: string } }; return { plantId, ancestors: result.ancestors.map(p => plantToPlantData(p as Plant & { owner: { id: string; name: string; email: string } })), descendants: result.descendants.map(p => plantToPlantData(p as Plant & { owner: { id: string; name: string; email: string } })), siblings: result.siblings.map(p => plantToPlantData(p as Plant & { owner: { id: string; name: string; email: string } })), generation: plant.generation, }; } /** * Find plants near a location */ async findNearbyPlants( latitude: number, longitude: number, radiusKm: number = 50, excludeOwnerId?: string ): Promise { const plants = await db.getNearbyPlants( { latitude, longitude, radiusKm }, excludeOwnerId ); return plants.map(p => ({ plant: plantToPlantData(p as Plant & { owner: { id: string; name: string; email: string } }), distance: p.distance, owner: { id: p.ownerId, name: 'Unknown', email: 'unknown@localgreenchain.io', }, })); } /** * Get network statistics */ async getNetworkStats(): Promise { return db.getPlantNetworkStats(); } /** * Validate the blockchain integrity */ async isChainValid(): Promise { const result = await db.verifyBlockchainIntegrity(); return result.isValid; } /** * Get blockchain statistics */ async getBlockchainStats() { return db.getBlockchainStats(); } /** * Search plants */ async searchPlants(query: string, options?: { page?: number; limit?: number }) { return db.searchPlants(query, options); } /** * Get plants by owner */ async getPlantsByOwner(ownerId: string) { return db.getPlantsByOwner(ownerId); } } // Singleton instance let plantChainInstance: PlantChainDB | null = null; /** * Get the singleton PlantChainDB instance */ export async function getPlantChain(): Promise { if (!plantChainInstance) { plantChainInstance = new PlantChainDB(); await plantChainInstance.initialize(); } return plantChainInstance; } export default PlantChainDB;