UNPKG

@gotake/gotake-sdk

Version:

SDK for interacting with GoTake blockchain contracts

319 lines (318 loc) 11 kB
import { BigNumber } from 'ethers'; import { PaymentMethod } from '../types.js'; /** * Utility functions for payment processing */ export class PaymentUtils { /** * Validate payment parameters * @param params Purchase parameters * @returns Validation result with errors if any */ static validatePurchaseParams(params) { const errors = []; // Validate content ID if (!Number.isInteger(params.contentId) || params.contentId <= 0) { errors.push('Content ID must be a positive integer'); } // Validate payment method if (!Object.values(PaymentMethod).includes(params.paymentMethod)) { errors.push('Invalid payment method'); } // Validate token address for ERC20 payments if (params.paymentMethod === PaymentMethod.ERC20) { if (!params.tokenAddress) { errors.push('Token address is required for ERC20 payments'); } else if (!this.isValidAddress(params.tokenAddress)) { errors.push('Invalid token address format'); } } // Validate amount if provided if (params.amount !== undefined) { try { const amount = BigNumber.from(params.amount); if (amount.lte(0)) { errors.push('Amount must be positive'); } } catch (_a) { errors.push('Invalid amount format'); } } return { isValid: errors.length === 0, errors }; } /** * Check if address is valid Ethereum address * @param address Address to check * @returns True if address is valid */ static isValidAddress(address) { const ethers = require('ethers'); try { return ethers.utils.isAddress(address); } catch (_a) { return false; } } /** * Format payment method for display * @param method Payment method * @returns Formatted string */ static formatPaymentMethod(method) { switch (method) { case PaymentMethod.ETH: return 'Ethereum (ETH)'; case PaymentMethod.ERC20: return 'ERC20 Token'; default: return 'Unknown'; } } /** * Get payment method icon/symbol * @param method Payment method * @returns Symbol string */ static getPaymentMethodSymbol(method) { switch (method) { case PaymentMethod.ETH: return 'ETH'; case PaymentMethod.ERC20: return 'ERC20'; default: return '?'; } } /** * Calculate total cost including gas estimate * @param price Content price * @param gasEstimate Estimated gas cost * @param paymentMethod Payment method * @returns Total cost breakdown */ static calculateTotalCost(price, gasEstimate, paymentMethod) { const isETHPayment = paymentMethod === PaymentMethod.ETH; return { contentPrice: price, gasCost: gasEstimate, total: isETHPayment ? price.add(gasEstimate) : gasEstimate, isETHPayment }; } /** * Format purchase result for display * @param result Purchase result * @returns Formatted result object */ static formatPurchaseResult(result) { return { transactionHash: result.transactionHash, shortHash: this.shortenHash(result.transactionHash), contentId: result.contentId, purchaseTime: this.formatTimestamp(result.purchaseTime), viewCount: result.viewCount.toNumber(), validUntil: this.formatTimestamp(result.validUntil), gasUsed: result.gasUsed ? result.gasUsed.toString() : undefined }; } /** * Shorten transaction hash for display * @param hash Full transaction hash * @returns Shortened hash */ static shortenHash(hash) { if (hash.length <= 10) return hash; return `${hash.slice(0, 6)}...${hash.slice(-4)}`; } /** * Format timestamp for display * @param timestamp BigNumber timestamp * @returns Formatted date string */ static formatTimestamp(timestamp) { const date = new Date(timestamp.toNumber() * 1000); return date.toLocaleString(); } /** * Calculate percentage savings * @param originalPrice Original price * @param discountedPrice Discounted price * @returns Savings percentage */ static calculateSavings(originalPrice, discountedPrice) { if (originalPrice.lte(0)) return 0; const savings = originalPrice.sub(discountedPrice); const percentage = savings.mul(10000).div(originalPrice).toNumber() / 100; return Math.max(0, Math.min(100, percentage)); } /** * Compare payment costs across different methods * @param costs Array of cost objects with method and price * @returns Sorted costs with savings information */ static comparePaymentCosts(costs) { // Calculate totals const costsWithTotals = costs.map(cost => { const gasEstimate = cost.gasEstimate || BigNumber.from(0); const totalCost = this.calculateTotalCost(cost.price, gasEstimate, cost.method); return { method: cost.method, price: cost.price, total: totalCost.total, tokenSymbol: cost.tokenSymbol }; }); // Sort by total cost costsWithTotals.sort((a, b) => { if (a.total.lt(b.total)) return -1; if (a.total.gt(b.total)) return 1; return 0; }); // Calculate savings and mark recommended const cheapest = costsWithTotals[0]; return costsWithTotals.map((cost, index) => ({ ...cost, savings: cheapest.total.eq(cost.total) ? 0 : this.calculateSavings(cost.total, cheapest.total), isRecommended: index === 0 })); } /** * Generate payment summary text * @param params Purchase parameters * @param price Content price * @returns Summary text */ static generatePaymentSummary(params, price) { const ethers = require('ethers'); const method = this.formatPaymentMethod(params.paymentMethod); const priceStr = ethers.utils.formatEther(price); let summary = `Purchase content ${params.contentId} for ${priceStr} ETH`; if (params.paymentMethod === PaymentMethod.ERC20 && params.tokenAddress) { summary += ` (paid with ${params.tokenAddress})`; } summary += ` using ${method}`; return summary; } /** * Estimate gas for purchase transaction * @param paymentMethod Payment method * @param hasApproval Whether token approval is needed (for ERC20) * @returns Estimated gas units */ static estimateGasUnits(paymentMethod, hasApproval = true) { switch (paymentMethod) { case PaymentMethod.ETH: return 65000; // Typical gas for ETH purchase case PaymentMethod.ERC20: // Add approval gas if needed const approvalGas = hasApproval ? 0 : 46000; const transferGas = 85000; // Typical gas for ERC20 purchase return approvalGas + transferGas; default: return 100000; // Conservative estimate } } /** * Check if sufficient balance for purchase * @param balance User's balance * @param requiredAmount Required amount * @param gasEstimate Gas cost estimate * @param paymentMethod Payment method * @returns Balance check result */ static checkSufficientBalance(balance, requiredAmount, gasEstimate, paymentMethod) { const totalRequired = paymentMethod === PaymentMethod.ETH ? requiredAmount.add(gasEstimate) : requiredAmount; if (balance.gte(totalRequired)) { return { sufficient: true, message: 'Sufficient balance' }; } const shortage = totalRequired.sub(balance); const ethers = require('ethers'); const shortageStr = ethers.utils.formatEther(shortage); return { sufficient: false, shortage, message: `Insufficient balance. Need ${shortageStr} more ${paymentMethod}` }; } /** * Generate transaction receipt URL for block explorer * @param transactionHash Transaction hash * @param networkId Network ID * @returns Block explorer URL */ static generateExplorerUrl(transactionHash, networkId) { const baseUrls = { 1: 'https://etherscan.io/tx/', 5: 'https://goerli.etherscan.io/tx/', 11155111: 'https://sepolia.etherscan.io/tx/', 8453: 'https://basescan.org/tx/', 84531: 'https://goerli.basescan.org/tx/', 84532: 'https://sepolia.basescan.org/tx/' }; const baseUrl = baseUrls[networkId]; return baseUrl ? `${baseUrl}${transactionHash}` : '#'; } /** * Parse error messages from failed transactions * @param error Error object * @returns User-friendly error message */ static parseTransactionError(error) { if (!error) return 'Unknown error occurred'; const errorMessage = error.message || error.toString(); // Common error patterns const patterns = [ { pattern: /insufficient funds/i, message: 'Insufficient funds to complete transaction' }, { pattern: /user rejected/i, message: 'Transaction was rejected by user' }, { pattern: /gas too low/i, message: 'Gas limit too low for transaction' }, { pattern: /nonce too low/i, message: 'Transaction nonce error. Please try again' }, { pattern: /content not active/i, message: 'Content is not available for purchase' }, { pattern: /already purchased/i, message: 'Content has already been purchased' }, { pattern: /token not supported/i, message: 'Payment token is not supported' } ]; for (const { pattern, message } of patterns) { if (pattern.test(errorMessage)) { return message; } } return 'Transaction failed. Please try again'; } }