UNPKG

@agentauth/mcp

Version:

Universal payment-enabled MCP gateway for AI agents with native x402 protocol support.

327 lines (326 loc) 14.9 kB
/* * Copyright (c) 2025 AgentAuth * SPDX-License-Identifier: MIT */ import { AgentPayV002Protocol } from '../protocols/agentpay-v002.js'; import { X402Protocol } from '../protocols/x402.js'; import { ProtocolDetector } from './protocolDetector.js'; import { debugLog } from '../lib/utils.js'; import { ethers } from 'ethers'; /** * Universal payment handler that works with multiple protocols */ export class PaymentHandler { walletService; protocols; protocolDetector; constructor(walletService) { this.walletService = walletService; this.protocols = new Map(); // Initialize protocol detector first this.protocolDetector = new ProtocolDetector(this.protocols); // Register protocols in priority order (x402 first, AgentPay fallback) this.registerProtocol('x402', new X402Protocol()); this.registerProtocol('agentpay-v002', new AgentPayV002Protocol()); debugLog('PaymentHandler initialized with protocols:', this.protocolDetector.getRegisteredProtocols()); } /** * Register a payment protocol * Handles both protocols map and detector registration in single operation */ registerProtocol(name, protocol) { this.protocols.set(name, protocol); this.protocolDetector.registerProtocol(name, protocol); debugLog(`Registered payment protocol: ${name}`); } /** * Check if a message contains a payment request */ isPaymentRequired(message) { const detection = this.protocolDetector.detectProtocol(message); return detection !== null; } /** * Process a payment required response (STATELESS) */ async processPaymentRequired(message, originalRequest) { const detection = this.protocolDetector.detectProtocol(message); if (!detection) { debugLog('No payment protocol detected for message'); return message; } const { protocol: activeProtocol, name: protocolName, extractedData } = detection; debugLog(`Processing payment with ${protocolName} protocol`); // Use pre-extracted data instead of extracting again const paymentDetails = activeProtocol.extractPaymentDetails(extractedData); if (!paymentDetails) { debugLog(`Failed to extract payment details from ${protocolName} response`); return message; } // Configure wallet for the required chain (x402 auto-configuration) if (paymentDetails.transaction?.chainId) { try { this.walletService.configureForChain(paymentDetails.transaction.chainId); debugLog(`Configured wallet for chain ${paymentDetails.transaction.chainId}`); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; debugLog(`Failed to configure wallet for chain: ${errorMessage}`); return this.createErrorResponse(message, `Chain configuration failed: ${errorMessage}`); } } // Get current wallet balances for user transparency let walletBalances; try { walletBalances = await this.walletService.getWalletBalances(); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; debugLog(`Failed to get wallet balances: ${errorMessage}`); return this.createErrorResponse(message, `Unable to check wallet balances: ${errorMessage}`); } const requiredAmount = parseFloat(paymentDetails.amount); const availableUsdc = parseFloat(walletBalances.usdc); const availableEth = parseFloat(walletBalances.eth); // Calculate actual gas requirements using centralized estimation const gasEstimate = await this.estimateTransactionGas(paymentDetails.transaction, availableEth); const hasSufficientFunds = availableUsdc >= requiredAmount && gasEstimate.sufficient; // Format for user and agent with wallet balances (now with real gas estimation) const userDisplay = await activeProtocol.formatForUser(paymentDetails, walletBalances, this.walletService); const agentInstructions = activeProtocol.formatForAgent(paymentDetails, originalRequest ? this.extractRequestParams(originalRequest) : undefined); // Create enhanced STATELESS response for the agent const enhancedResponse = { payment_authorization_required: { user_display: userDisplay, agent_instructions: agentInstructions, wallet_balances: { usdc: walletBalances.usdc, eth: walletBalances.eth, sufficient_funds: hasSufficientFunds }, payment_transaction: paymentDetails.transaction, payment_protocol: protocolName, // Include protocol name for stateless roundtrip cost_breakdown: { payment_amount: `${paymentDetails.amount} ${paymentDetails.currency}`, estimated_gas: `~${gasEstimate.costEth} ETH`, total_cost: `${paymentDetails.amount} ${paymentDetails.currency} + ~${gasEstimate.costEth} ETH gas`, gas_estimation_note: gasEstimate.errorMessage || 'Real-time gas estimate' }, payment_details: { amount: paymentDetails.amount, currency: paymentDetails.currency, description: paymentDetails.description || paymentDetails.message, recipient: paymentDetails.headers?.['x-agentpay-recipient'] || paymentDetails.transaction?.to || 'Unknown', }, }, }; // Return enhanced message return this.createEnhancedMessage(message, enhancedResponse); } /** * Check if a request contains payment authorization (STATELESS) */ hasPaymentAuthorization(message) { // Check for AgentAuth wallet payment authorization parameters if (message && typeof message === 'object' && 'params' in message && message.params) { const params = message.params; // Check for aa_ prefixed parameters and transaction if (params.arguments && (params.arguments.aa_payment_approved === true || params.arguments.aa_payment_approved === 'true') && params.arguments.aa_payment_transaction && params.arguments.aa_payment_protocol) { // Also require protocol return true; } } return false; } /** * Process a payment authorization request (STATELESS) */ async processPaymentAuthorization(message) { if (!('params' in message) || !message.params) { return { error: 'No parameters in message' }; } const params = message.params; const args = params.arguments; if (!args || !args.aa_payment_approved || !args.aa_payment_transaction || !args.aa_payment_protocol) { return { error: 'Missing payment authorization parameters (approved, transaction, or protocol)' }; } // Parse transaction if it's a string (JSON) let transaction = args.aa_payment_transaction; if (typeof transaction === 'string') { try { transaction = JSON.parse(transaction); } catch (error) { return { error: 'Invalid transaction format: unable to parse JSON' }; } } // Get the protocol name from the authorization request const protocolName = args.aa_payment_protocol; debugLog(`Payment authorization using protocol: ${protocolName}`); try { // Find the appropriate protocol (STATELESS - based on client's protocol parameter) const protocol = this.protocols.get(protocolName); if (!protocol) { return { error: `Payment protocol '${protocolName}' not available` }; } // Attempt to fix LLM-truncated transaction data const fixedTransaction = protocol.fixTruncatedTransactionData(transaction); // Validate transaction template (using fixed version) const validation = protocol.validateTransactionTemplate(fixedTransaction); if (!validation.valid) { return { error: `Invalid transaction: ${validation.errors.join(', ')}` }; } // Use the fixed transaction for signing transaction = fixedTransaction; // Check wallet balance const balances = await this.walletService.getWalletBalances(); // Extract actual required amount from transaction data const requiredAmount = this.extractAmountFromTransaction(transaction); const availableUsdc = parseFloat(balances.usdc); const availableEth = parseFloat(balances.eth); if (availableUsdc < requiredAmount) { return { error: `Insufficient USDC balance. Required: ${requiredAmount}, Available: ${availableUsdc}` }; } // Calculate actual gas cost for this transaction using centralized estimation const gasEstimate = await this.estimateTransactionGas(transaction, availableEth); if (!gasEstimate.sufficient) { return { error: `Insufficient ETH for gas. Required: ~${gasEstimate.costEth} ETH, Available: ${availableEth}${gasEstimate.errorMessage ? ` (${gasEstimate.errorMessage})` : ''}` }; } // Sign the transaction const { signedTx, from } = await protocol.signPaymentTransaction(transaction, this.walletService); // Create authorization headers (may be async for x402 EIP-712 signing) const headers = await Promise.resolve(protocol.createAuthorizationHeaders(signedTx, from, this.walletService)); debugLog('Payment authorization successful, headers created'); return { headers }; } catch (error) { debugLog('Payment authorization error:', error); return { error: error instanceof Error ? error.message : 'Payment processing failed', }; } } /** * Extract request parameters */ extractRequestParams(message) { if ('params' in message && message.params) { return message.params; } return null; } /** * Create an enhanced message with payment info */ createEnhancedMessage(original, enhancement) { // For error responses if ('error' in original) { return { ...original, error: { code: -32001, message: 'Payment authorization required', data: enhancement, }, }; } // For result responses return { ...original, result: { content: [ { type: 'text', text: JSON.stringify(enhancement, null, 2), }, ], }, }; } /** * Create an error response for payment failures */ createErrorResponse(original, errorMessage) { return { jsonrpc: '2.0', id: 'id' in original ? original.id : 'error', error: { code: -32001, message: 'Payment processing failed', data: { error_type: 'payment_configuration_failed', message: errorMessage, instructions: 'Please check your configuration and try again.' } } }; } /** * Estimate gas cost for transaction with robust error handling * Returns { costEth: string, sufficient: boolean, errorMessage?: string } */ async estimateTransactionGas(transaction, availableEth) { const fallbackGasCost = '0.00005'; // Realistic fallback for ERC-20 transfers on Base network (aligns with 50k gas limit) if (!transaction) { return { costEth: fallbackGasCost, sufficient: availableEth > parseFloat(fallbackGasCost), errorMessage: 'No transaction provided for gas estimation' }; } try { // Configure wallet for transaction's chain if needed if (transaction.chainId && this.walletService.getCurrentChainId() !== transaction.chainId) { this.walletService.configureForChain(transaction.chainId); } const gasCostEth = await this.walletService.estimateTxCost(transaction); const gasCostNumber = parseFloat(gasCostEth); debugLog(`Gas estimation successful: ${gasCostEth} ETH for transaction`); return { costEth: gasCostEth, sufficient: availableEth > gasCostNumber }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; debugLog(`Gas estimation failed, using fallback: ${errorMessage}`); return { costEth: fallbackGasCost, sufficient: availableEth > parseFloat(fallbackGasCost), errorMessage: `Gas estimation failed: ${errorMessage}` }; } } /** * Extract USDC amount from transaction data */ extractAmountFromTransaction(transaction) { try { // For ERC-20 transfer transactions, extract amount from transaction data if (transaction.data && transaction.data.startsWith('0xa9059cbb')) { // Decode ERC-20 transfer data: transfer(address to, uint256 amount) const amountHex = transaction.data.slice(-64); // Last 64 hex chars = amount const amountWei = BigInt('0x' + amountHex); const amountUsdc = parseFloat(ethers.formatUnits(amountWei, 6)); // USDC has 6 decimals debugLog(`Extracted amount from transaction: ${amountWei} atomic units = ${amountUsdc} USDC`); return amountUsdc; } else { debugLog('Transaction data format not recognized for amount extraction'); return 0; } } catch (error) { debugLog('Error extracting amount from transaction:', error); return 0; } } }