xache 5.0.0__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.
@@ -0,0 +1,415 @@
1
+ """Identity Service - Agent registration and ownership per LLD §2.2"""
2
+
3
+ import re
4
+ from typing import Optional, Dict, Any, List
5
+ from ..types import (
6
+ RegisterIdentityResponse,
7
+ SubmitClaimRequest,
8
+ SubmitClaimResponse,
9
+ ProcessClaimRequest,
10
+ ProcessClaimResponse,
11
+ PendingClaim,
12
+ PendingClaimByOwner,
13
+ OnChainClaimRequest,
14
+ OnChainClaimResponse,
15
+ )
16
+
17
+
18
+ class IdentityService:
19
+ """Identity service for agent registration and ownership management"""
20
+
21
+ def __init__(self, client):
22
+ self.client = client
23
+
24
+ async def register(
25
+ self,
26
+ wallet_address: str,
27
+ key_type: str,
28
+ chain: str,
29
+ owner_did: Optional[str] = None,
30
+ ) -> RegisterIdentityResponse:
31
+ """
32
+ Register a new agent identity per LLD §2.2
33
+
34
+ Args:
35
+ wallet_address: Wallet address
36
+ key_type: Key type ('evm' or 'solana')
37
+ chain: Chain ('base' or 'solana')
38
+ owner_did: Optional owner DID for SDK Auto-Registration (Option A)
39
+
40
+ Returns:
41
+ RegisterIdentityResponse
42
+
43
+ Example:
44
+ ```python
45
+ # Basic registration
46
+ identity = await client.identity.register(
47
+ wallet_address="0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
48
+ key_type="evm",
49
+ chain="base",
50
+ )
51
+ print(f"DID: {identity.did}")
52
+
53
+ # Option A: SDK Auto-Registration with owner
54
+ identity_with_owner = await client.identity.register(
55
+ wallet_address="0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
56
+ key_type="evm",
57
+ chain="base",
58
+ owner_did="did:owner:evm:0x123...", # Agent automatically linked
59
+ )
60
+ ```
61
+ """
62
+ # Validate request
63
+ self._validate_register_request(wallet_address, key_type, chain)
64
+
65
+ # Build request body
66
+ request_body: Dict[str, Any] = {
67
+ "walletAddress": wallet_address,
68
+ "keyType": key_type,
69
+ "chain": chain,
70
+ }
71
+
72
+ # Add optional owner_did for Option A
73
+ if owner_did:
74
+ request_body["ownerDID"] = owner_did
75
+
76
+ # Make API request (no authentication required for registration)
77
+ response = await self.client.request(
78
+ "POST",
79
+ "/v1/identity/register",
80
+ request_body,
81
+ skip_auth=True,
82
+ )
83
+
84
+ if not response.success or not response.data:
85
+ raise Exception("Identity registration failed")
86
+
87
+ data = response.data
88
+ return RegisterIdentityResponse(
89
+ did=data["did"],
90
+ wallet_address=data["walletAddress"],
91
+ key_type=data["keyType"],
92
+ chain=data["chain"],
93
+ created_at=data["createdAt"],
94
+ )
95
+
96
+ async def submit_claim_request(
97
+ self,
98
+ agent_did: str,
99
+ webhook_url: Optional[str] = None,
100
+ ) -> SubmitClaimResponse:
101
+ """
102
+ Submit ownership claim request (Option B: Async Claim Approval)
103
+
104
+ Args:
105
+ agent_did: Agent DID to claim
106
+ webhook_url: Optional webhook URL for claim notification
107
+
108
+ Returns:
109
+ SubmitClaimResponse
110
+
111
+ Example:
112
+ ```python
113
+ # Submit a claim request for an agent
114
+ claim = await client.identity.submit_claim_request(
115
+ agent_did="did:agent:evm:0x...",
116
+ webhook_url="https://my-app.com/webhooks/claim-notification",
117
+ )
118
+
119
+ print(f"Claim submitted: {claim.status}") # 'pending'
120
+ print(f"Claim ID: {claim.claim_id}")
121
+ ```
122
+ """
123
+ if not agent_did:
124
+ raise ValueError("agent_did is required")
125
+
126
+ request_body: Dict[str, Any] = {"agentDID": agent_did}
127
+ if webhook_url:
128
+ request_body["webhookUrl"] = webhook_url
129
+
130
+ response = await self.client.request(
131
+ "POST",
132
+ "/v1/ownership/claim-request",
133
+ request_body,
134
+ )
135
+
136
+ if not response.success or not response.data:
137
+ raise Exception("Failed to submit claim request")
138
+
139
+ data = response.data
140
+ return SubmitClaimResponse(
141
+ claim_id=data["claimId"],
142
+ status=data["status"],
143
+ message=data["message"],
144
+ )
145
+
146
+ async def process_claim_request(
147
+ self,
148
+ owner_did: str,
149
+ approved: bool,
150
+ owner_signature: Optional[str] = None,
151
+ agent_signature: Optional[str] = None,
152
+ message: Optional[str] = None,
153
+ timestamp: Optional[int] = None,
154
+ rejection_reason: Optional[str] = None,
155
+ ) -> ProcessClaimResponse:
156
+ """
157
+ Process ownership claim (approve/reject) (Option B: Async Claim Approval)
158
+
159
+ Args:
160
+ owner_did: Owner DID
161
+ approved: Whether to approve the claim
162
+ owner_signature: Owner signature (required if approved)
163
+ agent_signature: Agent signature (required if approved)
164
+ message: Optional message
165
+ timestamp: Optional timestamp
166
+ rejection_reason: Rejection reason (if not approved)
167
+
168
+ Returns:
169
+ ProcessClaimResponse
170
+
171
+ Example:
172
+ ```python
173
+ # Agent approves a claim
174
+ result = await agent_client.identity.process_claim_request(
175
+ owner_did="did:owner:evm:0x...",
176
+ approved=True,
177
+ owner_signature="0x...",
178
+ agent_signature="0x...",
179
+ message="Ownership claim approval",
180
+ timestamp=int(time.time() * 1000),
181
+ )
182
+
183
+ print(f"Claim status: {result.status}") # 'approved'
184
+
185
+ # Agent rejects a claim
186
+ rejected = await agent_client.identity.process_claim_request(
187
+ owner_did="did:owner:evm:0x...",
188
+ approved=False,
189
+ rejection_reason="Invalid claim",
190
+ )
191
+
192
+ print(f"Claim status: {rejected.status}") # 'rejected'
193
+ ```
194
+ """
195
+ if not owner_did:
196
+ raise ValueError("owner_did is required")
197
+
198
+ if not isinstance(approved, bool):
199
+ raise ValueError("approved field is required")
200
+
201
+ if approved and (not owner_signature or not agent_signature):
202
+ raise ValueError("Signatures are required when approving a claim")
203
+
204
+ request_body: Dict[str, Any] = {
205
+ "ownerDID": owner_did,
206
+ "approved": approved,
207
+ }
208
+
209
+ if owner_signature:
210
+ request_body["ownerSignature"] = owner_signature
211
+ if agent_signature:
212
+ request_body["agentSignature"] = agent_signature
213
+ if message:
214
+ request_body["message"] = message
215
+ if timestamp:
216
+ request_body["timestamp"] = timestamp
217
+ if rejection_reason:
218
+ request_body["rejectionReason"] = rejection_reason
219
+
220
+ response = await self.client.request(
221
+ "POST",
222
+ "/v1/ownership/claim-process",
223
+ request_body,
224
+ )
225
+
226
+ if not response.success or not response.data:
227
+ raise Exception("Failed to process claim request")
228
+
229
+ data = response.data
230
+ return ProcessClaimResponse(
231
+ status=data["status"],
232
+ message=data["message"],
233
+ )
234
+
235
+ async def get_pending_claims_for_agent(self) -> Dict[str, Any]:
236
+ """
237
+ Get pending claims for the authenticated agent (Option B: Async Claim Approval)
238
+
239
+ Returns:
240
+ Dictionary with 'claims' list and 'count'
241
+
242
+ Example:
243
+ ```python
244
+ # Agent checks pending claims
245
+ pending_claims = await agent_client.identity.get_pending_claims_for_agent()
246
+
247
+ print(f"You have {pending_claims['count']} pending claim(s)")
248
+
249
+ for claim in pending_claims['claims']:
250
+ print(f"Owner: {claim.owner_did}")
251
+ print(f"Requested at: {claim.requested_at}")
252
+ print(f"Webhook: {claim.webhook_url or 'None'}")
253
+ ```
254
+ """
255
+ agent_did = self.client.did
256
+
257
+ response = await self.client.request(
258
+ "GET",
259
+ f"/v1/ownership/pending-claims/{agent_did}",
260
+ )
261
+
262
+ if not response.success or not response.data:
263
+ raise Exception("Failed to get pending claims")
264
+
265
+ data = response.data
266
+ claims_list = [
267
+ PendingClaim(
268
+ claim_id=claim["claimId"],
269
+ owner_did=claim["ownerDID"],
270
+ owner_wallet=claim["ownerWallet"],
271
+ requested_at=claim["requestedAt"],
272
+ webhook_url=claim.get("webhookUrl"),
273
+ )
274
+ for claim in data.get("claims", [])
275
+ ]
276
+
277
+ return {"claims": claims_list, "count": data.get("count", 0)}
278
+
279
+ async def get_pending_claims_by_owner(self) -> Dict[str, Any]:
280
+ """
281
+ Get pending claims by the authenticated owner (Option B: Async Claim Approval)
282
+
283
+ Returns:
284
+ Dictionary with 'claims' list and 'count'
285
+
286
+ Example:
287
+ ```python
288
+ # Owner checks their submitted claims
289
+ my_claims = await owner_client.identity.get_pending_claims_by_owner()
290
+
291
+ print(f"You have submitted {my_claims['count']} claim(s)")
292
+
293
+ for claim in my_claims['claims']:
294
+ print(f"Agent: {claim.agent_did}")
295
+ print(f"Status: {claim.status}")
296
+ print(f"Requested at: {claim.requested_at}")
297
+ ```
298
+ """
299
+ owner_did = self.client.did
300
+
301
+ response = await self.client.request(
302
+ "GET",
303
+ f"/v1/ownership/pending-claims/owner/{owner_did}",
304
+ )
305
+
306
+ if not response.success or not response.data:
307
+ raise Exception("Failed to get pending claims")
308
+
309
+ data = response.data
310
+ claims_list = [
311
+ PendingClaimByOwner(
312
+ agent_did=claim["agentDID"],
313
+ agent_wallet=claim["agentWallet"],
314
+ requested_at=claim["requestedAt"],
315
+ status=claim["status"],
316
+ )
317
+ for claim in data.get("claims", [])
318
+ ]
319
+
320
+ return {"claims": claims_list, "count": data.get("count", 0)}
321
+
322
+ async def claim_on_chain(
323
+ self,
324
+ agent_did: str,
325
+ tx_hash: str,
326
+ chain: str,
327
+ ) -> OnChainClaimResponse:
328
+ """
329
+ Claim agent ownership via on-chain transaction (Option C: On-chain Claiming)
330
+
331
+ Args:
332
+ agent_did: Agent DID to claim
333
+ tx_hash: Transaction hash (Solana signature or EVM tx hash)
334
+ chain: Chain ('solana' or 'base')
335
+
336
+ Returns:
337
+ OnChainClaimResponse
338
+
339
+ Example:
340
+ ```python
341
+ # Claim ownership by providing a Solana transaction hash
342
+ result = await owner_client.identity.claim_on_chain(
343
+ agent_did="did:agent:sol:...",
344
+ tx_hash="5wHu7...", # Solana transaction signature
345
+ chain="solana",
346
+ )
347
+
348
+ print("Ownership claimed via on-chain transaction")
349
+ print(f"Status: {result.status}") # 'approved'
350
+ print(f"Transaction: {result.tx_hash}")
351
+ print(f"Method: {result.method}") # 'onchain-solana'
352
+
353
+ # Claim ownership by providing a Base (EVM) transaction hash
354
+ evm_result = await owner_client.identity.claim_on_chain(
355
+ agent_did="did:agent:evm:0x...",
356
+ tx_hash="0xabc123...", # EVM transaction hash
357
+ chain="base",
358
+ )
359
+
360
+ print("Ownership claimed via Base transaction")
361
+ print(f"Status: {evm_result.status}") # 'approved'
362
+ ```
363
+ """
364
+ if not agent_did:
365
+ raise ValueError("agent_did is required")
366
+
367
+ if not tx_hash:
368
+ raise ValueError("tx_hash is required")
369
+
370
+ if chain not in ["solana", "base"]:
371
+ raise ValueError('chain must be "solana" or "base"')
372
+
373
+ request_body = {
374
+ "agentDID": agent_did,
375
+ "txHash": tx_hash,
376
+ "chain": chain,
377
+ }
378
+
379
+ response = await self.client.request(
380
+ "POST",
381
+ "/v1/ownership/claim-onchain",
382
+ request_body,
383
+ )
384
+
385
+ if not response.success or not response.data:
386
+ raise Exception("Failed to claim ownership on-chain")
387
+
388
+ data = response.data
389
+ return OnChainClaimResponse(
390
+ status=data["status"],
391
+ tx_hash=data["txHash"],
392
+ method=data["method"],
393
+ message=data["message"],
394
+ )
395
+
396
+ def _validate_register_request(
397
+ self, wallet_address: str, key_type: str, chain: str
398
+ ):
399
+ """Validate registration request"""
400
+ if not wallet_address:
401
+ raise ValueError("wallet_address is required")
402
+
403
+ if key_type not in ["evm", "solana"]:
404
+ raise ValueError('key_type must be "evm" or "solana"')
405
+
406
+ if chain not in ["base", "solana"]:
407
+ raise ValueError('chain must be "base" or "solana"')
408
+
409
+ # Validate wallet address format
410
+ if key_type == "evm":
411
+ if not re.match(r"^0x[a-fA-F0-9]{40}$", wallet_address):
412
+ raise ValueError("Invalid EVM wallet address format")
413
+ else:
414
+ if not re.match(r"^[1-9A-HJ-NP-Za-km-z]{32,44}$", wallet_address):
415
+ raise ValueError("Invalid Solana wallet address format")