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.
- xache/__init__.py +142 -0
- xache/client.py +331 -0
- xache/crypto/__init__.py +17 -0
- xache/crypto/signing.py +244 -0
- xache/crypto/wallet.py +240 -0
- xache/errors.py +184 -0
- xache/payment/__init__.py +5 -0
- xache/payment/handler.py +244 -0
- xache/services/__init__.py +29 -0
- xache/services/budget.py +285 -0
- xache/services/collective.py +174 -0
- xache/services/extraction.py +173 -0
- xache/services/facilitator.py +296 -0
- xache/services/identity.py +415 -0
- xache/services/memory.py +401 -0
- xache/services/owner.py +293 -0
- xache/services/receipts.py +202 -0
- xache/services/reputation.py +274 -0
- xache/services/royalty.py +290 -0
- xache/services/sessions.py +268 -0
- xache/services/workspaces.py +447 -0
- xache/types.py +399 -0
- xache/utils/__init__.py +5 -0
- xache/utils/cache.py +214 -0
- xache/utils/http.py +209 -0
- xache/utils/retry.py +101 -0
- xache-5.0.0.dist-info/METADATA +337 -0
- xache-5.0.0.dist-info/RECORD +30 -0
- xache-5.0.0.dist-info/WHEEL +5 -0
- xache-5.0.0.dist-info/top_level.txt +1 -0
xache/crypto/signing.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Request signing utilities per LLD §2.1
|
|
3
|
+
Signature format: METHOD\nPATH\nSHA256(body)\nX-Ts\nX-Agent-DID
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import re
|
|
8
|
+
import time
|
|
9
|
+
from typing import Dict
|
|
10
|
+
|
|
11
|
+
from eth_account import Account
|
|
12
|
+
from eth_account.messages import encode_defunct
|
|
13
|
+
from solders.keypair import Keypair # type: ignore
|
|
14
|
+
from solders.pubkey import Pubkey # type: ignore
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def sign_request(
|
|
18
|
+
method: str,
|
|
19
|
+
path: str,
|
|
20
|
+
body: str,
|
|
21
|
+
timestamp: int,
|
|
22
|
+
did: str,
|
|
23
|
+
private_key: str,
|
|
24
|
+
) -> str:
|
|
25
|
+
"""
|
|
26
|
+
Sign a request per LLD §2.1
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
method: HTTP method (GET, POST, etc.)
|
|
30
|
+
path: URL path (e.g., /v1/memory/store)
|
|
31
|
+
body: Request body (empty string for GET)
|
|
32
|
+
timestamp: Unix timestamp in milliseconds
|
|
33
|
+
did: Agent DID
|
|
34
|
+
private_key: Private key (hex string without 0x prefix)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Signature (hex string)
|
|
38
|
+
"""
|
|
39
|
+
# Create signature message per LLD §2.1
|
|
40
|
+
message = create_signature_message(method, path, body, timestamp, did)
|
|
41
|
+
|
|
42
|
+
# Sign message with private key
|
|
43
|
+
signature = sign_message(message, private_key, did)
|
|
44
|
+
|
|
45
|
+
return signature
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def create_signature_message(
|
|
49
|
+
method: str,
|
|
50
|
+
path: str,
|
|
51
|
+
body: str,
|
|
52
|
+
timestamp: int,
|
|
53
|
+
did: str,
|
|
54
|
+
) -> str:
|
|
55
|
+
"""
|
|
56
|
+
Create signature message per LLD §2.1
|
|
57
|
+
Format: METHOD\nPATH\nSHA256(body)\nX-Ts\nX-Agent-DID
|
|
58
|
+
"""
|
|
59
|
+
# Calculate SHA256 of body
|
|
60
|
+
body_hash = hashlib.sha256(body.encode('utf-8')).hexdigest()
|
|
61
|
+
|
|
62
|
+
# Build message per LLD §2.1
|
|
63
|
+
message = f"{method}\n{path}\n{body_hash}\n{timestamp}\n{did}"
|
|
64
|
+
|
|
65
|
+
return message
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def sign_message(message: str, private_key: str, did: str) -> str:
|
|
69
|
+
"""
|
|
70
|
+
Sign a message with private key
|
|
71
|
+
Supports both EVM (secp256k1) and Solana (ed25519) signing
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
message: Message to sign
|
|
75
|
+
private_key: Private key (hex string without 0x prefix)
|
|
76
|
+
did: Agent DID (used to determine signing algorithm)
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Signature (hex string without 0x prefix)
|
|
80
|
+
"""
|
|
81
|
+
# Remove 0x prefix if present
|
|
82
|
+
clean_key = private_key[2:] if private_key.startswith('0x') else private_key
|
|
83
|
+
|
|
84
|
+
# Determine signing method based on DID
|
|
85
|
+
if ':evm:' in did:
|
|
86
|
+
# EVM signing using eth_account (secp256k1)
|
|
87
|
+
return sign_message_evm(message, clean_key)
|
|
88
|
+
elif ':sol:' in did:
|
|
89
|
+
# Solana signing using ed25519
|
|
90
|
+
return sign_message_solana(message, clean_key)
|
|
91
|
+
else:
|
|
92
|
+
raise ValueError(f"Unsupported DID type in: {did}")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def sign_message_evm(message: str, private_key: str) -> str:
|
|
96
|
+
"""
|
|
97
|
+
Sign message using EVM secp256k1 (eth_account)
|
|
98
|
+
"""
|
|
99
|
+
try:
|
|
100
|
+
# Create account from private key
|
|
101
|
+
account = Account.from_key(f"0x{private_key}")
|
|
102
|
+
|
|
103
|
+
# Encode message for Ethereum signing
|
|
104
|
+
message_hash = encode_defunct(text=message)
|
|
105
|
+
|
|
106
|
+
# Sign the message
|
|
107
|
+
signed = account.sign_message(message_hash)
|
|
108
|
+
|
|
109
|
+
# Return signature as hex without 0x prefix
|
|
110
|
+
return signed.signature.hex()[2:]
|
|
111
|
+
except Exception as e:
|
|
112
|
+
raise ValueError(f"EVM signing failed: {str(e)}")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def sign_message_solana(message: str, private_key: str) -> str:
|
|
116
|
+
"""
|
|
117
|
+
Sign message using Solana ed25519
|
|
118
|
+
"""
|
|
119
|
+
try:
|
|
120
|
+
# Convert hex private key to bytes
|
|
121
|
+
private_key_bytes = bytes.fromhex(private_key)
|
|
122
|
+
|
|
123
|
+
# Create keypair from secret key
|
|
124
|
+
# Solana keypairs are 64 bytes (32-byte secret + 32-byte public)
|
|
125
|
+
if len(private_key_bytes) == 32:
|
|
126
|
+
# If only 32 bytes, treat as seed
|
|
127
|
+
keypair = Keypair.from_seed(private_key_bytes)
|
|
128
|
+
elif len(private_key_bytes) == 64:
|
|
129
|
+
# Full keypair
|
|
130
|
+
keypair = Keypair.from_bytes(private_key_bytes)
|
|
131
|
+
else:
|
|
132
|
+
raise ValueError(f"Invalid Solana private key length: {len(private_key_bytes)} bytes")
|
|
133
|
+
|
|
134
|
+
# Sign the message
|
|
135
|
+
message_bytes = message.encode('utf-8')
|
|
136
|
+
signature = keypair.sign_message(message_bytes)
|
|
137
|
+
|
|
138
|
+
# Return signature as hex
|
|
139
|
+
return bytes(signature).hex()
|
|
140
|
+
except Exception as e:
|
|
141
|
+
raise ValueError(f"Solana signing failed: {str(e)}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def validate_did(did: str) -> bool:
|
|
145
|
+
"""
|
|
146
|
+
Validate DID format per LLD §2.1
|
|
147
|
+
Format: did:agent:<evm|sol>:<address>
|
|
148
|
+
- EVM: did:agent:evm:0x[40 hex chars]
|
|
149
|
+
- Solana: did:agent:sol:[32-44 base58 chars]
|
|
150
|
+
"""
|
|
151
|
+
# Check basic structure
|
|
152
|
+
if not did.startswith('did:agent:'):
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
parts = did.split(':')
|
|
156
|
+
if len(parts) != 4:
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
_, _, chain, address = parts
|
|
160
|
+
|
|
161
|
+
if chain == 'evm':
|
|
162
|
+
# EVM addresses: 0x followed by 40 hex characters
|
|
163
|
+
return bool(re.match(r'^0x[a-fA-F0-9]{40}$', address))
|
|
164
|
+
elif chain == 'sol':
|
|
165
|
+
# Solana addresses: base58 string (32-44 characters typical)
|
|
166
|
+
return bool(re.match(r'^[1-9A-HJ-NP-Za-km-z]{32,44}$', address))
|
|
167
|
+
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def validate_timestamp(timestamp: int) -> bool:
|
|
172
|
+
"""
|
|
173
|
+
Validate timestamp is within acceptable window (±300s per LLD §2.1)
|
|
174
|
+
"""
|
|
175
|
+
now = int(time.time() * 1000)
|
|
176
|
+
diff = abs(now - timestamp)
|
|
177
|
+
return diff <= 300000 # 5 minutes = 300 seconds = 300,000ms
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def generate_auth_headers(
|
|
181
|
+
method: str,
|
|
182
|
+
path: str,
|
|
183
|
+
body: str,
|
|
184
|
+
did: str,
|
|
185
|
+
private_key: str,
|
|
186
|
+
) -> Dict[str, str]:
|
|
187
|
+
"""
|
|
188
|
+
Generate authentication headers per LLD §2.1
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
method: HTTP method
|
|
192
|
+
path: URL path
|
|
193
|
+
body: Request body (empty string for GET)
|
|
194
|
+
did: Agent DID
|
|
195
|
+
private_key: Private key (hex string)
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Authentication headers
|
|
199
|
+
"""
|
|
200
|
+
# Validate DID format
|
|
201
|
+
if not validate_did(did):
|
|
202
|
+
raise ValueError(f"Invalid DID format: {did}")
|
|
203
|
+
|
|
204
|
+
# Generate timestamp
|
|
205
|
+
timestamp = int(time.time() * 1000)
|
|
206
|
+
|
|
207
|
+
# Validate timestamp
|
|
208
|
+
if not validate_timestamp(timestamp):
|
|
209
|
+
raise ValueError("Generated timestamp is outside acceptable window")
|
|
210
|
+
|
|
211
|
+
# Sign request
|
|
212
|
+
signature = sign_request(method, path, body, timestamp, did, private_key)
|
|
213
|
+
|
|
214
|
+
# Return headers per LLD §2.1
|
|
215
|
+
return {
|
|
216
|
+
"X-Agent-DID": did,
|
|
217
|
+
"X-Sig": signature,
|
|
218
|
+
"X-Ts": str(timestamp),
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def derive_evm_address(private_key: str) -> str:
|
|
223
|
+
"""
|
|
224
|
+
Derive Ethereum address from private key
|
|
225
|
+
"""
|
|
226
|
+
clean_key = private_key[2:] if private_key.startswith('0x') else private_key
|
|
227
|
+
account = Account.from_key(f"0x{clean_key}")
|
|
228
|
+
return account.address
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def derive_solana_address(private_key: str) -> str:
|
|
232
|
+
"""
|
|
233
|
+
Derive Solana address from private key
|
|
234
|
+
"""
|
|
235
|
+
private_key_bytes = bytes.fromhex(private_key)
|
|
236
|
+
|
|
237
|
+
if len(private_key_bytes) == 32:
|
|
238
|
+
keypair = Keypair.from_seed(private_key_bytes)
|
|
239
|
+
elif len(private_key_bytes) == 64:
|
|
240
|
+
keypair = Keypair.from_bytes(private_key_bytes)
|
|
241
|
+
else:
|
|
242
|
+
raise ValueError(f"Invalid Solana private key length: {len(private_key_bytes)} bytes")
|
|
243
|
+
|
|
244
|
+
return str(keypair.pubkey())
|
xache/crypto/wallet.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wallet Generation Utilities
|
|
3
|
+
Production-ready BIP-39 compliant wallet generation for EVM and Solana
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Optional, Literal
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from mnemonic import Mnemonic
|
|
9
|
+
from eth_account import Account
|
|
10
|
+
from bip_utils import (
|
|
11
|
+
Bip39MnemonicGenerator,
|
|
12
|
+
Bip39SeedGenerator,
|
|
13
|
+
Bip39WordsNum,
|
|
14
|
+
Bip44,
|
|
15
|
+
Bip44Coins,
|
|
16
|
+
Bip44Changes
|
|
17
|
+
)
|
|
18
|
+
import nacl.signing
|
|
19
|
+
import base58
|
|
20
|
+
|
|
21
|
+
KeyType = Literal['evm', 'solana']
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class WalletGenerationResult:
|
|
26
|
+
"""Wallet generation result"""
|
|
27
|
+
did: str # DID format: did:agent:<evm|sol>:<address>
|
|
28
|
+
address: str # Wallet address
|
|
29
|
+
private_key: str # Private key (hex string with 0x prefix)
|
|
30
|
+
mnemonic: str # BIP-39 mnemonic (24 words)
|
|
31
|
+
key_type: KeyType # Key type (evm or solana)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class WalletGenerationOptions:
|
|
36
|
+
"""Wallet generation options"""
|
|
37
|
+
label: Optional[str] = None
|
|
38
|
+
index: int = 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class WalletGenerator:
|
|
42
|
+
"""
|
|
43
|
+
Wallet Generator class
|
|
44
|
+
Provides secure, deterministic wallet generation for AI agents
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def generate(
|
|
49
|
+
key_type: KeyType,
|
|
50
|
+
options: Optional[WalletGenerationOptions] = None
|
|
51
|
+
) -> WalletGenerationResult:
|
|
52
|
+
"""
|
|
53
|
+
Generate a new wallet with random mnemonic
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
key_type: 'evm' for Ethereum/Base or 'solana' for Solana
|
|
57
|
+
options: Optional generation options
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Wallet generation result with DID, keys, and mnemonic
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
>>> wallet = WalletGenerator.generate('evm')
|
|
64
|
+
>>> print(wallet.did)
|
|
65
|
+
did:agent:evm:0x1234...
|
|
66
|
+
"""
|
|
67
|
+
if options is None:
|
|
68
|
+
options = WalletGenerationOptions()
|
|
69
|
+
|
|
70
|
+
# Generate 24-word mnemonic (256-bit entropy)
|
|
71
|
+
mnemonic_generator = Bip39MnemonicGenerator()
|
|
72
|
+
mnemonic = mnemonic_generator.FromWordsNumber(Bip39WordsNum.WORDS_NUM_24)
|
|
73
|
+
|
|
74
|
+
return WalletGenerator.from_mnemonic(str(mnemonic), key_type, options)
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def from_mnemonic(
|
|
78
|
+
mnemonic: str,
|
|
79
|
+
key_type: KeyType,
|
|
80
|
+
options: Optional[WalletGenerationOptions] = None
|
|
81
|
+
) -> WalletGenerationResult:
|
|
82
|
+
"""
|
|
83
|
+
Generate wallet from existing mnemonic
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
mnemonic: BIP-39 mnemonic phrase (12 or 24 words)
|
|
87
|
+
key_type: 'evm' for Ethereum/Base or 'solana' for Solana
|
|
88
|
+
options: Optional generation options
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Wallet generation result with DID and keys
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
ValueError: If mnemonic is invalid
|
|
95
|
+
"""
|
|
96
|
+
if options is None:
|
|
97
|
+
options = WalletGenerationOptions()
|
|
98
|
+
|
|
99
|
+
# Validate mnemonic
|
|
100
|
+
mnemo = Mnemonic("english")
|
|
101
|
+
if not mnemo.check(mnemonic):
|
|
102
|
+
raise ValueError("Invalid mnemonic phrase")
|
|
103
|
+
|
|
104
|
+
if key_type == 'evm':
|
|
105
|
+
return WalletGenerator._generate_evm_wallet(mnemonic, options)
|
|
106
|
+
elif key_type == 'solana':
|
|
107
|
+
return WalletGenerator._generate_solana_wallet(mnemonic, options)
|
|
108
|
+
else:
|
|
109
|
+
raise ValueError(f"Unsupported key type: {key_type}")
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def _generate_evm_wallet(
|
|
113
|
+
mnemonic: str,
|
|
114
|
+
options: WalletGenerationOptions
|
|
115
|
+
) -> WalletGenerationResult:
|
|
116
|
+
"""Generate EVM wallet (Ethereum, Base) using BIP-44 path"""
|
|
117
|
+
# BIP-44 path: m/44'/60'/0'/0/{index}
|
|
118
|
+
seed = Bip39SeedGenerator(mnemonic).Generate()
|
|
119
|
+
bip44_ctx = Bip44.FromSeed(seed, Bip44Coins.ETHEREUM)
|
|
120
|
+
|
|
121
|
+
# Derive account
|
|
122
|
+
bip44_acc = bip44_ctx.Purpose().Coin().Account(0)
|
|
123
|
+
bip44_chg = bip44_acc.Change(Bip44Changes.CHAIN_EXT)
|
|
124
|
+
bip44_addr = bip44_chg.AddressIndex(options.index)
|
|
125
|
+
|
|
126
|
+
# Get private key
|
|
127
|
+
private_key_bytes = bip44_addr.PrivateKey().Raw().ToBytes()
|
|
128
|
+
private_key_hex = '0x' + private_key_bytes.hex()
|
|
129
|
+
|
|
130
|
+
# Create account from private key
|
|
131
|
+
account = Account.from_key(private_key_bytes)
|
|
132
|
+
address = account.address
|
|
133
|
+
|
|
134
|
+
did = f"did:agent:evm:{address}"
|
|
135
|
+
|
|
136
|
+
return WalletGenerationResult(
|
|
137
|
+
did=did,
|
|
138
|
+
address=address,
|
|
139
|
+
private_key=private_key_hex,
|
|
140
|
+
mnemonic=mnemonic,
|
|
141
|
+
key_type='evm'
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def _generate_solana_wallet(
|
|
146
|
+
mnemonic: str,
|
|
147
|
+
options: WalletGenerationOptions
|
|
148
|
+
) -> WalletGenerationResult:
|
|
149
|
+
"""Generate Solana wallet using BIP-44 path"""
|
|
150
|
+
# BIP-44 path: m/44'/501'/{index}'/0'
|
|
151
|
+
seed = Bip39SeedGenerator(mnemonic).Generate()
|
|
152
|
+
bip44_ctx = Bip44.FromSeed(seed, Bip44Coins.SOLANA)
|
|
153
|
+
|
|
154
|
+
# Derive account
|
|
155
|
+
bip44_acc = bip44_ctx.Purpose().Coin().Account(options.index)
|
|
156
|
+
bip44_chg = bip44_acc.Change(Bip44Changes.CHAIN_EXT)
|
|
157
|
+
|
|
158
|
+
# Get private key (first 32 bytes for ed25519)
|
|
159
|
+
private_key_bytes = bip44_chg.PrivateKey().Raw().ToBytes()[:32]
|
|
160
|
+
private_key_hex = '0x' + private_key_bytes.hex()
|
|
161
|
+
|
|
162
|
+
# Create ed25519 key pair
|
|
163
|
+
signing_key = nacl.signing.SigningKey(private_key_bytes)
|
|
164
|
+
verify_key = signing_key.verify_key
|
|
165
|
+
|
|
166
|
+
# Encode public key as base58
|
|
167
|
+
address = base58.b58encode(bytes(verify_key)).decode('ascii')
|
|
168
|
+
|
|
169
|
+
did = f"did:agent:sol:{address}"
|
|
170
|
+
|
|
171
|
+
return WalletGenerationResult(
|
|
172
|
+
did=did,
|
|
173
|
+
address=address,
|
|
174
|
+
private_key=private_key_hex,
|
|
175
|
+
mnemonic=mnemonic,
|
|
176
|
+
key_type='solana'
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
@staticmethod
|
|
180
|
+
def validate_did(did: str) -> bool:
|
|
181
|
+
"""
|
|
182
|
+
Validate DID format
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
did: DID string to validate
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
True if DID format is valid
|
|
189
|
+
"""
|
|
190
|
+
import re
|
|
191
|
+
|
|
192
|
+
evm_pattern = r'^did:agent:evm:0x[a-fA-F0-9]{40}$'
|
|
193
|
+
sol_pattern = r'^did:agent:sol:[1-9A-HJ-NP-Za-km-z]{32,44}$'
|
|
194
|
+
|
|
195
|
+
return bool(re.match(evm_pattern, did) or re.match(sol_pattern, did))
|
|
196
|
+
|
|
197
|
+
@staticmethod
|
|
198
|
+
def did_to_address(did: str) -> str:
|
|
199
|
+
"""
|
|
200
|
+
Extract address from DID
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
did: DID string
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Address extracted from DID
|
|
207
|
+
|
|
208
|
+
Raises:
|
|
209
|
+
ValueError: If DID format is invalid
|
|
210
|
+
"""
|
|
211
|
+
parts = did.split(':')
|
|
212
|
+
if len(parts) != 4 or parts[0] != 'did' or parts[1] != 'agent':
|
|
213
|
+
raise ValueError('Invalid DID format')
|
|
214
|
+
return parts[3]
|
|
215
|
+
|
|
216
|
+
@staticmethod
|
|
217
|
+
def did_to_key_type(did: str) -> KeyType:
|
|
218
|
+
"""
|
|
219
|
+
Extract key type from DID
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
did: DID string
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Key type extracted from DID
|
|
226
|
+
|
|
227
|
+
Raises:
|
|
228
|
+
ValueError: If DID format is invalid
|
|
229
|
+
"""
|
|
230
|
+
parts = did.split(':')
|
|
231
|
+
if len(parts) != 4 or parts[0] != 'did' or parts[1] != 'agent':
|
|
232
|
+
raise ValueError('Invalid DID format')
|
|
233
|
+
|
|
234
|
+
key_type = parts[2]
|
|
235
|
+
if key_type == 'evm':
|
|
236
|
+
return 'evm'
|
|
237
|
+
if key_type == 'sol':
|
|
238
|
+
return 'solana'
|
|
239
|
+
|
|
240
|
+
raise ValueError(f'Invalid key type in DID: {key_type}')
|
xache/errors.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exception classes for Xache SDK
|
|
3
|
+
Maps to API error codes per LLD §2.1
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Optional, Dict, Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class XacheError(Exception):
|
|
10
|
+
"""Base Xache error class"""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
code: str,
|
|
15
|
+
message: str,
|
|
16
|
+
status_code: int,
|
|
17
|
+
details: Optional[Dict[str, Any]] = None,
|
|
18
|
+
request_id: Optional[str] = None,
|
|
19
|
+
):
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.code = code
|
|
22
|
+
self.message = message
|
|
23
|
+
self.status_code = status_code
|
|
24
|
+
self.details = details or {}
|
|
25
|
+
self.request_id = request_id
|
|
26
|
+
|
|
27
|
+
def __str__(self) -> str:
|
|
28
|
+
msg = f"[{self.code}] {self.message}"
|
|
29
|
+
if self.request_id:
|
|
30
|
+
msg += f" (request_id: {self.request_id})"
|
|
31
|
+
return msg
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class UnauthenticatedError(XacheError):
|
|
35
|
+
"""Authentication error (401)"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
message: str,
|
|
40
|
+
details: Optional[Dict[str, Any]] = None,
|
|
41
|
+
request_id: Optional[str] = None,
|
|
42
|
+
):
|
|
43
|
+
super().__init__("UNAUTHENTICATED", message, 401, details, request_id)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PaymentRequiredError(XacheError):
|
|
47
|
+
"""Payment required error (402)"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
message: str,
|
|
52
|
+
challenge_id: str,
|
|
53
|
+
amount: str,
|
|
54
|
+
chain_hint: str,
|
|
55
|
+
pay_to: str,
|
|
56
|
+
description: str,
|
|
57
|
+
request_id: Optional[str] = None,
|
|
58
|
+
):
|
|
59
|
+
details = {
|
|
60
|
+
"challenge_id": challenge_id,
|
|
61
|
+
"amount": amount,
|
|
62
|
+
"chain_hint": chain_hint,
|
|
63
|
+
"pay_to": pay_to,
|
|
64
|
+
"description": description,
|
|
65
|
+
}
|
|
66
|
+
super().__init__("PAYMENT_REQUIRED", message, 402, details, request_id)
|
|
67
|
+
self.challenge_id = challenge_id
|
|
68
|
+
self.amount = amount
|
|
69
|
+
self.chain_hint = chain_hint
|
|
70
|
+
self.pay_to = pay_to
|
|
71
|
+
self.description = description
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class RateLimitedError(XacheError):
|
|
75
|
+
"""Rate limited error (429)"""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
message: str,
|
|
80
|
+
reset_at: Optional[str] = None,
|
|
81
|
+
details: Optional[Dict[str, Any]] = None,
|
|
82
|
+
request_id: Optional[str] = None,
|
|
83
|
+
):
|
|
84
|
+
error_details = details or {}
|
|
85
|
+
if reset_at:
|
|
86
|
+
error_details["reset_at"] = reset_at
|
|
87
|
+
super().__init__("RATE_LIMITED", message, 429, error_details, request_id)
|
|
88
|
+
self.reset_at = reset_at
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class BudgetExceededError(XacheError):
|
|
92
|
+
"""Budget exceeded error (400)"""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
message: str,
|
|
97
|
+
details: Optional[Dict[str, Any]] = None,
|
|
98
|
+
request_id: Optional[str] = None,
|
|
99
|
+
):
|
|
100
|
+
super().__init__("BUDGET_EXCEEDED", message, 400, details, request_id)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class InvalidInputError(XacheError):
|
|
104
|
+
"""Invalid input error (400)"""
|
|
105
|
+
|
|
106
|
+
def __init__(
|
|
107
|
+
self,
|
|
108
|
+
message: str,
|
|
109
|
+
details: Optional[Dict[str, Any]] = None,
|
|
110
|
+
request_id: Optional[str] = None,
|
|
111
|
+
):
|
|
112
|
+
super().__init__("INVALID_INPUT", message, 400, details, request_id)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ConflictError(XacheError):
|
|
116
|
+
"""Conflict error (409)"""
|
|
117
|
+
|
|
118
|
+
def __init__(
|
|
119
|
+
self,
|
|
120
|
+
message: str,
|
|
121
|
+
details: Optional[Dict[str, Any]] = None,
|
|
122
|
+
request_id: Optional[str] = None,
|
|
123
|
+
):
|
|
124
|
+
super().__init__("CONFLICT", message, 409, details, request_id)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class RetryLaterError(XacheError):
|
|
128
|
+
"""Retry later error (503)"""
|
|
129
|
+
|
|
130
|
+
def __init__(
|
|
131
|
+
self,
|
|
132
|
+
message: str,
|
|
133
|
+
details: Optional[Dict[str, Any]] = None,
|
|
134
|
+
request_id: Optional[str] = None,
|
|
135
|
+
):
|
|
136
|
+
super().__init__("RETRY_LATER", message, 503, details, request_id)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class InternalError(XacheError):
|
|
140
|
+
"""Internal server error (500)"""
|
|
141
|
+
|
|
142
|
+
def __init__(
|
|
143
|
+
self,
|
|
144
|
+
message: str,
|
|
145
|
+
details: Optional[Dict[str, Any]] = None,
|
|
146
|
+
request_id: Optional[str] = None,
|
|
147
|
+
):
|
|
148
|
+
super().__init__("INTERNAL", message, 500, details, request_id)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class NetworkError(Exception):
|
|
152
|
+
"""Network error (not from API)"""
|
|
153
|
+
|
|
154
|
+
def __init__(self, message: str, original_error: Optional[Exception] = None):
|
|
155
|
+
super().__init__(message)
|
|
156
|
+
self.original_error = original_error
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def create_error_from_response(
|
|
160
|
+
code: str,
|
|
161
|
+
message: str,
|
|
162
|
+
status_code: int,
|
|
163
|
+
details: Optional[Dict[str, Any]] = None,
|
|
164
|
+
request_id: Optional[str] = None,
|
|
165
|
+
) -> XacheError:
|
|
166
|
+
"""Create appropriate error from API response"""
|
|
167
|
+
error_map = {
|
|
168
|
+
"UNAUTHENTICATED": UnauthenticatedError,
|
|
169
|
+
"RATE_LIMITED": RateLimitedError,
|
|
170
|
+
"BUDGET_EXCEEDED": BudgetExceededError,
|
|
171
|
+
"INVALID_INPUT": InvalidInputError,
|
|
172
|
+
"CONFLICT": ConflictError,
|
|
173
|
+
"RETRY_LATER": RetryLaterError,
|
|
174
|
+
"INTERNAL": InternalError,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
error_class = error_map.get(code)
|
|
178
|
+
if error_class:
|
|
179
|
+
if code == "RATE_LIMITED":
|
|
180
|
+
reset_at = details.get("reset_at") if details else None
|
|
181
|
+
return error_class(message, reset_at, details, request_id)
|
|
182
|
+
return error_class(message, details, request_id)
|
|
183
|
+
|
|
184
|
+
return XacheError(code, message, status_code, details, request_id)
|