agent-mcp 0.1.4__py3-none-any.whl → 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_mcp/__init__.py +66 -12
- agent_mcp/a2a_protocol.py +316 -0
- agent_mcp/agent_lightning_library.py +214 -0
- agent_mcp/claude_mcp_adapter.py +195 -0
- agent_mcp/google_ai_mcp_adapter.py +183 -0
- agent_mcp/llamaindex_mcp_adapter.py +410 -0
- agent_mcp/microsoft_agent_framework.py +591 -0
- agent_mcp/missing_frameworks.py +435 -0
- agent_mcp/openapi_protocol.py +616 -0
- agent_mcp/payments.py +804 -0
- agent_mcp/pydantic_ai_mcp_adapter.py +628 -0
- agent_mcp/registry.py +768 -0
- agent_mcp/security.py +864 -0
- {agent_mcp-0.1.4.dist-info → agent_mcp-0.1.5.dist-info}/METADATA +174 -52
- {agent_mcp-0.1.4.dist-info → agent_mcp-0.1.5.dist-info}/RECORD +19 -6
- {agent_mcp-0.1.4.dist-info → agent_mcp-0.1.5.dist-info}/WHEEL +1 -1
- agent_mcp-0.1.5.dist-info/entry_points.txt +4 -0
- demos/comprehensive_framework_demo.py +202 -0
- agent_mcp-0.1.4.dist-info/entry_points.txt +0 -2
- {agent_mcp-0.1.4.dist-info → agent_mcp-0.1.5.dist-info}/top_level.txt +0 -0
agent_mcp/payments.py
ADDED
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hybrid Payment Gateway for AgentMCP
|
|
3
|
+
Business-friendly payment system supporting both traditional fiat and cryptocurrency
|
|
4
|
+
|
|
5
|
+
This module provides a comprehensive payment solution supporting:
|
|
6
|
+
- Traditional fiat payments via Stripe Connect
|
|
7
|
+
- Cryptocurrency payments via USDC on Base
|
|
8
|
+
- x402 Protocol for agent micropayments
|
|
9
|
+
- Agent Payments Protocol (AP2) integration
|
|
10
|
+
- Escrow services for task completion assurance
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import json
|
|
15
|
+
import uuid
|
|
16
|
+
import hashlib
|
|
17
|
+
import time
|
|
18
|
+
from datetime import datetime, timezone, timedelta
|
|
19
|
+
from typing import Dict, Any, List, Optional, Union, Tuple
|
|
20
|
+
from dataclasses import dataclass, asdict
|
|
21
|
+
from enum import Enum
|
|
22
|
+
import logging
|
|
23
|
+
import aiohttp
|
|
24
|
+
from decimal import Decimal, ROUND_HALF_UP
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
# Try to import payment libraries
|
|
29
|
+
try:
|
|
30
|
+
import stripe
|
|
31
|
+
STRIPE_AVAILABLE = True
|
|
32
|
+
except ImportError:
|
|
33
|
+
STRIPE_AVAILABLE = False
|
|
34
|
+
stripe = None
|
|
35
|
+
logger.warning("Stripe not available. Install with: pip install stripe")
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
from web3 import Web3
|
|
39
|
+
from web3.middleware import geth_poa_middleware
|
|
40
|
+
from eth_account import Account
|
|
41
|
+
WEB3_AVAILABLE = True
|
|
42
|
+
except ImportError:
|
|
43
|
+
WEB3_AVAILABLE = False
|
|
44
|
+
Web3 = None
|
|
45
|
+
Account = None
|
|
46
|
+
logger.warning("Web3 not available. Install with: pip install web3")
|
|
47
|
+
|
|
48
|
+
class PaymentMethod(Enum):
|
|
49
|
+
"""Supported payment methods"""
|
|
50
|
+
STRIPE = "stripe"
|
|
51
|
+
USDC = "usdc"
|
|
52
|
+
X402 = "x402"
|
|
53
|
+
AP2 = "ap2"
|
|
54
|
+
ESCROW = "escrow"
|
|
55
|
+
|
|
56
|
+
class PaymentStatus(Enum):
|
|
57
|
+
"""Payment status tracking"""
|
|
58
|
+
PENDING = "pending"
|
|
59
|
+
PROCESSING = "processing"
|
|
60
|
+
COMPLETED = "completed"
|
|
61
|
+
FAILED = "failed"
|
|
62
|
+
REFUNDED = "refunded"
|
|
63
|
+
ESCROWED = "escrowed"
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class PaymentRequest:
|
|
67
|
+
"""Standardized payment request structure"""
|
|
68
|
+
sender_agent_id: str
|
|
69
|
+
receiver_agent_id: str
|
|
70
|
+
amount: float
|
|
71
|
+
currency: str = "USD"
|
|
72
|
+
task_id: str = None
|
|
73
|
+
description: str = ""
|
|
74
|
+
payment_method: PaymentMethod = PaymentMethod.STRIPE
|
|
75
|
+
metadata: Dict[str, Any] = None
|
|
76
|
+
|
|
77
|
+
def __post_init__(self):
|
|
78
|
+
if self.metadata is None:
|
|
79
|
+
self.metadata = {}
|
|
80
|
+
if self.task_id is None:
|
|
81
|
+
self.task_id = str(uuid.uuid4())
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class PaymentResponse:
|
|
85
|
+
"""Standardized payment response structure"""
|
|
86
|
+
payment_id: str
|
|
87
|
+
status: PaymentStatus
|
|
88
|
+
amount: float
|
|
89
|
+
currency: str
|
|
90
|
+
sender_agent_id: str
|
|
91
|
+
receiver_agent_id: str
|
|
92
|
+
transaction_id: str = None
|
|
93
|
+
block_number: int = None
|
|
94
|
+
created_at: str = None
|
|
95
|
+
completed_at: str = None
|
|
96
|
+
fee: float = 0.0
|
|
97
|
+
error_message: str = None
|
|
98
|
+
metadata: Dict[str, Any] = None
|
|
99
|
+
|
|
100
|
+
def __post_init__(self):
|
|
101
|
+
if self.metadata is None:
|
|
102
|
+
self.metadata = {}
|
|
103
|
+
if self.created_at is None:
|
|
104
|
+
self.created_at = datetime.now(timezone.utc).isoformat()
|
|
105
|
+
|
|
106
|
+
class StripePaymentGateway:
|
|
107
|
+
"""Stripe Connect integration for fiat payments"""
|
|
108
|
+
|
|
109
|
+
def __init__(self, api_key: str, webhook_secret: str = None):
|
|
110
|
+
if not STRIPE_AVAILABLE:
|
|
111
|
+
raise ImportError("Stripe is not installed")
|
|
112
|
+
|
|
113
|
+
stripe.api_key = api_key
|
|
114
|
+
self.api_key = api_key
|
|
115
|
+
self.webhook_secret = webhook_secret
|
|
116
|
+
self.agent_accounts = {}
|
|
117
|
+
|
|
118
|
+
async def create_agent_account(
|
|
119
|
+
self,
|
|
120
|
+
agent_id: str,
|
|
121
|
+
email: str,
|
|
122
|
+
business_name: str = None
|
|
123
|
+
) -> Dict[str, Any]:
|
|
124
|
+
"""Create a Stripe Connect account for an agent"""
|
|
125
|
+
try:
|
|
126
|
+
# Check if account already exists
|
|
127
|
+
if agent_id in self.agent_accounts:
|
|
128
|
+
return {
|
|
129
|
+
"status": "success",
|
|
130
|
+
"account_id": self.agent_accounts[agent_id]["account_id"],
|
|
131
|
+
"message": "Existing account retrieved"
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# Create Express account
|
|
135
|
+
account_data = {
|
|
136
|
+
"type": "express",
|
|
137
|
+
"country": "US",
|
|
138
|
+
"email": email,
|
|
139
|
+
"capabilities": ["card_payments", "transfers"],
|
|
140
|
+
"business_type": "individual",
|
|
141
|
+
"metadata": {
|
|
142
|
+
"agent_id": agent_id,
|
|
143
|
+
"created_by": "AgentMCP"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if business_name:
|
|
148
|
+
account_data["business_profile"] = {
|
|
149
|
+
"name": business_name,
|
|
150
|
+
"product_description": "AI Agent Services"
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
account = stripe.Account.create(**account_data)
|
|
154
|
+
|
|
155
|
+
# Store account info
|
|
156
|
+
self.agent_accounts[agent_id] = {
|
|
157
|
+
"account_id": account.id,
|
|
158
|
+
"email": email,
|
|
159
|
+
"business_name": business_name,
|
|
160
|
+
"created_at": datetime.now(timezone.utc).isoformat()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
"status": "success",
|
|
165
|
+
"account_id": account.id,
|
|
166
|
+
"account_link": account.get("account_links", [{}])[0].get("url"),
|
|
167
|
+
"message": "Stripe Connect account created successfully"
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
logger.error(f"Error creating Stripe account for agent {agent_id}: {e}")
|
|
172
|
+
return {
|
|
173
|
+
"status": "error",
|
|
174
|
+
"message": str(e)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async def process_payment(self, request: PaymentRequest) -> PaymentResponse:
|
|
178
|
+
"""Process a fiat payment via Stripe"""
|
|
179
|
+
try:
|
|
180
|
+
# Get receiver's Stripe account
|
|
181
|
+
receiver_account = await self._get_agent_account(request.receiver_agent_id)
|
|
182
|
+
if not receiver_account:
|
|
183
|
+
return PaymentResponse(
|
|
184
|
+
payment_id=str(uuid.uuid4()),
|
|
185
|
+
status=PaymentStatus.FAILED,
|
|
186
|
+
amount=request.amount,
|
|
187
|
+
currency=request.currency,
|
|
188
|
+
sender_agent_id=request.sender_agent_id,
|
|
189
|
+
receiver_agent_id=request.receiver_agent_id,
|
|
190
|
+
error_message="Receiver account not found"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Create payment intent with transfer
|
|
194
|
+
amount_cents = int(request.amount * 100) # Convert to cents
|
|
195
|
+
|
|
196
|
+
payment_intent = stripe.PaymentIntent.create(
|
|
197
|
+
amount=amount_cents,
|
|
198
|
+
currency=request.currency.lower(),
|
|
199
|
+
transfer_data={
|
|
200
|
+
"destination": receiver_account["account_id"],
|
|
201
|
+
"amount": amount_cents - int(self._calculate_fee(request.amount) * 100) # Subtract fee
|
|
202
|
+
},
|
|
203
|
+
metadata={
|
|
204
|
+
"sender_agent_id": request.sender_agent_id,
|
|
205
|
+
"receiver_agent_id": request.receiver_agent_id,
|
|
206
|
+
"task_id": request.task_id,
|
|
207
|
+
"agentmcp_payment": "true"
|
|
208
|
+
},
|
|
209
|
+
description=request.description or f"Payment from {request.sender_agent_id} to {request.receiver_agent_id}"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
return PaymentResponse(
|
|
213
|
+
payment_id=payment_intent.id,
|
|
214
|
+
status=self._convert_stripe_status(payment_intent.status),
|
|
215
|
+
amount=request.amount,
|
|
216
|
+
currency=request.currency,
|
|
217
|
+
sender_agent_id=request.sender_agent_id,
|
|
218
|
+
receiver_agent_id=request.receiver_agent_id,
|
|
219
|
+
transaction_id=payment_intent.charges.data[0].id if payment_intent.charges.data else None,
|
|
220
|
+
fee=self._calculate_fee(request.amount),
|
|
221
|
+
created_at=datetime.fromtimestamp(payment_intent.created, timezone.utc).isoformat()
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
except Exception as e:
|
|
225
|
+
logger.error(f"Error processing Stripe payment: {e}")
|
|
226
|
+
return PaymentResponse(
|
|
227
|
+
payment_id=str(uuid.uuid4()),
|
|
228
|
+
status=PaymentStatus.FAILED,
|
|
229
|
+
amount=request.amount,
|
|
230
|
+
currency=request.currency,
|
|
231
|
+
sender_agent_id=request.sender_agent_id,
|
|
232
|
+
receiver_agent_id=request.receiver_agent_id,
|
|
233
|
+
error_message=str(e)
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
async def create_escrow_payment(self, request: PaymentRequest) -> PaymentResponse:
|
|
237
|
+
"""Create an escrow payment held until task completion"""
|
|
238
|
+
try:
|
|
239
|
+
amount_cents = int(request.amount * 100)
|
|
240
|
+
|
|
241
|
+
# Create payment intent with manual capture (authorization only)
|
|
242
|
+
payment_intent = stripe.PaymentIntent.create(
|
|
243
|
+
amount=amount_cents,
|
|
244
|
+
currency=request.currency.lower(),
|
|
245
|
+
capture_method="manual", # Don't capture immediately
|
|
246
|
+
metadata={
|
|
247
|
+
"sender_agent_id": request.sender_agent_id,
|
|
248
|
+
"receiver_agent_id": request.receiver_agent_id,
|
|
249
|
+
"task_id": request.task_id,
|
|
250
|
+
"escrow": "true",
|
|
251
|
+
"agentmcp_payment": "true"
|
|
252
|
+
},
|
|
253
|
+
description=f"Escrow payment for task {request.task_id}"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
return PaymentResponse(
|
|
257
|
+
payment_id=payment_intent.id,
|
|
258
|
+
status=PaymentStatus.ESCROWED,
|
|
259
|
+
amount=request.amount,
|
|
260
|
+
currency=request.currency,
|
|
261
|
+
sender_agent_id=request.sender_agent_id,
|
|
262
|
+
receiver_agent_id=request.receiver_agent_id,
|
|
263
|
+
transaction_id=payment_intent.charges.data[0].id if payment_intent.charges.data else None,
|
|
264
|
+
created_at=datetime.fromtimestamp(payment_intent.created, timezone.utc).isoformat(),
|
|
265
|
+
metadata={"escrow_release_required": True}
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
except Exception as e:
|
|
269
|
+
logger.error(f"Error creating Stripe escrow: {e}")
|
|
270
|
+
return PaymentResponse(
|
|
271
|
+
payment_id=str(uuid.uuid4()),
|
|
272
|
+
status=PaymentStatus.FAILED,
|
|
273
|
+
amount=request.amount,
|
|
274
|
+
currency=request.currency,
|
|
275
|
+
sender_agent_id=request.sender_agent_id,
|
|
276
|
+
receiver_agent_id=request.receiver_agent_id,
|
|
277
|
+
error_message=str(e)
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
async def release_escrow(self, payment_id: str) -> PaymentResponse:
|
|
281
|
+
"""Release captured escrow payment"""
|
|
282
|
+
try:
|
|
283
|
+
# Retrieve the payment intent
|
|
284
|
+
payment_intent = stripe.PaymentIntent.retrieve(payment_id)
|
|
285
|
+
|
|
286
|
+
if payment_intent.status != "requires_capture":
|
|
287
|
+
return PaymentResponse(
|
|
288
|
+
payment_id=payment_id,
|
|
289
|
+
status=PaymentStatus.FAILED,
|
|
290
|
+
amount=payment_intent.amount / 100,
|
|
291
|
+
currency=payment_intent.currency.upper(),
|
|
292
|
+
sender_agent_id=payment_intent.metadata.get("sender_agent_id"),
|
|
293
|
+
receiver_agent_id=payment_intent.metadata.get("receiver_agent_id"),
|
|
294
|
+
error_message="Payment is not in escrow"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Capture the payment
|
|
298
|
+
captured_payment = stripe.PaymentIntent.capture(payment_id)
|
|
299
|
+
|
|
300
|
+
return PaymentResponse(
|
|
301
|
+
payment_id=payment_id,
|
|
302
|
+
status=self._convert_stripe_status(captured_payment.status),
|
|
303
|
+
amount=captured_payment.amount / 100,
|
|
304
|
+
currency=captured_payment.currency.upper(),
|
|
305
|
+
sender_agent_id=payment_intent.metadata.get("sender_agent_id"),
|
|
306
|
+
receiver_agent_id=payment_intent.metadata.get("receiver_agent_id"),
|
|
307
|
+
transaction_id=captured_payment.charges.data[0].id if captured_payment.charges.data else None,
|
|
308
|
+
completed_at=datetime.fromtimestamp(captured_payment.created, timezone.utc).isoformat()
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
except Exception as e:
|
|
312
|
+
logger.error(f"Error releasing escrow {payment_id}: {e}")
|
|
313
|
+
return PaymentResponse(
|
|
314
|
+
payment_id=payment_id,
|
|
315
|
+
status=PaymentStatus.FAILED,
|
|
316
|
+
amount=0,
|
|
317
|
+
currency="USD",
|
|
318
|
+
sender_agent_id="unknown",
|
|
319
|
+
receiver_agent_id="unknown",
|
|
320
|
+
error_message=str(e)
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
async def _get_agent_account(self, agent_id: str) -> Optional[Dict[str, Any]]:
|
|
324
|
+
"""Get Stripe account info for an agent"""
|
|
325
|
+
return self.agent_accounts.get(agent_id)
|
|
326
|
+
|
|
327
|
+
def _calculate_fee(self, amount: float) -> float:
|
|
328
|
+
"""Calculate payment fee (2.9% + $0.30)"""
|
|
329
|
+
return round((amount * 0.029) + 0.30, 2)
|
|
330
|
+
|
|
331
|
+
def _convert_stripe_status(self, stripe_status: str) -> PaymentStatus:
|
|
332
|
+
"""Convert Stripe status to PaymentStatus"""
|
|
333
|
+
status_mapping = {
|
|
334
|
+
"requires_payment_method": PaymentStatus.PENDING,
|
|
335
|
+
"requires_confirmation": PaymentStatus.PENDING,
|
|
336
|
+
"requires_action": PaymentStatus.PROCESSING,
|
|
337
|
+
"processing": PaymentStatus.PROCESSING,
|
|
338
|
+
"succeeded": PaymentStatus.COMPLETED,
|
|
339
|
+
"canceled": PaymentStatus.FAILED
|
|
340
|
+
}
|
|
341
|
+
return status_mapping.get(stripe_status, PaymentStatus.FAILED)
|
|
342
|
+
|
|
343
|
+
class USDCPaymentGateway:
|
|
344
|
+
"""USDC payment gateway on Base blockchain"""
|
|
345
|
+
|
|
346
|
+
def __init__(self, rpc_url: str, private_key: str, usdc_contract: str = None):
|
|
347
|
+
if not WEB3_AVAILABLE:
|
|
348
|
+
raise ImportError("Web3 is not installed")
|
|
349
|
+
|
|
350
|
+
self.w3 = Web3(Web3.HTTPProvider(rpc_url))
|
|
351
|
+
self.private_key = private_key
|
|
352
|
+
self.account = Account.from_key(private_key)
|
|
353
|
+
self.address = self.account.address
|
|
354
|
+
|
|
355
|
+
# USDC contract on Base
|
|
356
|
+
self.usdc_address = usdc_contract or "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
357
|
+
|
|
358
|
+
# USDC ABI (minimal for transfers)
|
|
359
|
+
self.usdc_abi = [
|
|
360
|
+
{
|
|
361
|
+
"constant": False,
|
|
362
|
+
"inputs": [
|
|
363
|
+
{"name": "_to", "type": "address"},
|
|
364
|
+
{"name": "_value", "type": "uint256"}
|
|
365
|
+
],
|
|
366
|
+
"name": "transfer",
|
|
367
|
+
"outputs": [{"name": "", "type": "bool"}],
|
|
368
|
+
"payable": False,
|
|
369
|
+
"stateMutability": "nonpayable",
|
|
370
|
+
"type": "function"
|
|
371
|
+
}
|
|
372
|
+
]
|
|
373
|
+
|
|
374
|
+
self.contract = self.w3.eth.contract(
|
|
375
|
+
address=self.usdc_address,
|
|
376
|
+
abi=self.usdc_abi
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
self.agent_wallets = {}
|
|
380
|
+
|
|
381
|
+
async def create_agent_wallet(self, agent_id: str) -> Dict[str, Any]:
|
|
382
|
+
"""Create or derive a wallet for an agent"""
|
|
383
|
+
try:
|
|
384
|
+
if agent_id in self.agent_wallets:
|
|
385
|
+
return {
|
|
386
|
+
"status": "success",
|
|
387
|
+
"address": self.agent_wallets[agent_id]["address"],
|
|
388
|
+
"message": "Existing wallet retrieved"
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
# Derive deterministic wallet from agent_id
|
|
392
|
+
# In production, use HD wallet derivation
|
|
393
|
+
wallet_seed = hashlib.sha256(f"agent:{agent_id}:{self.private_key}".encode()).hexdigest()
|
|
394
|
+
agent_wallet = Account.from_key(wallet_seed)
|
|
395
|
+
|
|
396
|
+
self.agent_wallets[agent_id] = {
|
|
397
|
+
"address": agent_wallet.address,
|
|
398
|
+
"private_key": wallet_seed,
|
|
399
|
+
"created_at": datetime.now(timezone.utc).isoformat()
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
"status": "success",
|
|
404
|
+
"address": agent_wallet.address,
|
|
405
|
+
"message": "Agent wallet created successfully"
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
except Exception as e:
|
|
409
|
+
logger.error(f"Error creating agent wallet: {e}")
|
|
410
|
+
return {
|
|
411
|
+
"status": "error",
|
|
412
|
+
"message": str(e)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async def process_payment(self, request: PaymentRequest) -> PaymentResponse:
|
|
416
|
+
"""Process USDC payment on Base"""
|
|
417
|
+
try:
|
|
418
|
+
# Get receiver wallet
|
|
419
|
+
receiver_wallet = await self._get_agent_wallet(request.receiver_agent_id)
|
|
420
|
+
if not receiver_wallet:
|
|
421
|
+
return PaymentResponse(
|
|
422
|
+
payment_id=str(uuid.uuid4()),
|
|
423
|
+
status=PaymentStatus.FAILED,
|
|
424
|
+
amount=request.amount,
|
|
425
|
+
currency="USDC",
|
|
426
|
+
sender_agent_id=request.sender_agent_id,
|
|
427
|
+
receiver_agent_id=request.receiver_agent_id,
|
|
428
|
+
error_message="Receiver wallet not found"
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
# Convert USDC amount (6 decimals)
|
|
432
|
+
amount_usdc = int(request.amount * 1e6)
|
|
433
|
+
|
|
434
|
+
# Get current gas price
|
|
435
|
+
gas_price = self.w3.eth.gas_price
|
|
436
|
+
gas_limit = 100000 # Estimate for ERC20 transfer
|
|
437
|
+
gas_cost = self.w3.from_wei(gas_price * gas_limit, 'ether')
|
|
438
|
+
|
|
439
|
+
# Build transaction
|
|
440
|
+
transaction = {
|
|
441
|
+
'from': self.address,
|
|
442
|
+
'to': self.usdc_address,
|
|
443
|
+
'gas': gas_limit,
|
|
444
|
+
'gasPrice': gas_price,
|
|
445
|
+
'nonce': self.w3.eth.get_transaction_count(self.address),
|
|
446
|
+
'data': self.contract.encodeABI(
|
|
447
|
+
fn_name='transfer',
|
|
448
|
+
args=[receiver_wallet["address"], amount_usdc]
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
# Sign and send transaction
|
|
453
|
+
signed_txn = self.w3.eth.account.sign_transaction(transaction, self.private_key)
|
|
454
|
+
tx_hash = self.w3.eth.send_raw_transaction(signed_txn.rawTransaction)
|
|
455
|
+
|
|
456
|
+
# Wait for confirmation
|
|
457
|
+
receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
|
|
458
|
+
|
|
459
|
+
if receipt.status == 1:
|
|
460
|
+
return PaymentResponse(
|
|
461
|
+
payment_id=tx_hash.hex(),
|
|
462
|
+
status=PaymentStatus.COMPLETED,
|
|
463
|
+
amount=request.amount,
|
|
464
|
+
currency="USDC",
|
|
465
|
+
sender_agent_id=request.sender_agent_id,
|
|
466
|
+
receiver_agent_id=request.receiver_agent_id,
|
|
467
|
+
transaction_id=tx_hash.hex(),
|
|
468
|
+
block_number=receipt.blockNumber,
|
|
469
|
+
completed_at=datetime.now(timezone.utc).isoformat(),
|
|
470
|
+
fee=float(gas_cost)
|
|
471
|
+
)
|
|
472
|
+
else:
|
|
473
|
+
return PaymentResponse(
|
|
474
|
+
payment_id=tx_hash.hex(),
|
|
475
|
+
status=PaymentStatus.FAILED,
|
|
476
|
+
amount=request.amount,
|
|
477
|
+
currency="USDC",
|
|
478
|
+
sender_agent_id=request.sender_agent_id,
|
|
479
|
+
receiver_agent_id=request.receiver_agent_id,
|
|
480
|
+
transaction_id=tx_hash.hex(),
|
|
481
|
+
error_message="Transaction failed on blockchain"
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
except Exception as e:
|
|
485
|
+
logger.error(f"Error processing USDC payment: {e}")
|
|
486
|
+
return PaymentResponse(
|
|
487
|
+
payment_id=str(uuid.uuid4()),
|
|
488
|
+
status=PaymentStatus.FAILED,
|
|
489
|
+
amount=request.amount,
|
|
490
|
+
currency="USDC",
|
|
491
|
+
sender_agent_id=request.sender_agent_id,
|
|
492
|
+
receiver_agent_id=request.receiver_agent_id,
|
|
493
|
+
error_message=str(e)
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
async def _get_agent_wallet(self, agent_id: str) -> Optional[Dict[str, Any]]:
|
|
497
|
+
"""Get wallet info for an agent"""
|
|
498
|
+
return self.agent_wallets.get(agent_id)
|
|
499
|
+
|
|
500
|
+
class X402PaymentGateway:
|
|
501
|
+
"""x402 Protocol implementation for HTTP 402 payments"""
|
|
502
|
+
|
|
503
|
+
def __init__(self, gateway_url: str, api_key: str = None):
|
|
504
|
+
self.gateway_url = gateway_url
|
|
505
|
+
self.api_key = api_key
|
|
506
|
+
self.session = None
|
|
507
|
+
|
|
508
|
+
async def __aenter__(self):
|
|
509
|
+
self.session = aiohttp.ClientSession()
|
|
510
|
+
return self
|
|
511
|
+
|
|
512
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
513
|
+
if self.session:
|
|
514
|
+
await self.session.close()
|
|
515
|
+
|
|
516
|
+
async def process_payment(self, request: PaymentRequest) -> PaymentResponse:
|
|
517
|
+
"""Process payment via x402 protocol"""
|
|
518
|
+
try:
|
|
519
|
+
if not self.session:
|
|
520
|
+
self.session = aiohttp.ClientSession()
|
|
521
|
+
|
|
522
|
+
# Create x402 payment header
|
|
523
|
+
payment_header = self._create_payment_header(request.amount)
|
|
524
|
+
|
|
525
|
+
# Build x402 request
|
|
526
|
+
x402_request = {
|
|
527
|
+
"receiver": request.receiver_agent_id,
|
|
528
|
+
"amount": str(request.amount),
|
|
529
|
+
"currency": request.currency,
|
|
530
|
+
"task_id": request.task_id,
|
|
531
|
+
"description": request.description,
|
|
532
|
+
"sender": request.sender_agent_id,
|
|
533
|
+
"timestamp": datetime.now(timezone.utc).isoformat()
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
headers = {
|
|
537
|
+
"X-402-Payment": payment_header,
|
|
538
|
+
"Content-Type": "application/json"
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if self.api_key:
|
|
542
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
543
|
+
|
|
544
|
+
async with self.session.post(
|
|
545
|
+
f"{self.gateway_url}/pay",
|
|
546
|
+
json=x402_request,
|
|
547
|
+
headers=headers
|
|
548
|
+
) as response:
|
|
549
|
+
if response.status == 402: # Expected x402 response
|
|
550
|
+
result = await response.json()
|
|
551
|
+
return PaymentResponse(
|
|
552
|
+
payment_id=result.get("payment_id", str(uuid.uuid4())),
|
|
553
|
+
status=self._convert_x402_status(result.get("status", "pending")),
|
|
554
|
+
amount=request.amount,
|
|
555
|
+
currency=request.currency,
|
|
556
|
+
sender_agent_id=request.sender_agent_id,
|
|
557
|
+
receiver_agent_id=request.receiver_agent_id,
|
|
558
|
+
transaction_id=result.get("transaction_id"),
|
|
559
|
+
metadata={"x402_gateway": True}
|
|
560
|
+
)
|
|
561
|
+
else:
|
|
562
|
+
return PaymentResponse(
|
|
563
|
+
payment_id=str(uuid.uuid4()),
|
|
564
|
+
status=PaymentStatus.FAILED,
|
|
565
|
+
amount=request.amount,
|
|
566
|
+
currency=request.currency,
|
|
567
|
+
sender_agent_id=request.sender_agent_id,
|
|
568
|
+
receiver_agent_id=request.receiver_agent_id,
|
|
569
|
+
error_message=f"x402 gateway error: HTTP {response.status}"
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
except Exception as e:
|
|
573
|
+
logger.error(f"Error processing x402 payment: {e}")
|
|
574
|
+
return PaymentResponse(
|
|
575
|
+
payment_id=str(uuid.uuid4()),
|
|
576
|
+
status=PaymentStatus.FAILED,
|
|
577
|
+
amount=request.amount,
|
|
578
|
+
currency=request.currency,
|
|
579
|
+
sender_agent_id=request.sender_agent_id,
|
|
580
|
+
receiver_agent_id=request.receiver_agent_id,
|
|
581
|
+
error_message=str(e)
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
def _create_payment_header(self, amount: float) -> str:
|
|
585
|
+
"""Create x402 payment header"""
|
|
586
|
+
# Simplified x402 header - in production use proper cryptographic signing
|
|
587
|
+
timestamp = str(int(time.time()))
|
|
588
|
+
amount_str = str(amount)
|
|
589
|
+
signature = hashlib.sha256(f"{amount_str}:{timestamp}:{self.api_key}".encode()).hexdigest()
|
|
590
|
+
|
|
591
|
+
return f"x402 amount={amount_str}, ts={timestamp}, sig={signature}"
|
|
592
|
+
|
|
593
|
+
def _convert_x402_status(self, x402_status: str) -> PaymentStatus:
|
|
594
|
+
"""Convert x402 status to PaymentStatus"""
|
|
595
|
+
status_mapping = {
|
|
596
|
+
"pending": PaymentStatus.PENDING,
|
|
597
|
+
"processing": PaymentStatus.PROCESSING,
|
|
598
|
+
"completed": PaymentStatus.COMPLETED,
|
|
599
|
+
"failed": PaymentStatus.FAILED,
|
|
600
|
+
"refunded": PaymentStatus.REFUNDED
|
|
601
|
+
}
|
|
602
|
+
return status_mapping.get(x402_status, PaymentStatus.FAILED)
|
|
603
|
+
|
|
604
|
+
class HybridPaymentGateway:
|
|
605
|
+
"""Unified payment gateway supporting multiple payment methods"""
|
|
606
|
+
|
|
607
|
+
def __init__(
|
|
608
|
+
self,
|
|
609
|
+
stripe_config: Dict[str, str] = None,
|
|
610
|
+
usdc_config: Dict[str, str] = None,
|
|
611
|
+
x402_config: Dict[str, str] = None
|
|
612
|
+
):
|
|
613
|
+
# Initialize payment gateways
|
|
614
|
+
self.stripe_gateway = None
|
|
615
|
+
if stripe_config and stripe_config.get("api_key"):
|
|
616
|
+
self.stripe_gateway = StripePaymentGateway(
|
|
617
|
+
stripe_config["api_key"],
|
|
618
|
+
stripe_config.get("webhook_secret")
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
self.usdc_gateway = None
|
|
622
|
+
if usdc_config and usdc_config.get("rpc_url") and usdc_config.get("private_key"):
|
|
623
|
+
self.usdc_gateway = USDCPaymentGateway(
|
|
624
|
+
usdc_config["rpc_url"],
|
|
625
|
+
usdc_config["private_key"],
|
|
626
|
+
usdc_config.get("usdc_contract")
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
self.x402_gateway = None
|
|
630
|
+
if x402_config and x402_config.get("gateway_url"):
|
|
631
|
+
self.x402_gateway = X402PaymentGateway(
|
|
632
|
+
x402_config["gateway_url"],
|
|
633
|
+
x402_config.get("api_key")
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
self.payment_history = []
|
|
637
|
+
|
|
638
|
+
async def process_payment(self, request: PaymentRequest) -> PaymentResponse:
|
|
639
|
+
"""Process payment using the specified method"""
|
|
640
|
+
try:
|
|
641
|
+
# Log payment attempt
|
|
642
|
+
logger.info(f"Processing payment: {request.sender_agent_id} -> {request.receiver_agent_id}, {request.amount} {request.currency} via {request.payment_method.value}")
|
|
643
|
+
|
|
644
|
+
# Route to appropriate gateway
|
|
645
|
+
if request.payment_method == PaymentMethod.STRIPE and self.stripe_gateway:
|
|
646
|
+
return await self.stripe_gateway.process_payment(request)
|
|
647
|
+
elif request.payment_method == PaymentMethod.USDC and self.usdc_gateway:
|
|
648
|
+
return await self.usdc_gateway.process_payment(request)
|
|
649
|
+
elif request.payment_method == PaymentMethod.X402 and self.x402_gateway:
|
|
650
|
+
async with self.x402_gateway as gateway:
|
|
651
|
+
return await gateway.process_payment(request)
|
|
652
|
+
else:
|
|
653
|
+
return PaymentResponse(
|
|
654
|
+
payment_id=str(uuid.uuid4()),
|
|
655
|
+
status=PaymentStatus.FAILED,
|
|
656
|
+
amount=request.amount,
|
|
657
|
+
currency=request.currency,
|
|
658
|
+
sender_agent_id=request.sender_agent_id,
|
|
659
|
+
receiver_agent_id=request.receiver_agent_id,
|
|
660
|
+
error_message=f"Payment method {request.payment_method.value} not supported or not configured"
|
|
661
|
+
)
|
|
662
|
+
except Exception as e:
|
|
663
|
+
logger.error(f"Error in hybrid payment gateway: {e}")
|
|
664
|
+
return PaymentResponse(
|
|
665
|
+
payment_id=str(uuid.uuid4()),
|
|
666
|
+
status=PaymentStatus.FAILED,
|
|
667
|
+
amount=request.amount,
|
|
668
|
+
currency=request.currency,
|
|
669
|
+
sender_agent_id=request.sender_agent_id,
|
|
670
|
+
receiver_agent_id=request.receiver_agent_id,
|
|
671
|
+
error_message=str(e)
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
async def create_escrow_payment(self, request: PaymentRequest) -> PaymentResponse:
|
|
675
|
+
"""Create escrow payment (Stripe only for now)"""
|
|
676
|
+
if self.stripe_gateway:
|
|
677
|
+
return await self.stripe_gateway.create_escrow_payment(request)
|
|
678
|
+
else:
|
|
679
|
+
return PaymentResponse(
|
|
680
|
+
payment_id=str(uuid.uuid4()),
|
|
681
|
+
status=PaymentStatus.FAILED,
|
|
682
|
+
amount=request.amount,
|
|
683
|
+
currency=request.currency,
|
|
684
|
+
sender_agent_id=request.sender_agent_id,
|
|
685
|
+
receiver_agent_id=request.receiver_agent_id,
|
|
686
|
+
error_message="Escrow not supported for this payment method"
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
async def release_escrow(self, payment_id: str) -> PaymentResponse:
|
|
690
|
+
"""Release escrow payment"""
|
|
691
|
+
if self.stripe_gateway:
|
|
692
|
+
return await self.stripe_gateway.release_escrow(payment_id)
|
|
693
|
+
else:
|
|
694
|
+
return PaymentResponse(
|
|
695
|
+
payment_id=payment_id,
|
|
696
|
+
status=PaymentStatus.FAILED,
|
|
697
|
+
amount=0,
|
|
698
|
+
currency="USD",
|
|
699
|
+
sender_agent_id="unknown",
|
|
700
|
+
receiver_agent_id="unknown",
|
|
701
|
+
error_message="Escrow release not supported"
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
async def setup_agent_accounts(
|
|
705
|
+
self,
|
|
706
|
+
agent_id: str,
|
|
707
|
+
email: str = None,
|
|
708
|
+
business_name: str = None
|
|
709
|
+
) -> Dict[str, Any]:
|
|
710
|
+
"""Setup payment accounts for an agent across all gateways"""
|
|
711
|
+
results = {}
|
|
712
|
+
|
|
713
|
+
# Setup Stripe account
|
|
714
|
+
if self.stripe_gateway:
|
|
715
|
+
stripe_result = await self.stripe_gateway.create_agent_account(
|
|
716
|
+
agent_id, email or f"{agent_id}@agentmcp.com", business_name
|
|
717
|
+
)
|
|
718
|
+
results["stripe"] = stripe_result
|
|
719
|
+
|
|
720
|
+
# Setup USDC wallet
|
|
721
|
+
if self.usdc_gateway:
|
|
722
|
+
usdc_result = await self.usdc_gateway.create_agent_wallet(agent_id)
|
|
723
|
+
results["usdc"] = usdc_result
|
|
724
|
+
|
|
725
|
+
# Store results
|
|
726
|
+
self.payment_history.append({
|
|
727
|
+
"agent_id": agent_id,
|
|
728
|
+
"action": "account_setup",
|
|
729
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
730
|
+
"results": results
|
|
731
|
+
})
|
|
732
|
+
|
|
733
|
+
return {
|
|
734
|
+
"status": "success",
|
|
735
|
+
"agent_id": agent_id,
|
|
736
|
+
"accounts": results
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
def get_supported_methods(self) -> List[Dict[str, Any]]:
|
|
740
|
+
"""Get list of supported payment methods"""
|
|
741
|
+
methods = []
|
|
742
|
+
|
|
743
|
+
if self.stripe_gateway:
|
|
744
|
+
methods.append({
|
|
745
|
+
"method": PaymentMethod.STRIPE.value,
|
|
746
|
+
"display_name": "Stripe (Fiat)",
|
|
747
|
+
"currencies": ["USD", "EUR", "GBP"],
|
|
748
|
+
"fees": "2.9% + $0.30",
|
|
749
|
+
"escrow_supported": True,
|
|
750
|
+
"min_amount": 0.50,
|
|
751
|
+
"max_amount": 999999.99
|
|
752
|
+
})
|
|
753
|
+
|
|
754
|
+
if self.usdc_gateway:
|
|
755
|
+
methods.append({
|
|
756
|
+
"method": PaymentMethod.USDC.value,
|
|
757
|
+
"display_name": "USDC (Base Blockchain)",
|
|
758
|
+
"currencies": ["USDC"],
|
|
759
|
+
"fees": "~$0.01 gas",
|
|
760
|
+
"escrow_supported": False,
|
|
761
|
+
"min_amount": 0.01,
|
|
762
|
+
"max_amount": None
|
|
763
|
+
})
|
|
764
|
+
|
|
765
|
+
if self.x402_gateway:
|
|
766
|
+
methods.append({
|
|
767
|
+
"method": PaymentMethod.X402.value,
|
|
768
|
+
"display_name": "x402 Protocol",
|
|
769
|
+
"currencies": ["USD", "USDC", "EUR"],
|
|
770
|
+
"fees": "Varies by provider",
|
|
771
|
+
"escrow_supported": False,
|
|
772
|
+
"min_amount": 0.01,
|
|
773
|
+
"max_amount": None
|
|
774
|
+
})
|
|
775
|
+
|
|
776
|
+
return methods
|
|
777
|
+
|
|
778
|
+
async def get_payment_history(
|
|
779
|
+
self,
|
|
780
|
+
agent_id: str = None,
|
|
781
|
+
limit: int = 50
|
|
782
|
+
) -> List[Dict[str, Any]]:
|
|
783
|
+
"""Get payment history"""
|
|
784
|
+
history = self.payment_history
|
|
785
|
+
|
|
786
|
+
if agent_id:
|
|
787
|
+
history = [
|
|
788
|
+
record for record in history
|
|
789
|
+
if record.get("sender_agent_id") == agent_id or record.get("receiver_agent_id") == agent_id
|
|
790
|
+
]
|
|
791
|
+
|
|
792
|
+
return sorted(history, key=lambda x: x.get("timestamp", ""), reverse=True)[:limit]
|
|
793
|
+
|
|
794
|
+
# Export classes for easy importing
|
|
795
|
+
__all__ = [
|
|
796
|
+
'PaymentMethod',
|
|
797
|
+
'PaymentStatus',
|
|
798
|
+
'PaymentRequest',
|
|
799
|
+
'PaymentResponse',
|
|
800
|
+
'StripePaymentGateway',
|
|
801
|
+
'USDCPaymentGateway',
|
|
802
|
+
'X402PaymentGateway',
|
|
803
|
+
'HybridPaymentGateway'
|
|
804
|
+
]
|