@gotake/gotake-sdk
Version:
SDK for interacting with GoTake blockchain contracts
319 lines (318 loc) • 11 kB
JavaScript
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';
}
}