This commit implements a complete blockchain-based plant tracking system that preserves lineage across clones, seeds, and all plant offspring while connecting growers through geographic proximity. Features implemented: - Custom blockchain with proof-of-work consensus - Plant registration and cloning with lineage tracking - Geographic discovery to find nearby plants and growers - Integration with plants.net API for plant identification - Comprehensive web UI for plant management - RESTful API endpoints for all operations - Network statistics and visualization Core Components: - lib/blockchain/: PlantBlock, PlantChain, and blockchain manager - lib/services/: plants.net API and geolocation services - pages/api/plants/: REST API endpoints for all operations - pages/: Frontend UI pages for registration, exploration, and lineage Technical Details: - TypeScript for type safety - Next.js for server-side rendering - Tailwind CSS for responsive design - JSON file-based blockchain storage - Haversine distance calculations for geolocation - OpenStreetMap integration for geocoding This system enables large-scale adoption by: - Making plant lineage tracking accessible to everyone - Connecting local communities through plant sharing - Providing immutable proof of plant provenance - Supporting unlimited generations of plant propagation - Scaling from individual growers to global networks Documentation includes comprehensive README with: - Quick start guide - API reference - Architecture details - Scaling recommendations - Use cases for various audiences - Roadmap for future enhancements
396 lines
10 KiB
TypeScript
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.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<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;
|
|
}
|
|
}
|