@agentauth/mcp
Version:
Universal payment-enabled MCP gateway for AI agents with native x402 protocol support.
327 lines (326 loc) • 14.9 kB
JavaScript
/*
* 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;
}
}
}