UNPKG

@legacychain/sdk

Version:

Official SDK for interacting with LegacyChain smart contracts and APIs

1,241 lines (1,229 loc) 60.4 kB
import { EventEmitter } from 'eventemitter3'; import { ethers } from 'ethers'; import { http, createPublicClient, createWalletClient, custom } from 'viem'; import axios from 'axios'; import PQueue from 'p-queue'; import NodeCache from 'node-cache'; function defineChain(chain) { return { formatters: undefined, fees: undefined, serializers: undefined, ...chain, }; } const mainnet = /*#__PURE__*/ defineChain({ id: 1, name: 'Ethereum', nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, rpcUrls: { default: { http: ['https://eth.merkle.io'], }, }, blockExplorers: { default: { name: 'Etherscan', url: 'https://etherscan.io', apiUrl: 'https://api.etherscan.io/api', }, }, contracts: { ensRegistry: { address: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e', }, ensUniversalResolver: { address: '0xce01f8eee7E479C928F8919abD53E553a36CeF67', blockCreated: 19_258_213, }, multicall3: { address: '0xca11bde05977b3631167028862be2a173976ca11', blockCreated: 14_353_601, }, }, }); const sepolia = /*#__PURE__*/ defineChain({ id: 11_155_111, name: 'Sepolia', nativeCurrency: { name: 'Sepolia Ether', symbol: 'ETH', decimals: 18 }, rpcUrls: { default: { http: ['https://sepolia.drpc.org'], }, }, blockExplorers: { default: { name: 'Etherscan', url: 'https://sepolia.etherscan.io', apiUrl: 'https://api-sepolia.etherscan.io/api', }, }, contracts: { multicall3: { address: '0xca11bde05977b3631167028862be2a173976ca11', blockCreated: 751532, }, ensRegistry: { address: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e' }, ensUniversalResolver: { address: '0xc8Af999e38273D658BE1b921b88A9Ddf005769cC', blockCreated: 5_317_080, }, }, testnet: true, }); // API Plans and Rate Limiting var PlanType; (function (PlanType) { PlanType["FREE"] = "FREE"; PlanType["STARTER"] = "STARTER"; PlanType["PRO"] = "PRO"; PlanType["ENTERPRISE"] = "ENTERPRISE"; })(PlanType || (PlanType = {})); const PLAN_LIMITS = { [PlanType.FREE]: { requests: { perMinute: 10, perHour: 100, perDay: 500 }, capsules: { maxPerMonth: 5, maxStorage: 50 }, features: { encryption: true, multiSignature: false, advancedAnalytics: false, customBranding: false, prioritySupport: false, webhooks: false } }, [PlanType.STARTER]: { requests: { perMinute: 30, perHour: 500, perDay: 5000 }, capsules: { maxPerMonth: 50, maxStorage: 500 }, features: { encryption: true, multiSignature: true, advancedAnalytics: false, customBranding: false, prioritySupport: false, webhooks: true } }, [PlanType.PRO]: { requests: { perMinute: 100, perHour: 2000, perDay: 20000 }, capsules: { maxPerMonth: 500, maxStorage: 5000 }, features: { encryption: true, multiSignature: true, advancedAnalytics: true, customBranding: true, prioritySupport: true, webhooks: true } }, [PlanType.ENTERPRISE]: { requests: { perMinute: -1, // unlimited perHour: -1, perDay: -1 }, capsules: { maxPerMonth: -1, maxStorage: -1 }, features: { encryption: true, multiSignature: true, advancedAnalytics: true, customBranding: true, prioritySupport: true, webhooks: true } } }; class PaymentVerificationService { constructor(provider) { this.paymentRecords = new Map(); this.apiKeyToAddress = new Map(); this.addressToPlan = new Map(); this.provider = provider || new ethers.JsonRpcProvider(process.env.RPC_URL || 'https://rpc.ankr.com/eth'); // Initialize with some mock data for development if (process.env.NODE_ENV === 'development') { this.initializeMockData(); } } /** * Verify a payment transaction and activate the plan */ async verifyPayment(txHash, expectedPlan, duration = 1) { try { const tx = await this.provider.getTransaction(txHash); if (!tx) { throw new Error('Transaction not found'); } const receipt = await this.provider.getTransactionReceipt(txHash); if (!receipt || receipt.status !== 1) { throw new Error('Transaction failed or not confirmed'); } // Verify payment recipient if (tx.to?.toLowerCase() !== PaymentVerificationService.PAYMENT_ADDRESS.toLowerCase()) { throw new Error('Invalid payment address'); } // Verify amount matches plan pricing const pricing = PaymentVerificationService.PLAN_PRICING[expectedPlan]; const expectedAmount = duration === 12 ? pricing.yearly : (parseFloat(pricing.monthly) * duration).toString(); // For ETH payments, check value if (pricing.token === 'ETH') { const ethAmount = ethers.formatEther(tx.value); // Add some tolerance for ETH price fluctuations (5%) const tolerance = parseFloat(expectedAmount) * 0.05; if (Math.abs(parseFloat(ethAmount) - parseFloat(expectedAmount)) > tolerance) { throw new Error('Incorrect payment amount'); } } else { // For token payments, decode the transfer event // This would need the actual token contract ABI // For now, we'll trust the amount } // Create payment record const paymentRecord = { txHash, payer: tx.from, amount: expectedAmount, token: pricing.token, plan: expectedPlan, duration, timestamp: Date.now(), expiresAt: Date.now() + (duration * 30 * 24 * 60 * 60 * 1000), // duration in months verified: true }; // Store payment record const records = this.paymentRecords.get(tx.from) || []; records.push(paymentRecord); this.paymentRecords.set(tx.from, records); // Update plan this.addressToPlan.set(tx.from, { plan: expectedPlan, expiresAt: paymentRecord.expiresAt }); return true; } catch (error) { console.error('Payment verification failed:', error); return false; } } /** * Get the current plan for an API key */ getPlanForApiKey(apiKey) { const address = this.apiKeyToAddress.get(apiKey); if (!address) { return PlanType.FREE; } const planInfo = this.addressToPlan.get(address); if (!planInfo) { return PlanType.FREE; } // Check if plan is expired if (Date.now() > planInfo.expiresAt) { return PlanType.FREE; } return planInfo.plan; } /** * Link an API key to a wallet address */ linkApiKeyToAddress(apiKey, address) { this.apiKeyToAddress.set(apiKey, address.toLowerCase()); } /** * Get payment history for an address */ getPaymentHistory(address) { return this.paymentRecords.get(address.toLowerCase()) || []; } /** * Get pricing information */ static getPricing() { return this.PLAN_PRICING; } /** * Generate a payment request */ generatePaymentRequest(plan, duration = 1) { const pricing = PaymentVerificationService.PLAN_PRICING[plan]; const amount = duration === 12 ? pricing.yearly : (parseFloat(pricing.monthly) * duration).toString(); return { to: PaymentVerificationService.PAYMENT_ADDRESS, amount, token: pricing.token, data: ethers.id(`LegacyChain:${plan}:${duration}months`).slice(0, 10) // First 4 bytes as identifier }; } /** * Check if a plan is active */ isPlanActive(address) { const planInfo = this.addressToPlan.get(address.toLowerCase()); return planInfo ? Date.now() < planInfo.expiresAt : false; } /** * Get days until plan expires */ getDaysUntilExpiry(address) { const planInfo = this.addressToPlan.get(address.toLowerCase()); if (!planInfo) return 0; const daysRemaining = Math.max(0, planInfo.expiresAt - Date.now()) / (24 * 60 * 60 * 1000); return Math.floor(daysRemaining); } initializeMockData() { // Add some test data for development const testAddress = '0x1234567890123456789012345678901234567890'; const testApiKey = 'test-api-key-pro'; this.linkApiKeyToAddress(testApiKey, testAddress); this.addressToPlan.set(testAddress, { plan: PlanType.PRO, expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000 // 30 days }); console.log('Initialized mock payment data for development'); } } // Pricing in USD (converted to ETH/USDC at payment time) PaymentVerificationService.PLAN_PRICING = { [PlanType.FREE]: { monthly: '0', yearly: '0', token: 'ETH' }, [PlanType.STARTER]: { monthly: '29', // $29/month yearly: '278.4', // $29 * 12 * 0.8 (20% discount) token: 'USDC' }, [PlanType.PRO]: { monthly: '99', // $99/month yearly: '950.4', // $99 * 12 * 0.8 token: 'USDC' }, [PlanType.ENTERPRISE]: { monthly: '499', // $499/month (custom pricing available) yearly: '4790.4', // $499 * 12 * 0.8 token: 'USDC' } }; // Payment address for receiving payments PaymentVerificationService.PAYMENT_ADDRESS = process.env.LEGACYCHAIN_PAYMENT_ADDRESS || '0x742d35Cc6634C0532925a3b844Bc9e7595f6E3D2'; class RateLimiter { constructor(planType = PlanType.FREE, paymentService) { this.cache = new NodeCache({ stdTTL: 86400 }); // 24 hour default TTL this.planType = planType; this.paymentService = paymentService || new PaymentVerificationService(); } async checkLimit(userId) { // Check if user has an active paid plan const activePlan = this.paymentService.getPlanForApiKey(userId); const limits = PLAN_LIMITS[activePlan]; // Enterprise has no limits if (activePlan === PlanType.ENTERPRISE) { return { allowed: true }; } const now = Date.now(); const state = this.getOrCreateState(userId, now); // Reset counters if needed if (now > state.minute.reset) { state.minute = { count: 0, reset: now + 60 * 1000 }; } if (now > state.hour.reset) { state.hour = { count: 0, reset: now + 60 * 60 * 1000 }; } if (now > state.day.reset) { state.day = { count: 0, reset: now + 24 * 60 * 60 * 1000 }; } // Check limits if (state.minute.count >= limits.requests.perMinute) { return { allowed: false, retryAfter: Math.ceil((state.minute.reset - now) / 1000) }; } if (state.hour.count >= limits.requests.perHour) { return { allowed: false, retryAfter: Math.ceil((state.hour.reset - now) / 1000) }; } if (state.day.count >= limits.requests.perDay) { return { allowed: false, retryAfter: Math.ceil((state.day.reset - now) / 1000) }; } // Increment counters state.minute.count++; state.hour.count++; state.day.count++; this.cache.set(userId, state); return { allowed: true }; } async checkCapsuleLimit(userId) { const activePlan = this.paymentService.getPlanForApiKey(userId); const limits = PLAN_LIMITS[activePlan]; if (activePlan === PlanType.ENTERPRISE) { return { allowed: true, remaining: -1 }; } const monthKey = `capsules:${userId}:${new Date().toISOString().slice(0, 7)}`; const count = this.cache.get(monthKey) || 0; if (count >= limits.capsules.maxPerMonth) { return { allowed: false, remaining: 0 }; } return { allowed: true, remaining: limits.capsules.maxPerMonth - count }; } async incrementCapsuleCount(userId) { const monthKey = `capsules:${userId}:${new Date().toISOString().slice(0, 7)}`; const count = this.cache.get(monthKey) || 0; this.cache.set(monthKey, count + 1, 30 * 24 * 60 * 60); // 30 days TTL } async checkStorageLimit(userId, sizeInMB) { const activePlan = this.paymentService.getPlanForApiKey(userId); const limits = PLAN_LIMITS[activePlan]; if (activePlan === PlanType.ENTERPRISE) { return { allowed: true, remaining: -1 }; } const storageKey = `storage:${userId}`; const used = this.cache.get(storageKey) || 0; const newTotal = used + sizeInMB; if (newTotal > limits.capsules.maxStorage) { return { allowed: false, remaining: limits.capsules.maxStorage - used }; } return { allowed: true, remaining: limits.capsules.maxStorage - newTotal }; } async incrementStorage(userId, sizeInMB) { const storageKey = `storage:${userId}`; const used = this.cache.get(storageKey) || 0; this.cache.set(storageKey, used + sizeInMB); } getRemainingLimits(userId) { const activePlan = this.paymentService.getPlanForApiKey(userId); const limits = PLAN_LIMITS[activePlan]; if (activePlan === PlanType.ENTERPRISE) { return PLAN_LIMITS[PlanType.ENTERPRISE]; } const now = Date.now(); const state = this.getOrCreateState(userId, now); const monthKey = `capsules:${userId}:${new Date().toISOString().slice(0, 7)}`; const storageKey = `storage:${userId}`; const capsuleCount = this.cache.get(monthKey) || 0; const storageUsed = this.cache.get(storageKey) || 0; // Create a custom RateLimits object with current usage return { requests: { perMinute: Math.max(0, limits.requests.perMinute - state.minute.count), perHour: Math.max(0, limits.requests.perHour - state.hour.count), perDay: Math.max(0, limits.requests.perDay - state.day.count) }, capsules: { maxPerMonth: Math.max(0, limits.capsules.maxPerMonth - capsuleCount), maxStorage: Math.max(0, limits.capsules.maxStorage - storageUsed) }, features: limits.features }; } changePlan(newPlan) { this.planType = newPlan; } reset(userId) { this.cache.del(userId); this.cache.del(`capsules:${userId}:${new Date().toISOString().slice(0, 7)}`); this.cache.del(`storage:${userId}`); } getPlanInfo(userId) { const plan = this.paymentService.getPlanForApiKey(userId); const address = this.getAddressForApiKey(userId); const daysRemaining = address ? this.paymentService.getDaysUntilExpiry(address) : 0; const isActive = address ? this.paymentService.isPlanActive(address) : false; return { plan, daysRemaining, isActive }; } getAddressForApiKey(apiKey) { // This is a simple implementation - in production, you'd query a database // For now, we'll use a reverse lookup from the payment service // This would need to be implemented properly with a database return undefined; } getOrCreateState(userId, now) { let state = this.cache.get(userId); if (!state) { state = { minute: { count: 0, reset: now + 60 * 1000 }, hour: { count: 0, reset: now + 60 * 60 * 1000 }, day: { count: 0, reset: now + 24 * 60 * 60 * 1000 } }; this.cache.set(userId, state); } return state; } // Middleware function for Express-like frameworks middleware() { return async (req, res, next) => { const userId = req.headers['x-api-key'] || 'anonymous'; const { allowed, retryAfter } = await this.checkLimit(userId); if (!allowed) { res.status(429).json({ success: false, error: { code: 'RATE_LIMIT_EXCEEDED', message: 'Too many requests', details: { retryAfter } } }); return; } // Add rate limit headers const activePlan = this.paymentService.getPlanForApiKey(userId); const remaining = this.getRemainingLimits(userId); res.setHeader('X-RateLimit-Limit', PLAN_LIMITS[activePlan].requests.perMinute); res.setHeader('X-RateLimit-Remaining', remaining.requests.perMinute); res.setHeader('X-RateLimit-Reset', new Date(Date.now() + 60 * 1000).toISOString()); res.setHeader('X-Plan-Type', activePlan); next(); }; } } const LEGACY_NFT_ABI = [ 'function createCapsule(string memory ipfsHash, uint256 unlockTime, address[] memory authorizedAddresses) public returns (uint256)', 'function getCapsuleDetails(uint256 tokenId) public view returns (string memory ipfsHash, uint256 unlockTime, address[] memory authorizedAddresses, bool isUnlocked)', 'function unlockCapsule(uint256 tokenId) public', 'function addAuthorizedAddress(uint256 tokenId, address newAddress) public', 'function removeAuthorizedAddress(uint256 tokenId, address addressToRemove) public', 'function isAuthorized(uint256 tokenId, address user) public view returns (bool)' ]; class TimeCapsuleService { constructor(client) { this.client = client; this.contractAddress = process.env.NEXT_PUBLIC_LEGACY_NFT_ADDRESS || ''; } async create(input) { // Check rate limits const { allowed, remaining } = await this.client['rateLimiter'].checkCapsuleLimit(this.client.getConfig().apiKey); if (!allowed) { throw new Error('Monthly capsule limit reached'); } let ipfsHash; let encryptionMetadata = {}; // Handle encryption if requested if (input.encrypt) { const password = input.password || this.client.crypto.generatePassword(); let contentToEncrypt; if (input.content instanceof File) { contentToEncrypt = await this.fileToBase64(input.content); } else if (Array.isArray(input.content)) { const files = await Promise.all(input.content.map(f => this.fileToBase64(f))); contentToEncrypt = JSON.stringify(files); } else { contentToEncrypt = input.content; } const { encrypted, salt, iv } = await this.client.crypto.encrypt(contentToEncrypt, password); // Upload encrypted content ipfsHash = await this.client.ipfs.uploadJSON({ encrypted, salt, iv, metadata: input.metadata }, input.title, { type: 'time-capsule', encrypted: true }); encryptionMetadata = { encrypted: true, password }; } else { // Upload unencrypted content if (input.content instanceof File) { ipfsHash = await this.client.ipfs.uploadFile(input.content, { type: 'time-capsule', title: input.title, ...input.metadata }); } else if (Array.isArray(input.content)) { // For multiple files, create a JSON manifest const hashes = await Promise.all(input.content.map(file => this.client.ipfs.uploadFile(file))); ipfsHash = await this.client.ipfs.uploadJSON({ files: hashes, metadata: input.metadata }, input.title); } else { ipfsHash = await this.client.ipfs.uploadJSON({ content: input.content, metadata: input.metadata }, input.title); } } // Create on-chain capsule const signer = this.client.getSigner(); const contract = new ethers.Contract(this.contractAddress, LEGACY_NFT_ABI, signer); const unlockTimestamp = Math.floor(input.unlockTime.getTime() / 1000); const tx = await contract.createCapsule(ipfsHash, unlockTimestamp, input.authorizedAddresses || []); const receipt = await tx.wait(); const tokenId = receipt.logs[0].args.tokenId.toString(); // Update rate limiter await this.client['rateLimiter'].incrementCapsuleCount(this.client.getConfig().apiKey); // Create capsule object const capsule = { id: tokenId, ipfsHash, unlockTime: input.unlockTime, authorizedAddresses: input.authorizedAddresses || [], isUnlocked: false, metadata: { title: input.title, description: input.description, type: input.content instanceof File ? 'file' : 'text', encrypted: input.encrypt || false }, createdAt: new Date(), updatedAt: new Date() }; // Store in backend API await this.client.makeRequest('POST', '/capsules', capsule); // Emit event this.client.emit('capsule:created', capsule); return { ...capsule, ...encryptionMetadata }; } async get(capsuleId) { const response = await this.client.makeRequest('GET', `/capsules/${capsuleId}`); if (!response.success || !response.data) { throw new Error(response.error?.message || 'Failed to fetch capsule'); } return response.data; } async list(params) { const queryParams = new URLSearchParams(); if (params) { if (params.page) queryParams.append('page', params.page.toString()); if (params.limit) queryParams.append('limit', params.limit.toString()); if (params.sortBy) queryParams.append('sortBy', params.sortBy); if (params.sortOrder) queryParams.append('sortOrder', params.sortOrder); if (params.filters) { Object.entries(params.filters).forEach(([key, value]) => { if (value !== undefined) { queryParams.append(key, value.toString()); } }); } } const response = await this.client.makeRequest('GET', `/capsules?${queryParams.toString()}`); if (!response.success || !response.data) { throw new Error(response.error?.message || 'Failed to fetch capsules'); } return response.data; } async unlock(capsuleId, password) { // Check on-chain status const provider = this.client.getProvider(); const contract = new ethers.Contract(this.contractAddress, LEGACY_NFT_ABI, provider); const [ipfsHash, unlockTime, , isUnlocked] = await contract.getCapsuleDetails(capsuleId); if (!isUnlocked && Date.now() < unlockTime * 1000) { throw new Error('Capsule is still locked'); } // Unlock on-chain if needed if (!isUnlocked) { const signer = this.client.getSigner(); const contractWithSigner = new ethers.Contract(this.contractAddress, LEGACY_NFT_ABI, signer); const tx = await contractWithSigner.unlockCapsule(capsuleId); await tx.wait(); } // Retrieve content from IPFS const content = await this.client.ipfs.retrieveJSON(ipfsHash); // Decrypt if needed if (content.encrypted) { if (!password) { throw new Error('Password required to decrypt capsule'); } const decrypted = await this.client.crypto.decrypt(content.encrypted, password, content.salt, content.iv); return JSON.parse(decrypted); } this.client.emit('capsule:unlocked', capsuleId); return content; } async addAuthorizedAddress(capsuleId, address) { const signer = this.client.getSigner(); const contract = new ethers.Contract(this.contractAddress, LEGACY_NFT_ABI, signer); const tx = await contract.addAuthorizedAddress(capsuleId, address); await tx.wait(); // Update backend await this.client.makeRequest('POST', `/capsules/${capsuleId}/authorized`, { address }); } async removeAuthorizedAddress(capsuleId, address) { const signer = this.client.getSigner(); const contract = new ethers.Contract(this.contractAddress, LEGACY_NFT_ABI, signer); const tx = await contract.removeAuthorizedAddress(capsuleId, address); await tx.wait(); // Update backend await this.client.makeRequest('DELETE', `/capsules/${capsuleId}/authorized/${address}`); } async isAuthorized(capsuleId, address) { const provider = this.client.getProvider(); const contract = new ethers.Contract(this.contractAddress, LEGACY_NFT_ABI, provider); return contract.isAuthorized(capsuleId, address); } async fileToBase64(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(file); }); } } const LEGACY_PAYMENTS_ABI = [ 'function createPayment(address recipient, uint256 unlockTime, address token, string memory message) public payable returns (uint256)', 'function claimPayment(uint256 paymentId) public', 'function refundPayment(uint256 paymentId) public', 'function getPayment(uint256 paymentId) public view returns (address sender, address recipient, uint256 amount, address token, uint256 unlockTime, bool claimed, string memory message)' ]; class PaymentService { constructor(client) { this.client = client; this.contractAddress = process.env.NEXT_PUBLIC_LEGACY_PAYMENTS_ADDRESS || ''; } async create(input) { const signer = this.client.getSigner(); const contract = new ethers.Contract(this.contractAddress, LEGACY_PAYMENTS_ABI, signer); let message = input.message || ''; let encryptionMetadata = {}; // Encrypt message if requested if (input.encrypt && input.message) { const password = input.password || this.client.crypto.generatePassword(); const { encrypted, salt, iv } = await this.client.crypto.encrypt(input.message, password); // Store encrypted message on IPFS const ipfsHash = await this.client.ipfs.uploadJSON({ encrypted, salt, iv }, 'payment-message'); message = `ipfs://${ipfsHash}`; encryptionMetadata = { encrypted: true, password }; } const unlockTimestamp = Math.floor(input.unlockTime.getTime() / 1000); const value = input.token === 'ETH' ? ethers.parseEther(input.amount) : '0'; const tokenAddress = this.getTokenAddress(input.token); // Approve token if not ETH if (input.token !== 'ETH') { await this.approveToken(tokenAddress, input.amount); } const tx = await contract.createPayment(input.recipient, unlockTimestamp, tokenAddress, message, { value }); const receipt = await tx.wait(); const paymentId = receipt.logs[0].args.paymentId.toString(); const payment = { id: paymentId, sender: await signer.getAddress(), recipient: input.recipient, amount: input.amount, token: input.token, unlockTime: input.unlockTime, status: 'pending', message: input.message, encrypted: input.encrypt, createdAt: new Date() }; // Store in backend await this.client.makeRequest('POST', '/payments', payment); // Emit event this.client.emit('payment:created', payment); return { ...payment, ...encryptionMetadata }; } async claim(paymentId) { const signer = this.client.getSigner(); const contract = new ethers.Contract(this.contractAddress, LEGACY_PAYMENTS_ABI, signer); const tx = await contract.claimPayment(paymentId); await tx.wait(); // Update backend await this.client.makeRequest('PUT', `/payments/${paymentId}/claim`); // Emit event this.client.emit('payment:claimed', paymentId); } async refund(paymentId) { const signer = this.client.getSigner(); const contract = new ethers.Contract(this.contractAddress, LEGACY_PAYMENTS_ABI, signer); const tx = await contract.refundPayment(paymentId); await tx.wait(); // Update backend await this.client.makeRequest('PUT', `/payments/${paymentId}/refund`); } async get(paymentId) { const response = await this.client.makeRequest('GET', `/payments/${paymentId}`); if (!response.success || !response.data) { throw new Error(response.error?.message || 'Failed to fetch payment'); } return response.data; } async list(params) { const queryParams = new URLSearchParams(); if (params) { if (params.page) queryParams.append('page', params.page.toString()); if (params.limit) queryParams.append('limit', params.limit.toString()); if (params.sortBy) queryParams.append('sortBy', params.sortBy); if (params.sortOrder) queryParams.append('sortOrder', params.sortOrder); } const response = await this.client.makeRequest('GET', `/payments?${queryParams.toString()}`); if (!response.success || !response.data) { throw new Error(response.error?.message || 'Failed to fetch payments'); } return response.data; } async getBalance(token) { const signer = this.client.getSigner(); const address = await signer.getAddress(); if (token === 'ETH') { const balance = await signer.provider?.getBalance(address); return ethers.formatEther(balance || '0'); } // For ERC20 tokens const tokenAddress = this.getTokenAddress(token); const tokenContract = new ethers.Contract(tokenAddress, ['function balanceOf(address) view returns (uint256)'], signer); const balance = await tokenContract.balanceOf(address); return ethers.formatUnits(balance, 6); // Assuming 6 decimals for stablecoins } getTokenAddress(token) { const addresses = { 'ETH': ethers.ZeroAddress, 'USDC': '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 'USDT': '0xdAC17F958D2ee523a2206206994597C13D831ec7', 'DAI': '0x6B175474E89094C44Da98b954EedeAC495271d0F' }; return addresses[token] || ethers.ZeroAddress; } async approveToken(tokenAddress, amount) { const signer = this.client.getSigner(); const tokenContract = new ethers.Contract(tokenAddress, ['function approve(address spender, uint256 amount) returns (bool)'], signer); const amountInUnits = ethers.parseUnits(amount, 6); const tx = await tokenContract.approve(this.contractAddress, amountInUnits); await tx.wait(); } async decryptMessage(payment, password) { if (!payment.encrypted || !payment.message?.startsWith('ipfs://')) { return payment.message || ''; } const ipfsHash = payment.message.replace('ipfs://', ''); const encryptedData = await this.client.ipfs.retrieveJSON(ipfsHash); return this.client.crypto.decrypt(encryptedData.encrypted, password, encryptedData.salt, encryptedData.iv); } } const LEGACY_LEGAL_DOCS_ABI = [ 'function createDocument(uint8 docType, string memory documentHash, address[] memory signers, uint256 requiredSignatures, uint256 effectiveDate, uint256 expiryDate) public returns (uint256)', 'function signDocument(uint256 documentId, string memory signature) public', 'function getDocument(uint256 documentId) public view returns (uint8 docType, string memory documentHash, address creator, uint256 requiredSignatures, uint256 signatureCount, uint256 effectiveDate, uint256 expiryDate, uint256 createdAt, bool isActive)', 'function activateSelfDestruct(uint256 documentId, uint8 destructType, uint256 destructValue) public', 'function destroyDocument(uint256 documentId) public', 'function recordView(uint256 documentId) public', 'function isDocumentValid(uint256 documentId) public view returns (bool)' ]; class LegalDocumentService { constructor(client) { this.client = client; this.contractAddress = process.env.NEXT_PUBLIC_LEGACY_LEGAL_DOCS_ADDRESS || ''; } async create(input) { // Check storage limits const fileSizeMB = input.file.size / (1024 * 1024); const { allowed, remaining } = await this.client['rateLimiter'].checkStorageLimit(this.client.getConfig().apiKey, fileSizeMB); if (!allowed) { throw new Error(`Storage limit exceeded. ${remaining}MB remaining`); } let documentHash; let encryptionMetadata = {}; // Handle encryption if requested if (input.encrypt) { const password = input.password || this.client.crypto.generatePassword(); const fileContent = await this.fileToBase64(input.file); const { encrypted, salt, iv } = await this.client.crypto.encrypt(fileContent, password); // Upload encrypted document const blob = new Blob([JSON.stringify({ encrypted, salt, iv })], { type: 'application/json' }); const encryptedFile = new File([blob], input.file.name, { type: 'application/json' }); documentHash = await this.client.ipfs.uploadFile(encryptedFile, { type: 'legal-document', documentType: input.documentType, encrypted: true, originalName: input.file.name, ...input.metadata }); encryptionMetadata = { encrypted: true, password }; } else { // Upload unencrypted document documentHash = await this.client.ipfs.uploadFile(input.file, { type: 'legal-document', documentType: input.documentType, encrypted: false, ...input.metadata }); } // Create on-chain document const signer = this.client.getSigner(); const contract = new ethers.Contract(this.contractAddress, LEGACY_LEGAL_DOCS_ABI, signer); const docTypeMap = { 'NDA': 0, 'CONTRACT': 1, 'WILL': 2, 'AGREEMENT': 3, 'CERTIFICATE': 4 }; const effectiveTimestamp = input.effectiveDate ? Math.floor(input.effectiveDate.getTime() / 1000) : Math.floor(Date.now() / 1000); const expiryTimestamp = input.expiryDate ? Math.floor(input.expiryDate.getTime() / 1000) : 0; const tx = await contract.createDocument(docTypeMap[input.documentType], documentHash, input.signers, input.requiredSignatures || input.signers.length, effectiveTimestamp, expiryTimestamp); const receipt = await tx.wait(); const documentId = receipt.logs[0].args.documentId.toString(); // Set up self-destruct if configured if (input.selfDestruct) { await this.configureSelfDestruct(documentId, input.selfDestruct); } // Update storage counter await this.client['rateLimiter'].incrementStorage(this.client.getConfig().apiKey, fileSizeMB); // Create document object const document = { id: documentId, documentHash, documentType: input.documentType, creator: await signer.getAddress(), signers: input.signers.map(address => ({ address, signed: false })), requiredSignatures: input.requiredSignatures || input.signers.length, status: 'draft', effectiveDate: input.effectiveDate || new Date(), expiryDate: input.expiryDate, selfDestruct: input.selfDestruct, metadata: { title: input.file.name, parties: input.signers, encrypted: input.encrypt || false, fileType: input.file.type, fileSize: input.file.size, checksum: await this.calculateChecksum(input.file) }, createdAt: new Date(), updatedAt: new Date() }; // Store in backend await this.client.makeRequest('POST', '/legal-documents', document); // Emit event this.client.emit('document:created', document); return { ...document, ...encryptionMetadata }; } async sign(documentId, signature) { const signer = this.client.getSigner(); const contract = new ethers.Contract(this.contractAddress, LEGACY_LEGAL_DOCS_ABI, signer); const tx = await contract.signDocument(documentId, signature); await tx.wait(); // Update backend await this.client.makeRequest('POST', `/legal-documents/${documentId}/sign`, { signature }); // Emit event this.client.emit('document:signed', documentId, await signer.getAddress()); } async generateSignature(documentId) { const document = await this.get(documentId); const signer = this.client.getSigner(); const message = `I hereby sign document ${documentId} with hash ${document.documentHash}`; const signature = await signer.signMessage(message); return signature; } async get(documentId) { const response = await this.client.makeRequest('GET', `/legal-documents/${documentId}`); if (!response.success || !response.data) { throw new Error(response.error?.message || 'Failed to fetch document'); } return response.data; } async list(params) { const queryParams = new URLSearchParams(); if (params) { if (params.page) queryParams.append('page', params.page.toString()); if (params.limit) queryParams.append('limit', params.limit.toString()); if (params.sortBy) queryParams.append('sortBy', params.sortBy); if (params.sortOrder) queryParams.append('sortOrder', params.sortOrder); } const response = await this.client.makeRequest('GET', `/legal-documents?${queryParams.toString()}`); if (!response.success || !response.data) { throw new Error(response.error?.message || 'Failed to fetch documents'); } return response.data; } async recordView(documentId) { const signer = this.client.getSigner(); const contract = new ethers.Contract(this.contractAddress, LEGACY_LEGAL_DOCS_ABI, signer); const tx = await contract.recordView(documentId); await tx.wait(); // Update backend await this.client.makeRequest('POST', `/legal-documents/${documentId}/view`); } async destroy(documentId) { const signer = this.client.getSigner(); const contract = new ethers.Contract(this.contractAddress, LEGACY_LEGAL_DOCS_ABI, signer); const tx = await contract.destroyDocument(documentId); await tx.wait(); // Update backend await this.client.makeRequest('DELETE', `/legal-documents/${documentId}`); } async downloadDocument(document, password) { const blob = await this.client.ipfs.retrieveFile(document.documentHash); if (document.metadata?.encrypted) { if (!password) { throw new Error('Password required to decrypt document'); } // Decrypt the document const encryptedData = await blob.text(); const { encrypted, salt, iv } = JSON.parse(encryptedData); const decrypted = await this.client.crypto.decrypt(encrypted, password, salt, iv); // Convert base64 back to blob const binaryString = atob(decrypted.split(',')[1]); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return new Blob([bytes], { type: document.metadata.fileType }); } return blob; } async verifyDocument(documentId) { const provider = this.client.getProvider(); const contract = new ethers.Contract(this.contractAddress, LEGACY_LEGAL_DOCS_ABI, provider); return contract.isDocumentValid(documentId); } async configureSelfDestruct(documentId, config) { const signer = this.client.getSigner(); const contract = new ethers.Contract(this.contractAddress, LEGACY_LEGAL_DOCS_ABI, signer); const destructTypeMap = { 'time': 0, 'views': 1, 'manual': 2 }; const value = config.type === 'time' && config.value instanceof Date ? Math.floor(config.value.getTime() / 1000) : config.value || 0; const tx = await contract.activateSelfDestruct(documentId, destructTypeMap[config.type], value); await tx.wait(); } async fileToBase64(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(file); }); } async calculateChecksum(file) { const buffer = await file.arrayBuffer(); const hashBuffer = await crypto.subtle.digest('SHA-256', buffer); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); } } class AnalyticsService { constructor(client) { this.client = client; } async getOverview() { const response = await this.client.makeRequest('GET', '/analytics/overview'); if (!response.success || !response.data) { throw new Error(response.error?.message || 'Failed to fetch analytics'); } return response.data; } async getCapsuleStats(timeRange) { const params = new URLSearchParams(); if (timeRange) { params.append('startDate', timeRange.start.toISOString()); params.append('endDate', timeRange.end.toISOString()); } const response = await this.client.makeRequest('GET', `/analytics/capsules?${params.toString()}`); if (!response.success || !response.data) { throw new Error(response.error?.message || 'Failed to fetch capsule stats'); } return response.data; } async getPaymentStats(timeRange) { const params = new URLSearchParams(); if (timeRange) { params.append('startDate', timeRange.start.toISOString()); params.append('endDate', timeRange.end.toISOString()); } const response = await this.client.makeRequest('GET', `/analytics/payments?${params.toString()}`); if (!response.success || !response.data) { throw new Error(response.error?.message || 'Failed to fetch payment stats'); } return response.data; } async getDocumentStats(timeRange) { const params = new URLSearchParams(); if (timeRange) { params.append('startDate', timeRange.start.toISOString()); params.append('endDate', timeRange.end.toISOString()); } const response = await this.client.makeRequest('GET', `/analytics/documents?${params.toString()}`); if (!response.success || !response.data) { throw new Error(response.error?.message || 'Failed to fetch document stats'); } return response.data; } async getUsageMetrics() { const response = await this.client.makeRequest('GET', '/analytics/usage'); if (!response.success || !response.data) { throw new Error(response.error?.message || 'Failed to fetch usage metrics'); } return response.data; } async trackEvent(eventName, properties) { await this.client.makeRequest('POST', '/analytics/events', { event: eventName, properties, timestamp: Date.now() }); } async getStorageUsage() { const response = await this.client.makeRequest('GET', '/analytics/storage'); if (!response.success || !response.data) { throw new Error(response.error?.message || 'Failed to fetch storage usage'); } return response.data; } async getRateLimitStatus() { return this.client.getRateLimits(); } async exportData(format, type) { const response = await this.client.makeRequest('POST', '/analytics/export', { format, type }); if (!response.success || !response.data) { throw new Error(response.error?.message || 'Failed to export data'); } // Download the exported file const fileResponse = await fetch(response.data.url); return fileResponse.blob(); } } class IPFSService { constructor(gateway, pinataJwt) { this.pinataUrl = 'https://api.pinata.cloud/pinning/pinFileToIPFS'; this.gateway = gateway || 'https://gateway.pinata.cloud/ipfs/'; this.pinataJwt = pinataJwt; } async uploadFile(file, metadata) { if (!this.pinataJwt) { throw new Error('Pinata JWT token is required for file uploads'); } const formData = new FormData(); formData.append('file', file); if (metadata) { formData.append('pinataMetadata', JSON.stringify({ name: file.name, keyvalues: metadata })); } const response = await axios.post(this.pinataUrl, formData, { headers: { 'Authorization': `Bearer ${this.pinataJwt}`, 'Content-Type': 'multipart/form-data' } }); return response.data.IpfsHash; } async uploadJSON(data, name, metadata) { if (!this.pinataJwt) { throw new Error('Pinata JWT token is required for uploads'); } const response = await axios.post('https://api.pinata.cloud/pinning/pinJSONToIPFS', { pinataContent: data, pinataMetadata: { name, keyvalues: metadata || {} } }, { headers: { 'Authorization': `Bearer ${this.pinataJwt}`, 'Content-Type': 'application/json' } }); return response.data.IpfsHash; } async retrieveFile(ipfsHash) { const url = `${this.gateway}${ipfsHash}`; const response = await axios.get(url, { responseType: 'blob' }); return response.data; } async retrieveJSON(ipfsHash) { const url = `${this.gateway}${ipfsHash}`; const response = await axios.get(url); return response.data; } async unpin(ipfsHash) { if (!this.pinataJwt) { throw new Error('Pinata JWT token is required'); } try { await axios.delete(`https://api.pinata.cloud/pinning/unpin/${ipfsHash}`, { headers: { 'Authorization': `Bearer ${this.pinataJwt}` } }); return true; } catch (error) { console.error('Failed to unpin:', error); return false; } } getGatewayUrl(ipfsHash) { return `${this.gateway}${ipfsHash}`; } setGateway(gateway) { this.gateway = gateway.endsWith('/') ? gateway : `${gateway}/`; } } class CryptoService { constructor() { this.encoder