localgreenchain/lib/blockchain/PlantChain.ts
Claude 507df5912f
Deploy GrowerAdvisoryAgent (Agent 10) and fix type errors
- Add GrowerAdvisoryAgent test file
- Fix PlantChain constructor initialization order (plantIndex before genesis block)
- Fix blockchain.getChain() calls to use blockchain.chain property
- Add PropagationType export to blockchain types
- Fix SoilComposition.type property references (was soilType)
- Fix ClimateConditions.temperatureDay property references (was avgTemperature)
- Fix ClimateConditions.humidityAverage property references (was avgHumidity)
- Fix LightingConditions.naturalLight.hoursPerDay nested access
- Add 'critical' severity to QualityReport issues
- Add 'sqm' unit to PlantingRecommendation.quantityUnit
- Fix GrowerAdvisoryAgent growthMetrics property access
- Update TypeScript to v5 for react-hook-form compatibility
- Enable downlevelIteration in tsconfig for Map iteration
- Fix crypto Buffer type issues in anonymity.ts
- Fix zones.tsx status type comparison
- Fix next.config.js images.domains filter
- Rename [[...slug]].tsx to [...slug].tsx to resolve routing conflict
2025-11-23 00:44:58 +00:00

396 lines
10 KiB
TypeScript

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<string, PlantBlock>; // Quick lookup by plant ID
constructor(difficulty: number = 4) {
this.difficulty = difficulty;
this.plantIndex = new Map();
this.chain = [this.createGenesisBlock()];
}
/**
* 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<PlantData>,
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<PlantData>
): 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<string>();
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;
}
}