UNPKG

defarm-sdk

Version:

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

454 lines (399 loc) 12.9 kB
/** * Tokenization Relay System * Handles gas-free tokenization for SDK clients * * Architecture: * 1. Client requests tokenization (no private key needed) * 2. SDK generates request and sends to DeFarm relay * 3. DeFarm approves and sponsors the transaction * 4. Token is minted with client as owner */ const crypto = require('crypto'); const axios = require('axios'); class TokenizationRelay { constructor(config = {}) { this.config = { relayUrl: config.relayUrl || process.env.DEFARM_RELAY_URL || 'https://api.defarm.io/relay', apiKey: config.apiKey || process.env.DEFARM_API_KEY, clientId: config.clientId || process.env.DEFARM_CLIENT_ID, network: config.network || 'stellar', // stellar, polygon, etc sponsorshipModel: config.sponsorshipModel || 'defarm', // defarm, client-prepaid, subsidized maxRetries: config.maxRetries || 3, timeout: config.timeout || 30000, ...config }; // Request queue for batching this.pendingRequests = []; this.processingInterval = null; // Statistics this.stats = { totalRequests: 0, approvedRequests: 0, rejectedRequests: 0, pendingRequests: 0, totalGasSaved: 0 }; } /** * Request tokenization without requiring client to pay gas */ async requestTokenization(assetData, clientWallet, options = {}) { console.log('🎫 Requesting gas-free tokenization...'); // Validate client wallet (just the address, no private key) if (!clientWallet || !this.isValidAddress(clientWallet)) { throw new Error('Valid client wallet address required'); } // Create tokenization request const request = { id: this.generateRequestId(), timestamp: Date.now(), client: { id: this.config.clientId, wallet: clientWallet, // No private key ever transmitted! }, asset: { ...assetData, owner: clientWallet }, network: options.network || this.config.network, tokenStandard: options.tokenStandard || this.determineTokenStandard(assetData), metadata: options.metadata || {}, sponsorship: { model: this.config.sponsorshipModel, prepaidCredits: options.prepaidCredits || 0, priority: options.priority || 'normal' }, signature: null // Will be signed by SDK }; // Sign the request (proves it came from authorized SDK) request.signature = this.signRequest(request); // Add to queue or send immediately if (options.batch) { return this.addToQueue(request); } else { return await this.sendRequest(request); } } /** * Send tokenization request to DeFarm relay */ async sendRequest(request, attempt = 1) { try { this.stats.totalRequests++; this.stats.pendingRequests++; const response = await axios.post( `${this.config.relayUrl}/tokenize`, request, { headers: { 'X-API-Key': this.config.apiKey, 'X-Client-ID': this.config.clientId, 'Content-Type': 'application/json' }, timeout: this.config.timeout } ); this.stats.pendingRequests--; if (response.data.status === 'approved') { this.stats.approvedRequests++; console.log('✅ Tokenization approved by DeFarm'); console.log(` Transaction will be sponsored: ${response.data.sponsorAddress}`); console.log(` Estimated gas cost: ${response.data.estimatedGas}`); // Poll for completion return await this.waitForCompletion(response.data.trackingId); } else if (response.data.status === 'queued') { console.log('⏳ Request queued for processing'); return { success: true, status: 'queued', trackingId: response.data.trackingId, estimatedTime: response.data.estimatedTime, position: response.data.queuePosition }; } else { this.stats.rejectedRequests++; throw new Error(`Request rejected: ${response.data.reason}`); } } catch (error) { if (attempt < this.config.maxRetries) { console.log(`🔄 Retrying request (attempt ${attempt + 1})`); await this.sleep(2000 * attempt); return this.sendRequest(request, attempt + 1); } this.stats.pendingRequests--; this.stats.rejectedRequests++; throw error; } } /** * Wait for tokenization to complete */ async waitForCompletion(trackingId, maxWait = 60000) { const startTime = Date.now(); const pollInterval = 2000; while (Date.now() - startTime < maxWait) { try { const response = await axios.get( `${this.config.relayUrl}/status/${trackingId}`, { headers: { 'X-API-Key': this.config.apiKey, 'X-Client-ID': this.config.clientId } } ); const status = response.data; if (status.status === 'completed') { console.log('✅ Tokenization completed!'); // Calculate gas saved for client if (status.gasCost) { this.stats.totalGasSaved += parseFloat(status.gasCost); } return { success: true, tokenId: status.tokenId, transactionHash: status.transactionHash, blockNumber: status.blockNumber, network: status.network, owner: status.owner, gasSaved: status.gasCost, explorerUrl: this.getExplorerUrl(status.network, status.transactionHash) }; } else if (status.status === 'failed') { throw new Error(`Tokenization failed: ${status.error}`); } // Still processing await this.sleep(pollInterval); } catch (error) { console.error('Error checking status:', error.message); await this.sleep(pollInterval); } } throw new Error('Tokenization timeout'); } /** * Batch multiple tokenization requests */ async batchTokenize(assets, clientWallet, options = {}) { console.log(`📦 Batching ${assets.length} tokenization requests...`); const requests = assets.map(asset => ({ id: this.generateRequestId(), asset, clientWallet, options })); const batchRequest = { batchId: this.generateRequestId(), timestamp: Date.now(), client: { id: this.config.clientId, wallet: clientWallet }, requests, network: options.network || this.config.network, sponsorship: { model: this.config.sponsorshipModel, prepaidCredits: options.prepaidCredits || 0, priority: options.priority || 'normal' }, signature: null }; batchRequest.signature = this.signRequest(batchRequest); try { const response = await axios.post( `${this.config.relayUrl}/batch-tokenize`, batchRequest, { headers: { 'X-API-Key': this.config.apiKey, 'X-Client-ID': this.config.clientId, 'Content-Type': 'application/json' }, timeout: this.config.timeout * 2 // Longer timeout for batch } ); console.log(`✅ Batch accepted: ${response.data.batchId}`); console.log(` Total items: ${response.data.totalItems}`); console.log(` Estimated cost: ${response.data.estimatedTotalGas}`); return { success: true, batchId: response.data.batchId, trackingIds: response.data.trackingIds, estimatedTime: response.data.estimatedTime, status: response.data.status }; } catch (error) { console.error('Batch tokenization failed:', error.message); throw error; } } /** * Check client's tokenization quota/credits */ async checkQuota() { try { const response = await axios.get( `${this.config.relayUrl}/quota`, { headers: { 'X-API-Key': this.config.apiKey, 'X-Client-ID': this.config.clientId } } ); return { model: response.data.sponsorshipModel, available: response.data.availableCredits, used: response.data.usedCredits, limit: response.data.creditLimit, resetDate: response.data.resetDate, gasSponsored: response.data.totalGasSponsored }; } catch (error) { console.error('Failed to check quota:', error.message); return null; } } /** * Configure Stellar-specific options */ configureStellar(options = {}) { return { network: 'stellar', assetCode: options.assetCode || 'DEFARM', issuerAccount: options.issuerAccount || 'DEFARM_ISSUER_PUBLIC_KEY', memo: options.memo || '', // Stellar supports native fee sponsorship sponsorship: { model: 'stellar-sponsored', sponsorAccount: options.sponsorAccount || 'DEFARM_SPONSOR_PUBLIC_KEY' } }; } /** * Configure Polygon-specific options */ configurePolygon(options = {}) { return { network: 'polygon', gasPrice: options.gasPrice || 'standard', // standard, fast, instant // Polygon supports meta-transactions (gasless) sponsorship: { model: 'meta-transaction', relayer: options.relayer || 'defarm-relayer' } }; } /** * Sign request to prove authenticity */ signRequest(request) { // This would use a proper signing mechanism // For now, using HMAC for demonstration const hmac = crypto.createHmac('sha256', this.config.apiKey || 'secret'); hmac.update(JSON.stringify(request)); return hmac.digest('hex'); } /** * Validate wallet address format */ isValidAddress(address) { // Basic validation - would be more sophisticated per network if (this.config.network === 'stellar') { return /^G[A-Z0-9]{55}$/.test(address); } else { return /^0x[a-fA-F0-9]{40}$/.test(address); } } /** * Determine token standard based on asset type */ determineTokenStandard(assetData) { const fungibleTypes = ['produce', 'grain', 'commodity']; const uniqueTypes = ['livestock', 'land', 'equipment']; if (uniqueTypes.includes(assetData.asset_type)) { return 'NFT'; // ERC-721 equivalent } else if (fungibleTypes.includes(assetData.asset_type)) { return 'FUNGIBLE'; // ERC-20 equivalent } else { return 'SEMI_FUNGIBLE'; // ERC-1155 equivalent } } /** * Get blockchain explorer URL */ getExplorerUrl(network, txHash) { const explorers = { 'stellar': `https://stellar.expert/explorer/public/tx/${txHash}`, 'stellar-testnet': `https://stellar.expert/explorer/testnet/tx/${txHash}`, 'polygon': `https://polygonscan.com/tx/${txHash}`, 'polygon-mumbai': `https://mumbai.polygonscan.com/tx/${txHash}`, 'ethereum': `https://etherscan.io/tx/${txHash}` }; return explorers[network] || null; } /** * Add request to queue for batch processing */ addToQueue(request) { this.pendingRequests.push(request); // Start processing if not already running if (!this.processingInterval) { this.startBatchProcessor(); } return { queued: true, queuePosition: this.pendingRequests.length, requestId: request.id }; } /** * Start batch processor */ startBatchProcessor() { this.processingInterval = setInterval(async () => { if (this.pendingRequests.length >= 10) { // Batch size threshold const batch = this.pendingRequests.splice(0, 10); await this.processBatch(batch); } }, 5000); // Check every 5 seconds } /** * Process batch of requests */ async processBatch(batch) { const clientWallet = batch[0].asset.owner; const assets = batch.map(req => req.asset); try { await this.batchTokenize(assets, clientWallet); } catch (error) { console.error('Batch processing failed:', error); } } /** * Generate unique request ID */ generateRequestId() { return `REQ-${Date.now()}-${crypto.randomBytes(4).toString('hex').toUpperCase()}`; } /** * Get relay statistics */ getStats() { return { ...this.stats, successRate: this.stats.totalRequests > 0 ? (this.stats.approvedRequests / this.stats.totalRequests * 100).toFixed(2) + '%' : '0%', totalGasSavedFormatted: `${this.stats.totalGasSaved.toFixed(4)} ${this.config.network === 'stellar' ? 'XLM' : 'MATIC'}` }; } /** * Sleep helper */ sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } module.exports = { TokenizationRelay };