UNPKG

defarm-sdk

Version:

DeFarm SDK - On-premise blockchain data processing and tokenization engine for agriculture supply chain

776 lines (654 loc) 21.6 kB
const { ethers } = require('ethers'); const crypto = require('crypto'); /** * Blockchain Engine for DeFarm SDK * Handles all blockchain interactions and smart contract operations */ class BlockchainEngine { constructor(config = {}) { this.config = { enabled: config.enabled !== false, network: config.network || 'polygon', rpcUrl: config.rpcUrl || this.getDefaultRpcUrl(config.network), privateKey: config.privateKey, contractAddress: config.contractAddress, gasLimit: config.gasLimit || 500000, maxGasPrice: config.maxGasPrice || 100, // in gwei confirmations: config.confirmations || 2, retryAttempts: config.retryAttempts || 3, retryDelay: config.retryDelay || 5000, batchSize: config.batchSize || 100, ...config }; // Network configurations this.networks = { polygon: { chainId: 137, name: 'Polygon', rpcUrl: 'https://polygon-rpc.com', explorer: 'https://polygonscan.com', nativeCurrency: 'MATIC' }, 'polygon-mumbai': { chainId: 80001, name: 'Polygon Mumbai', rpcUrl: 'https://rpc-mumbai.maticvigil.com', explorer: 'https://mumbai.polygonscan.com', nativeCurrency: 'MATIC' }, ethereum: { chainId: 1, name: 'Ethereum', rpcUrl: 'https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY', explorer: 'https://etherscan.io', nativeCurrency: 'ETH' }, bsc: { chainId: 56, name: 'BSC', rpcUrl: 'https://bsc-dataseed.binance.org', explorer: 'https://bscscan.com', nativeCurrency: 'BNB' }, localhost: { chainId: 31337, name: 'Localhost', rpcUrl: 'http://localhost:8545', explorer: '', nativeCurrency: 'ETH' } }; this.provider = null; this.signer = null; this.contract = null; this.initialized = false; // Transaction tracking this.pendingTransactions = new Map(); this.transactionHistory = []; // Event listeners this.eventListeners = new Map(); // Statistics this.stats = { totalTransactions: 0, successfulTransactions: 0, failedTransactions: 0, totalGasUsed: BigInt(0), totalCost: BigInt(0), averageGasPrice: BigInt(0), eventsReceived: 0 }; } /** * Initialize blockchain connection */ async initialize() { if (!this.config.enabled) { console.log('⏭️ Blockchain engine disabled'); return; } if (this.initialized) return; console.log('🔗 Initializing Blockchain Engine...'); try { // Setup provider await this.setupProvider(); // Setup signer if private key provided if (this.config.privateKey) { await this.setupSigner(); } // Load contract if address provided if (this.config.contractAddress) { await this.loadContract(); } // Subscribe to events this.subscribeToEvents(); // Check network await this.checkNetwork(); this.initialized = true; console.log(`✅ Blockchain Engine initialized on ${this.config.network}`); } catch (error) { console.error('❌ Blockchain initialization failed:', error); throw error; } } /** * Setup provider */ async setupProvider() { const networkConfig = this.networks[this.config.network]; if (!networkConfig) { throw new Error(`Unknown network: ${this.config.network}`); } const rpcUrl = this.config.rpcUrl || networkConfig.rpcUrl; try { this.provider = new ethers.JsonRpcProvider(rpcUrl); // Test connection await this.provider.getBlockNumber(); console.log(` ✓ Connected to ${networkConfig.name}`); } catch (error) { throw new Error(`Failed to connect to ${networkConfig.name}: ${error.message}`); } } /** * Setup signer */ async setupSigner() { if (!this.config.privateKey) { throw new Error('Private key required for signing transactions'); } try { this.signer = new ethers.Wallet(this.config.privateKey, this.provider); const address = await this.signer.getAddress(); const balance = await this.provider.getBalance(address); console.log(` ✓ Signer configured: ${address}`); console.log(` ✓ Balance: ${ethers.formatEther(balance)} ${this.networks[this.config.network].nativeCurrency}`); } catch (error) { throw new Error(`Failed to setup signer: ${error.message}`); } } /** * Load smart contract */ async loadContract() { if (!this.config.contractAddress) return; // Default ABI for DeFarm contract const abi = this.config.contractABI || [ // Events "event AssetCreated(bytes32 indexed assetId, address indexed owner, string assetType, uint256 timestamp)", "event AssetTransferred(bytes32 indexed assetId, address indexed from, address indexed to, uint256 timestamp)", "event EventRecorded(bytes32 indexed assetId, string eventType, address indexed actor, uint256 timestamp, string dataHash)", "event TokenMinted(uint256 indexed tokenId, bytes32 indexed assetId, address indexed owner, uint256 amount)", // Functions "function createAsset(bytes32 assetId, string assetType, string dataHash) returns (bool)", "function transferAsset(bytes32 assetId, address to) returns (bool)", "function recordEvent(bytes32 assetId, string eventType, string dataHash) returns (bool)", "function getAsset(bytes32 assetId) view returns (tuple(address owner, string assetType, string dataHash, uint256 createdAt, uint256 updatedAt))", "function getAssetHistory(bytes32 assetId) view returns (tuple(string eventType, address actor, string dataHash, uint256 timestamp)[])", "function mintToken(bytes32 assetId, address to, uint256 amount, string uri) returns (uint256)", "function batchRecordEvents(bytes32[] assetIds, string[] eventTypes, string[] dataHashes) returns (bool)", "function verifyAsset(bytes32 assetId, string dataHash) view returns (bool)", "function getOwner(bytes32 assetId) view returns (address)", "function totalAssets() view returns (uint256)", "function totalEvents() view returns (uint256)" ]; try { this.contract = new ethers.Contract( this.config.contractAddress, abi, this.signer || this.provider ); // Test contract if (this.contract.totalAssets) { const totalAssets = await this.contract.totalAssets(); console.log(` ✓ Contract loaded: ${this.config.contractAddress}`); console.log(` ✓ Total assets on chain: ${totalAssets}`); } } catch (error) { console.warn(` ⚠ Contract loading failed: ${error.message}`); } } /** * Check network connection */ async checkNetwork() { const network = await this.provider.getNetwork(); const expectedChainId = this.networks[this.config.network].chainId; if (network.chainId !== BigInt(expectedChainId)) { throw new Error(`Wrong network. Expected ${this.config.network} (${expectedChainId}), got ${network.chainId}`); } } /** * Record data on blockchain */ async recordData(data, options = {}) { if (!this.initialized) await this.initialize(); if (!this.config.enabled) return null; const assetId = data.asset_id || this.generateAssetId(data); const dataHash = this.calculateDataHash(data); try { // Prepare transaction const tx = await this.prepareTransaction( 'createAsset', [assetId, data.asset_type || 'unknown', dataHash], options ); // Send transaction const receipt = await this.sendTransaction(tx); // Store transaction reference this.transactionHistory.push({ type: 'record_data', assetId, hash: receipt.hash, blockNumber: receipt.blockNumber, timestamp: Date.now(), data: data }); console.log(`📝 Data recorded on blockchain: ${receipt.hash}`); return { success: true, assetId, transactionHash: receipt.hash, blockNumber: receipt.blockNumber, dataHash, explorerUrl: this.getExplorerUrl(receipt.hash) }; } catch (error) { console.error('Failed to record data:', error); this.stats.failedTransactions++; throw error; } } /** * Record supply chain event */ async recordEvent(eventData, options = {}) { if (!this.initialized) await this.initialize(); if (!this.config.enabled) return null; const dataHash = this.calculateDataHash(eventData); try { const tx = await this.prepareTransaction( 'recordEvent', [eventData.asset_id, eventData.event_type, dataHash], options ); const receipt = await this.sendTransaction(tx); this.stats.eventsReceived++; return { success: true, eventId: eventData.event_id, transactionHash: receipt.hash, blockNumber: receipt.blockNumber, dataHash, explorerUrl: this.getExplorerUrl(receipt.hash) }; } catch (error) { console.error('Failed to record event:', error); throw error; } } /** * Batch record multiple events */ async batchRecord(dataArray, options = {}) { if (!this.initialized) await this.initialize(); if (!this.config.enabled) return null; const batches = []; const batchSize = options.batchSize || this.config.batchSize; // Split into batches for (let i = 0; i < dataArray.length; i += batchSize) { batches.push(dataArray.slice(i, i + batchSize)); } const results = []; for (const batch of batches) { try { const assetIds = batch.map(d => d.asset_id || this.generateAssetId(d)); const eventTypes = batch.map(d => d.event_type || 'update'); const dataHashes = batch.map(d => this.calculateDataHash(d)); const tx = await this.prepareTransaction( 'batchRecordEvents', [assetIds, eventTypes, dataHashes], options ); const receipt = await this.sendTransaction(tx); results.push({ success: true, batchSize: batch.length, transactionHash: receipt.hash, blockNumber: receipt.blockNumber, explorerUrl: this.getExplorerUrl(receipt.hash) }); } catch (error) { console.error('Batch recording failed:', error); results.push({ success: false, error: error.message }); } } return results; } /** * Get asset from blockchain */ async getAsset(assetId) { if (!this.initialized) await this.initialize(); if (!this.contract) throw new Error('Contract not configured'); try { const asset = await this.contract.getAsset(assetId); return { owner: asset.owner, assetType: asset.assetType, dataHash: asset.dataHash, createdAt: Number(asset.createdAt), updatedAt: Number(asset.updatedAt) }; } catch (error) { console.error('Failed to get asset:', error); return null; } } /** * Get asset history from blockchain */ async getAssetHistory(assetId) { if (!this.initialized) await this.initialize(); if (!this.contract) throw new Error('Contract not configured'); try { const history = await this.contract.getAssetHistory(assetId); return history.map(event => ({ eventType: event.eventType, actor: event.actor, dataHash: event.dataHash, timestamp: Number(event.timestamp) })); } catch (error) { console.error('Failed to get asset history:', error); return []; } } /** * Verify data integrity on blockchain */ async verifyData(assetId, data) { if (!this.initialized) await this.initialize(); if (!this.contract) throw new Error('Contract not configured'); const dataHash = this.calculateDataHash(data); try { const isValid = await this.contract.verifyAsset(assetId, dataHash); return isValid; } catch (error) { console.error('Failed to verify data:', error); return false; } } /** * Transfer asset ownership */ async transferAsset(assetId, toAddress, options = {}) { if (!this.initialized) await this.initialize(); if (!this.contract) throw new Error('Contract not configured'); try { const tx = await this.prepareTransaction( 'transferAsset', [assetId, toAddress], options ); const receipt = await this.sendTransaction(tx); return { success: true, assetId, toAddress, transactionHash: receipt.hash, blockNumber: receipt.blockNumber, explorerUrl: this.getExplorerUrl(receipt.hash) }; } catch (error) { console.error('Failed to transfer asset:', error); throw error; } } /** * Mint token for asset */ async mintToken(assetId, toAddress, amount, metadata = {}) { if (!this.initialized) await this.initialize(); if (!this.contract) throw new Error('Contract not configured'); const metadataUri = await this.uploadMetadata(metadata); try { const tx = await this.prepareTransaction( 'mintToken', [assetId, toAddress, amount, metadataUri] ); const receipt = await this.sendTransaction(tx); // Extract token ID from events const tokenId = this.extractTokenIdFromReceipt(receipt); return { success: true, tokenId, assetId, toAddress, amount, metadataUri, transactionHash: receipt.hash, blockNumber: receipt.blockNumber, explorerUrl: this.getExplorerUrl(receipt.hash) }; } catch (error) { console.error('Failed to mint token:', error); throw error; } } /** * Subscribe to blockchain events */ subscribeToEvents() { if (!this.contract) return; // Asset Created this.contract.on('AssetCreated', (assetId, owner, assetType, timestamp, event) => { this.handleEvent('AssetCreated', { assetId, owner, assetType, timestamp: Number(timestamp), blockNumber: event.log.blockNumber, transactionHash: event.log.transactionHash }); }); // Asset Transferred this.contract.on('AssetTransferred', (assetId, from, to, timestamp, event) => { this.handleEvent('AssetTransferred', { assetId, from, to, timestamp: Number(timestamp), blockNumber: event.log.blockNumber, transactionHash: event.log.transactionHash }); }); // Event Recorded this.contract.on('EventRecorded', (assetId, eventType, actor, timestamp, dataHash, event) => { this.handleEvent('EventRecorded', { assetId, eventType, actor, timestamp: Number(timestamp), dataHash, blockNumber: event.log.blockNumber, transactionHash: event.log.transactionHash }); }); console.log(' ✓ Event listeners configured'); } /** * Handle blockchain event */ handleEvent(eventName, data) { console.log(`📡 Event received: ${eventName}`, data); this.stats.eventsReceived++; // Call registered listeners const listeners = this.eventListeners.get(eventName) || []; for (const listener of listeners) { try { listener(data); } catch (error) { console.error(`Event listener error: ${error.message}`); } } } /** * Register event listener */ on(eventName, callback) { if (!this.eventListeners.has(eventName)) { this.eventListeners.set(eventName, []); } this.eventListeners.get(eventName).push(callback); } /** * Prepare transaction */ async prepareTransaction(method, params, options = {}) { if (!this.contract) throw new Error('Contract not configured'); if (!this.signer) throw new Error('Signer not configured'); // Estimate gas const estimatedGas = await this.contract[method].estimateGas(...params); // Get gas price const feeData = await this.provider.getFeeData(); const gasPrice = feeData.gasPrice; // Check gas price limit const maxGasPrice = ethers.parseUnits(this.config.maxGasPrice.toString(), 'gwei'); if (gasPrice > maxGasPrice) { throw new Error(`Gas price too high: ${ethers.formatUnits(gasPrice, 'gwei')} gwei`); } // Prepare transaction const tx = await this.contract[method].populateTransaction(...params); tx.gasLimit = estimatedGas * BigInt(120) / BigInt(100); // Add 20% buffer tx.gasPrice = gasPrice; if (options.nonce !== undefined) { tx.nonce = options.nonce; } return tx; } /** * Send transaction with retry */ async sendTransaction(tx, attempt = 1) { try { // Send transaction const txResponse = await this.signer.sendTransaction(tx); // Track pending transaction this.pendingTransactions.set(txResponse.hash, { hash: txResponse.hash, startTime: Date.now(), attempt }); console.log(`⏳ Transaction sent: ${txResponse.hash}`); // Wait for confirmation const receipt = await txResponse.wait(this.config.confirmations); // Remove from pending this.pendingTransactions.delete(txResponse.hash); // Update stats this.stats.totalTransactions++; this.stats.successfulTransactions++; this.stats.totalGasUsed += receipt.gasUsed; this.stats.totalCost += receipt.gasUsed * receipt.gasPrice; console.log(`✅ Transaction confirmed: ${receipt.hash}`); return receipt; } catch (error) { if (attempt < this.config.retryAttempts) { console.log(`🔄 Retrying transaction (attempt ${attempt + 1})`); await this.sleep(this.config.retryDelay); return this.sendTransaction(tx, attempt + 1); } this.stats.failedTransactions++; throw error; } } /** * Get blockchain statistics */ async getStats() { const stats = { ...this.stats }; if (this.provider) { stats.currentBlock = await this.provider.getBlockNumber(); stats.network = this.config.network; } if (this.signer) { const address = await this.signer.getAddress(); stats.signerAddress = address; stats.balance = ethers.formatEther(await this.provider.getBalance(address)); } if (this.contract) { try { stats.totalAssetsOnChain = Number(await this.contract.totalAssets()); stats.totalEventsOnChain = Number(await this.contract.totalEvents()); } catch (e) { // Contract may not have these methods } } stats.pendingTransactions = this.pendingTransactions.size; stats.averageGasPrice = this.stats.totalTransactions > 0 ? ethers.formatUnits(this.stats.totalCost / BigInt(this.stats.totalTransactions), 'gwei') : '0'; return stats; } /** * Get default RPC URL for network */ getDefaultRpcUrl(network) { return this.networks[network]?.rpcUrl || 'http://localhost:8545'; } /** * Get explorer URL for transaction */ getExplorerUrl(txHash) { const explorer = this.networks[this.config.network]?.explorer; if (!explorer) return null; return `${explorer}/tx/${txHash}`; } /** * Generate asset ID from data */ generateAssetId(data) { const input = JSON.stringify({ type: data.asset_type, timestamp: Date.now(), random: crypto.randomBytes(16).toString('hex') }); const hash = crypto.createHash('sha256').update(input).digest('hex'); return '0x' + hash.substring(0, 64); } /** * Calculate data hash */ calculateDataHash(data) { const hash = crypto.createHash('sha256'); hash.update(JSON.stringify(data)); return hash.digest('hex'); } /** * Upload metadata to IPFS or other storage */ async uploadMetadata(metadata) { // This would upload to IPFS or other decentralized storage // For now, return a mock URI const hash = this.calculateDataHash(metadata); return `ipfs://${hash}`; } /** * Extract token ID from receipt */ extractTokenIdFromReceipt(receipt) { // Parse TokenMinted event from receipt for (const log of receipt.logs) { try { const parsed = this.contract.interface.parseLog(log); if (parsed.name === 'TokenMinted') { return parsed.args.tokenId.toString(); } } catch (e) { // Not our event } } return null; } /** * Disconnect from blockchain */ async disconnect() { if (this.contract) { this.contract.removeAllListeners(); } this.provider = null; this.signer = null; this.contract = null; this.initialized = false; console.log('🔌 Disconnected from blockchain'); } /** * Sleep helper */ sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } module.exports = { BlockchainEngine };