diff --git a/cli/handlers/market.py b/cli/handlers/market.py index 63d84d6c..d1cb55f3 100644 --- a/cli/handlers/market.py +++ b/cli/handlers/market.py @@ -282,3 +282,108 @@ def handle_market_gpu_list(args, default_coordinator_url, output_format): except Exception as e: print(f"Error listing GPUs: {e}") sys.exit(1) + + +def handle_market_buy(args, default_rpc_url, read_password, render_mapping): + """Handle marketplace buy command with blockchain transaction.""" + rpc_url = getattr(args, 'rpc_url', default_rpc_url) + + if not args.item or not args.wallet: + print("Error: --item and --wallet are required") + sys.exit(1) + + # Get password for signing + password = read_password(args) + if not password: + print("Error: Password is required for signing") + sys.exit(1) + + # Load wallet and get address + from pathlib import Path + import json + from cryptography.hazmat.primitives.asymmetric import ed25519 + + keystore_dir = Path("/var/lib/aitbc/keystore") + sender_keystore = keystore_dir / f"{args.wallet}.json" + + if not sender_keystore.exists(): + print(f"Error: Wallet '{args.wallet}' not found") + sys.exit(1) + + with open(sender_keystore) as f: + sender_data = json.load(f) + sender_address = sender_data['address'] + + # Decrypt private key for signing + try: + sys.path.insert(0, "/opt/aitbc/cli") + from aitbc_cli import decrypt_private_key + private_key_hex = decrypt_private_key(sender_keystore, password) + private_key = ed25519.Ed25519PrivateKey.from_private_bytes(bytes.fromhex(private_key_hex)) + except Exception as e: + print(f"Error decrypting wallet: {e}") + sys.exit(1) + + # Get chain_id + try: + from sys.path import insert + insert(0, "/opt/aitbc") + from aitbc_cli.utils.chain_id import get_chain_id + chain_id = get_chain_id(rpc_url, override=None, timeout=5) + except Exception: + chain_id = "ait-testnet" + + # Get actual nonce from blockchain + actual_nonce = 0 + try: + account_data = requests.get(f"{rpc_url}/rpc/account/{sender_address}", timeout=5).json() + actual_nonce = account_data.get("nonce", 0) + except Exception: + actual_nonce = 0 + + # Get GPU listing details + try: + coordinator_url = "http://localhost:8000" + gpu_response = requests.get(f"{coordinator_url}/v1/marketplace/gpu/{args.item}", timeout=10) + if gpu_response.status_code == 200: + gpu_data = gpu_response.json() + price = int(gpu_data.get('price_per_hour', 0) * 1000000) # Convert to AIT + else: + price = 1000000 # Default price + except Exception: + price = 1000000 # Default price + + # Create transaction with marketplace purchase payload + transaction = { + "type": "TRANSFER", + "chain_id": chain_id, + "from": sender_address, + "nonce": actual_nonce, + "fee": 10, + "payload": { + "recipient": gpu_data.get('owner_address', 'ait0000000000000000000000000000000000000000'), + "amount": price, + "item_id": args.item, + "action": "buy" + } + } + + # Sign transaction + message = json.dumps(transaction, sort_keys=True).encode() + signature = private_key.sign(message) + transaction["signature"] = signature.hex() + + print(f"Submitting purchase transaction to {rpc_url}...") + try: + response = requests.post(f"{rpc_url}/rpc/transaction", json=transaction, timeout=30) + if response.status_code == 200: + result = response.json() + print("Purchase transaction submitted successfully") + render_mapping("Transaction:", result) + else: + print(f"Purchase failed: {response.status_code}") + print(f"Error: {response.text}") + sys.exit(1) + except Exception as e: + print(f"Error submitting purchase: {e}") + sys.exit(1)