defarm-sdk
Version:
DeFarm SDK - On-premise blockchain data processing and tokenization engine for agriculture supply chain
454 lines (399 loc) • 12.9 kB
JavaScript
/**
* 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 };